feat(agent): hierarchical executor with path resolve, runtime probe, quiet UI
Make Zed Agent closer to Cursor: deterministic DevOps path index, live Traefik port probe before blind edits, stop-after-edit, and quieter Russian progress.
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
"""Compact, readable progress lines for Zed agent chat."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
_MODEL_LABEL = {
|
||||
"novita-planner": "Max",
|
||||
"novita-verifier": "verify",
|
||||
"a-medium-code": "Coder",
|
||||
"a-simple": "Llama",
|
||||
"b-complex": "DeepSeek",
|
||||
"c-complex": "Max",
|
||||
"approved-plan": "план",
|
||||
}
|
||||
|
||||
_NOISE = (
|
||||
"ждём LiteLLM",
|
||||
"payload:",
|
||||
"ctx ",
|
||||
"ctx-",
|
||||
"worker #",
|
||||
"волна ",
|
||||
"параллельно",
|
||||
"bytes≈",
|
||||
"upstream_stream",
|
||||
"non-stream",
|
||||
)
|
||||
|
||||
|
||||
def model_label(model: str | None) -> str:
|
||||
if not model:
|
||||
return "модель"
|
||||
m = str(model)
|
||||
return _MODEL_LABEL.get(m, m.split("/")[-1][:18])
|
||||
|
||||
|
||||
def short_path(path: str | None, *, max_parts: int = 3) -> str:
|
||||
if not path:
|
||||
return ""
|
||||
p = str(path).replace("/", "\\").rstrip("\\")
|
||||
parts = [x for x in p.split("\\") if x]
|
||||
if len(parts) <= max_parts:
|
||||
return "\\".join(parts)
|
||||
return "\\".join(parts[-max_parts:])
|
||||
|
||||
|
||||
def plan_paths(plan: dict[str, Any] | None) -> list[str]:
|
||||
out: list[str] = []
|
||||
for item in (plan or {}).get("subtasks") or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
for p in item.get("paths") or []:
|
||||
if isinstance(p, str) and p.strip() and p not in out:
|
||||
out.append(p.strip())
|
||||
return out
|
||||
|
||||
|
||||
def is_noise(line: str) -> bool:
|
||||
s = (line or "").strip()
|
||||
if not s:
|
||||
return True
|
||||
low = s.lower()
|
||||
return any(n.lower() in low for n in _NOISE)
|
||||
|
||||
|
||||
def humanize_line(line: str) -> str | None:
|
||||
"""Map internal progress → short Russian status. None = drop."""
|
||||
s = (line or "").strip()
|
||||
if not s or is_noise(s):
|
||||
return None
|
||||
low = s.lower()
|
||||
|
||||
if s in ("план…", "план...", "план"):
|
||||
return "Планирую задачу"
|
||||
if "план через" in low or low.startswith("hierarchical:"):
|
||||
return "Планирую задачу"
|
||||
if "утверждён" in low or "утвержден" in low:
|
||||
m = re.search(r"(\d+)\s*подзадач", s)
|
||||
n = m.group(1) if m else ""
|
||||
return f"План утверждён{f' · {n} шаг(а)' if n else ''}"
|
||||
if "отменён" in low or "отменен" in low:
|
||||
return "План отменён"
|
||||
if "правки от пользователя" in low or "пересборк" in low:
|
||||
return "Пересобираю план по правкам"
|
||||
if "ожидает утверждения" in low or "ждём утверждения" in low or "awaiting" in low:
|
||||
return "План готов — подтверди («ok»)"
|
||||
if "agent: план готов" in low or "executor с tools" in low:
|
||||
return "Перехожу к выполнению"
|
||||
if "готово за" in low:
|
||||
return "Готово"
|
||||
if "json битый" in low or "repair" in low:
|
||||
return "Чиню формат плана"
|
||||
if low.startswith("path_index") or "path_index hit" in low:
|
||||
return None # shown via dedicated path line
|
||||
if "path_resolve" in low or "поиск файл" in low:
|
||||
return "Ищу файлы в проекте"
|
||||
if "runtime_probe" in low or "runtime:" in low:
|
||||
return "Проверяю runtime"
|
||||
if "force edit" in low:
|
||||
return "Правлю файл"
|
||||
if "kickstart" in low:
|
||||
return "Читаю файл"
|
||||
if "escalate" in low or "mid-loop" in low:
|
||||
return None
|
||||
if "408" in s or "timeout" in low:
|
||||
return "Таймаут модели — продолжаю иначе"
|
||||
if "400" in s and "bad" in low:
|
||||
return None
|
||||
# strip technical prefixes
|
||||
s = re.sub(r"^план:\s*", "", s, flags=re.I)
|
||||
s = re.sub(r"^agent:\s*", "", s, flags=re.I)
|
||||
if len(s) > 90:
|
||||
s = s[:87] + "…"
|
||||
return s
|
||||
|
||||
|
||||
def format_progress_block(lines: list[str]) -> str:
|
||||
"""Final/static progress block — numbered, no junk."""
|
||||
steps: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for raw in lines or []:
|
||||
h = humanize_line(str(raw))
|
||||
if not h or h in seen:
|
||||
continue
|
||||
seen.add(h)
|
||||
steps.append(h)
|
||||
if not steps:
|
||||
return ""
|
||||
body = "\n".join(f"{i}. {t}" for i, t in enumerate(steps, 1))
|
||||
return f"**Ход**\n{body}\n\n---\n\n"
|
||||
|
||||
|
||||
def stream_header() -> str:
|
||||
return "**Agent**\n"
|
||||
|
||||
|
||||
def stream_step(text: str) -> str:
|
||||
return f"· {text}\n"
|
||||
|
||||
|
||||
def execution_banner(
|
||||
*,
|
||||
path_mode: bool,
|
||||
plan: dict[str, Any] | None,
|
||||
model: str | None,
|
||||
) -> str:
|
||||
"""Shown once after plan approve — what happens next."""
|
||||
paths = plan_paths(plan)
|
||||
lines = ["", "---", "", "**Выполнение**"]
|
||||
facts = (plan or {}).get("runtime_facts") if isinstance(plan, dict) else None
|
||||
step = 1
|
||||
if isinstance(facts, dict) and (facts.get("host") or facts.get("hint")):
|
||||
host = facts.get("host") or "?"
|
||||
status = facts.get("http_status")
|
||||
suggest = facts.get("suggested_backend_url")
|
||||
bit = f"{host}"
|
||||
if status is not None:
|
||||
bit += f" → HTTP {status}"
|
||||
if suggest:
|
||||
bit += f" · fix `{suggest}`"
|
||||
lines.append(f"{step}. Runtime: {bit}")
|
||||
step += 1
|
||||
if paths:
|
||||
shown = ", ".join(f"`{short_path(p)}`" for p in paths[:2])
|
||||
src = ""
|
||||
tasks = (plan or {}).get("subtasks") or []
|
||||
if tasks and isinstance(tasks[0], dict):
|
||||
ps = str(tasks[0].get("path_source") or "")
|
||||
if ps == "index":
|
||||
src = " · индекс"
|
||||
elif ps == "find":
|
||||
src = " · поиск"
|
||||
lines.append(f"{step}. Файл{src}: {shown}")
|
||||
step += 1
|
||||
if path_mode:
|
||||
lines.append(f"{step}. Уточняю путь в репозитории…")
|
||||
else:
|
||||
lines.append(f"{step}. Читаю → правлю")
|
||||
elif path_mode:
|
||||
lines.append(f"{step}. Ищу нужные файлы…")
|
||||
else:
|
||||
lines.append(f"{step}. Запуск ({model_label(model)})")
|
||||
lines.append("")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def tool_status(tool_names: list[str] | None, *, path: str | None = None) -> str:
|
||||
names = [str(n).lower() for n in (tool_names or []) if n]
|
||||
if any("edit" in n or "write" in n for n in names):
|
||||
base = "Правлю"
|
||||
elif any("read" in n for n in names):
|
||||
base = "Читаю"
|
||||
elif any("find" in n or "list" in n or "grep" in n for n in names):
|
||||
base = "Ищу"
|
||||
else:
|
||||
base = "Инструмент"
|
||||
if path:
|
||||
return stream_step(f"{base} `{short_path(path)}`")
|
||||
return stream_step(base)
|
||||
Reference in New Issue
Block a user