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,430 @@
|
||||
"""Path resolve: deterministic index first, cheap find_* tools if gaps remain."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
log = logging.getLogger("ai-router")
|
||||
|
||||
# Absolute Windows root for EventHubDevOps (Zed multi-root).
|
||||
_DEFAULT_DEVOPS_ROOT = r"C:\Users\alexc\IdeaProjects\eventHub\EventHubDevOps"
|
||||
|
||||
# keyword groups → relative paths under DevOps root (first match wins per group).
|
||||
# Order matters: more specific rules first.
|
||||
_INDEX_RULES: list[tuple[tuple[str, ...], tuple[str, ...]]] = [
|
||||
(
|
||||
("loadtest", "load-test", "load_test"),
|
||||
(r"ift\traefik\dynamic_conf.loadtest.yml",),
|
||||
),
|
||||
(
|
||||
(
|
||||
"traefik",
|
||||
"dynamic_conf",
|
||||
"router",
|
||||
"middleware",
|
||||
"calentiq",
|
||||
"observer",
|
||||
"host(",
|
||||
"ift.calentiq",
|
||||
"stage.calentiq",
|
||||
),
|
||||
(r"ift\traefik\dynamic_conf.yml",),
|
||||
),
|
||||
(
|
||||
("portainer",),
|
||||
(r"ift\docker-compose.portainer.yml",),
|
||||
),
|
||||
(
|
||||
("grafana", "dashboard"),
|
||||
(
|
||||
r"ift\observability\grafana\provisioning\dashboards\dashboard.yml",
|
||||
r"ift\observability\grafana\provisioning\datasources\prometheus.yml",
|
||||
),
|
||||
),
|
||||
(
|
||||
("prometheus", "observability"),
|
||||
(r"ift\observability\prometheus.yml",),
|
||||
),
|
||||
(
|
||||
("admin", "compose.admin", "docker-compose.admin"),
|
||||
(r"ift\docker-compose.admin.yml",),
|
||||
),
|
||||
(
|
||||
("client", "compose.client", "docker-compose.client"),
|
||||
(r"ift\docker-compose.client.yml",),
|
||||
),
|
||||
(
|
||||
("compose", "swarm", "stack", "docker-compose", "service"),
|
||||
(r"ift\docker-compose.core.yml",),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _join_under(root: str, rel: str) -> str:
|
||||
root = root.rstrip("\\/")
|
||||
rel = rel.replace("/", "\\").lstrip("\\")
|
||||
return root + "\\" + rel
|
||||
|
||||
|
||||
def _norm(path: str) -> str:
|
||||
return path.replace("/", "\\").rstrip("\\").lower()
|
||||
|
||||
|
||||
def _blob(plan: dict[str, Any] | None) -> str:
|
||||
return json.dumps(plan or {}, ensure_ascii=False).lower()
|
||||
|
||||
|
||||
def devops_root_from_plan(plan: dict[str, Any] | None) -> str:
|
||||
blob = json.dumps(plan or {}, ensure_ascii=False)
|
||||
m = re.search(r"([A-Za-z]:\\[^\"'\n\r]*?EventHubDevOps)", blob)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return _DEFAULT_DEVOPS_ROOT
|
||||
|
||||
|
||||
def looks_devops(plan: dict[str, Any] | None) -> bool:
|
||||
b = _blob(plan)
|
||||
return any(
|
||||
k in b
|
||||
for k in (
|
||||
"traefik",
|
||||
"calentiq",
|
||||
"eventhubdevops",
|
||||
"docker-compose",
|
||||
"swarm",
|
||||
"observer",
|
||||
"ift.",
|
||||
"devops",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def match_index_paths(plan: dict[str, Any] | None) -> list[str]:
|
||||
"""Return absolute paths from keyword index (may be empty)."""
|
||||
if not looks_devops(plan):
|
||||
return []
|
||||
root = devops_root_from_plan(plan)
|
||||
text = _blob(plan)
|
||||
found: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for keys, rels in _INDEX_RULES:
|
||||
if not any(k in text for k in keys):
|
||||
continue
|
||||
for rel in rels:
|
||||
abs_p = _join_under(root, rel)
|
||||
nk = _norm(abs_p)
|
||||
if nk in seen:
|
||||
continue
|
||||
seen.add(nk)
|
||||
found.append(abs_p)
|
||||
# one rule group is usually enough for a focused plan
|
||||
if found:
|
||||
break
|
||||
return found
|
||||
|
||||
|
||||
def apply_deterministic_path_index(
|
||||
plan: dict[str, Any] | None,
|
||||
) -> tuple[dict[str, Any] | None, list[str]]:
|
||||
"""Fill plan.paths from index. Returns (plan, unresolved_queries).
|
||||
|
||||
unresolved_queries empty ⇒ index covered the goal; else cheap find_* needed.
|
||||
"""
|
||||
if not isinstance(plan, dict):
|
||||
return plan, []
|
||||
out = dict(plan)
|
||||
tasks = out.get("subtasks")
|
||||
if not isinstance(tasks, list) or not tasks:
|
||||
# still try goal-level match
|
||||
indexed = match_index_paths(out)
|
||||
if indexed:
|
||||
out["subtasks"] = [
|
||||
{
|
||||
"id": "1",
|
||||
"prompt": str(out.get("user_goal") or "edit")[:200],
|
||||
"worker_tier": "medium_code",
|
||||
"paths": indexed,
|
||||
"edit_goal": "",
|
||||
"constraints": [],
|
||||
}
|
||||
]
|
||||
out["path_resolve"] = "index"
|
||||
return out, []
|
||||
if looks_devops(out):
|
||||
q = _queries_from_text(_blob(out))
|
||||
out["path_resolve_queries"] = q
|
||||
return out, q
|
||||
return out, []
|
||||
|
||||
indexed = match_index_paths(out)
|
||||
unresolved: list[str] = []
|
||||
fixed: list[Any] = []
|
||||
for item in tasks:
|
||||
if not isinstance(item, dict):
|
||||
fixed.append(item)
|
||||
continue
|
||||
it = dict(item)
|
||||
paths = [
|
||||
p
|
||||
for p in (it.get("paths") or [])
|
||||
if isinstance(p, str) and p.strip()
|
||||
]
|
||||
# Prefer index hits when devops; replace empty/weak paths
|
||||
if indexed:
|
||||
it["paths"] = indexed
|
||||
it["path_source"] = "index"
|
||||
elif not paths:
|
||||
q = _queries_from_text(
|
||||
str(it.get("prompt") or "")
|
||||
+ " "
|
||||
+ str(it.get("edit_goal") or "")
|
||||
+ " "
|
||||
+ _blob(out)
|
||||
)
|
||||
unresolved.extend(q)
|
||||
it["paths"] = []
|
||||
it["path_source"] = "missing"
|
||||
else:
|
||||
it["path_source"] = "planner"
|
||||
fixed.append(it)
|
||||
out["subtasks"] = fixed
|
||||
if indexed:
|
||||
out["path_resolve"] = "index"
|
||||
out["path_resolve_queries"] = []
|
||||
log.info("path_index hit → %s", indexed)
|
||||
return out, []
|
||||
# Planner had paths but index missed — if devops, still verify via find
|
||||
if looks_devops(out) and not indexed:
|
||||
unresolved = unresolved or _queries_from_text(_blob(out))
|
||||
out["path_resolve_queries"] = unresolved
|
||||
out["path_resolve"] = "needed"
|
||||
return out, unresolved
|
||||
out["path_resolve_queries"] = unresolved
|
||||
if unresolved:
|
||||
out["path_resolve"] = "needed"
|
||||
return out, unresolved
|
||||
|
||||
|
||||
def _queries_from_text(text: str) -> list[str]:
|
||||
low = text.lower()
|
||||
qs: list[str] = []
|
||||
if any(k in low for k in ("traefik", "router", "calentiq", "observer", "dynamic")):
|
||||
qs.append("dynamic_conf.yml")
|
||||
if any(k in low for k in ("compose", "swarm", "docker")):
|
||||
qs.append("docker-compose.core.yml")
|
||||
if "portainer" in low:
|
||||
qs.append("docker-compose.portainer.yml")
|
||||
if "grafana" in low:
|
||||
qs.append("grafana")
|
||||
if "prometheus" in low:
|
||||
qs.append("prometheus.yml")
|
||||
if not qs:
|
||||
qs.append("dynamic_conf.yml")
|
||||
# unique preserve order
|
||||
out: list[str] = []
|
||||
for q in qs:
|
||||
if q not in out:
|
||||
out.append(q)
|
||||
return out[:3]
|
||||
|
||||
|
||||
def enrich_plan_from_discovery_tools(
|
||||
plan: dict[str, Any] | None,
|
||||
messages: list[dict[str, Any]] | None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Pull absolute paths from find_path / list_directory tool results into plan."""
|
||||
if not isinstance(plan, dict):
|
||||
return plan
|
||||
# Never overwrite a good deterministic index hit with noisy find parses
|
||||
if str(plan.get("path_resolve") or "") == "index":
|
||||
return plan
|
||||
tasks = plan.get("subtasks") or []
|
||||
if (
|
||||
isinstance(tasks, list)
|
||||
and tasks
|
||||
and isinstance(tasks[0], dict)
|
||||
and str(tasks[0].get("path_source") or "") == "index"
|
||||
):
|
||||
return plan
|
||||
found = _paths_from_discovery_messages(messages)
|
||||
if not found:
|
||||
return plan
|
||||
out = dict(plan)
|
||||
tasks = list(out.get("subtasks") or [])
|
||||
if not tasks:
|
||||
out["subtasks"] = [
|
||||
{
|
||||
"id": "1",
|
||||
"prompt": "edit",
|
||||
"worker_tier": "medium_code",
|
||||
"paths": found[:3],
|
||||
"path_source": "find",
|
||||
}
|
||||
]
|
||||
else:
|
||||
fixed = []
|
||||
for item in tasks:
|
||||
if not isinstance(item, dict):
|
||||
fixed.append(item)
|
||||
continue
|
||||
it = dict(item)
|
||||
it["paths"] = found[:3]
|
||||
it["path_source"] = "find"
|
||||
fixed.append(it)
|
||||
out["subtasks"] = fixed
|
||||
out["path_resolve"] = "find"
|
||||
out["path_resolve_queries"] = []
|
||||
log.info("path_resolve find → %s", found[:3])
|
||||
return out
|
||||
|
||||
|
||||
def _is_workspace_path(path: str) -> bool:
|
||||
"""Accept only real EventHub project paths — never /etc or null:/etc artifacts."""
|
||||
key = _norm(path)
|
||||
if "eventhubdevops" not in key and "eventhub" not in key:
|
||||
return False
|
||||
if "\\etc\\" in key or key.startswith("etc\\"):
|
||||
return False
|
||||
if "\\nginx\\" in key:
|
||||
return False
|
||||
# Reject 1-letter drive artifacts from YAML like null:/etc → l:\etc
|
||||
if re.match(r"^[a-z]:\\etc\\", key):
|
||||
return False
|
||||
base = key.rsplit("\\", 1)[-1]
|
||||
if base in ("traefik.yml", "traefik.yaml", "docker-compose.yml"):
|
||||
if "\\ift\\" not in key and "\\stage\\" not in key:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _paths_from_discovery_messages(
|
||||
messages: list[dict[str, Any]] | None,
|
||||
) -> list[str]:
|
||||
"""Extract existing file paths from find/list tool results only."""
|
||||
call_ids: set[str] = set()
|
||||
for msg in messages or []:
|
||||
if not isinstance(msg, dict) or msg.get("role") != "assistant":
|
||||
continue
|
||||
for tc in msg.get("tool_calls") or []:
|
||||
if not isinstance(tc, dict):
|
||||
continue
|
||||
name = str((tc.get("function") or {}).get("name") or "").lower()
|
||||
if not any(k in name for k in ("find", "list", "search", "grep", "glob")):
|
||||
continue
|
||||
# Do not treat read_file / edit_file as discovery
|
||||
if any(k in name for k in ("read", "edit", "write", "create", "delete")):
|
||||
continue
|
||||
cid = str(tc.get("id") or "")
|
||||
if cid:
|
||||
call_ids.add(cid)
|
||||
# No discovery tool calls → do not scrape read_file bodies (YAML has /etc/… paths)
|
||||
if not call_ids:
|
||||
return []
|
||||
paths: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for msg in messages or []:
|
||||
if not isinstance(msg, dict) or msg.get("role") != "tool":
|
||||
continue
|
||||
if str(msg.get("tool_call_id") or "") not in call_ids:
|
||||
continue
|
||||
content = str(msg.get("content") or "")
|
||||
low = content.lower()
|
||||
if "not found" in low and len(content) < 200:
|
||||
continue
|
||||
for m in re.finditer(
|
||||
r"([A-Za-z]:[\\/][^\s\"'<>\]\n\r]+\.(?:yml|yaml|toml|md|json|conf))",
|
||||
content,
|
||||
):
|
||||
p = m.group(1).replace("/", "\\")
|
||||
if not _is_workspace_path(p):
|
||||
continue
|
||||
nk = _norm(p)
|
||||
if nk in seen:
|
||||
continue
|
||||
seen.add(nk)
|
||||
paths.append(p)
|
||||
# Also accept plain relative hits that Zed returns under EventHubDevOps
|
||||
for m in re.finditer(
|
||||
r"((?:EventHubDevOps[\\/](?:ift|stage)[\\/][^\s\"'<>\]\n\r]+\.(?:yml|yaml)))",
|
||||
content,
|
||||
re.I,
|
||||
):
|
||||
p = _join_under(
|
||||
_DEFAULT_DEVOPS_ROOT,
|
||||
m.group(1).split("EventHubDevOps", 1)[-1].lstrip("\\/"),
|
||||
)
|
||||
if not _is_workspace_path(p):
|
||||
continue
|
||||
nk = _norm(p)
|
||||
if nk in seen:
|
||||
continue
|
||||
seen.add(nk)
|
||||
paths.append(p)
|
||||
return paths
|
||||
|
||||
|
||||
def discovery_tools_only(tools: list[Any], *, max_tools: int = 3) -> list[Any]:
|
||||
prefer_keys = ("find_path", "find", "list_directory", "list_dir", "grep", "search", "glob")
|
||||
ranked: list[Any] = []
|
||||
for key in prefer_keys:
|
||||
for t in tools or []:
|
||||
if not isinstance(t, dict):
|
||||
continue
|
||||
fn = t.get("function") if isinstance(t.get("function"), dict) else {}
|
||||
name = str(fn.get("name") or t.get("name") or "").lower()
|
||||
if key in name and t not in ranked:
|
||||
ranked.append(t)
|
||||
if len(ranked) >= max_tools:
|
||||
break
|
||||
return ranked[:max_tools]
|
||||
|
||||
|
||||
def pick_path_resolve_model(cfg: dict[str, Any]) -> str:
|
||||
"""Cheap model for find_* only — allow a-simple (unlike edit executor)."""
|
||||
return str(cfg.get("path_resolve_model") or "a-simple")
|
||||
|
||||
|
||||
def path_resolve_needed(plan: dict[str, Any] | None) -> bool:
|
||||
if not isinstance(plan, dict):
|
||||
return False
|
||||
qs = plan.get("path_resolve_queries") or []
|
||||
if qs:
|
||||
return True
|
||||
return str(plan.get("path_resolve") or "") == "needed"
|
||||
|
||||
|
||||
def force_find_path_kickstart(
|
||||
*,
|
||||
model: str,
|
||||
tools: list[Any],
|
||||
queries: list[str],
|
||||
) -> dict[str, Any]:
|
||||
"""Synthetic find_path so Zed searches instead of inventing read_file."""
|
||||
from agent_hier import _synthetic_tool_completion
|
||||
|
||||
tool_name = "find_path"
|
||||
for t in tools or []:
|
||||
if not isinstance(t, dict):
|
||||
continue
|
||||
fn = t.get("function") if isinstance(t.get("function"), dict) else {}
|
||||
name = str(fn.get("name") or "")
|
||||
low = name.lower()
|
||||
if "find" in low or "glob" in low or ("list" in low and "dir" in low):
|
||||
tool_name = name
|
||||
break
|
||||
q = (queries[0] if queries else "dynamic_conf.yml").strip()
|
||||
args: dict[str, Any]
|
||||
if "list" in tool_name.lower():
|
||||
args = {
|
||||
"path": _join_under(_DEFAULT_DEVOPS_ROOT, r"ift\traefik"),
|
||||
}
|
||||
else:
|
||||
args = {"query": q, "path": _DEFAULT_DEVOPS_ROOT}
|
||||
# Zed variants
|
||||
args["glob"] = f"**/{q}" if "." in q else f"**/*{q}*"
|
||||
return _synthetic_tool_completion(
|
||||
model=model, tool_name=tool_name, arguments=args
|
||||
)
|
||||
Reference in New Issue
Block a user