generated from coulomb/repo-seed
feat(WARDEN-WP-0020): worker drafts real route answers in dry-run (T3 groundwork)
build_plans now computes the concrete routing answer for each route_answer action in-process (reuses the catalog; read-only, no subprocess/network) and render_plans shows it as a `draft:` line. The dry-run demonstrates the actual answer the executor (T3) will send, not just an intent. RuleBrain stays the default; the llm-connect brain (T2) is gated on llm-connect being operational + its /execute contract. 230 tests, lint clean. Live dry-run verified against the real inbox. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -149,9 +149,43 @@ class HubClient:
|
|||||||
return data if isinstance(data, list) else []
|
return data if isinstance(data, list) else []
|
||||||
|
|
||||||
|
|
||||||
|
def draft_route_answer(query: str) -> str:
|
||||||
|
"""Compute the routing answer the worker would send for a query. Read-only.
|
||||||
|
|
||||||
|
Reuses the routing catalog in-process (no subprocess, no network) so the dry-run
|
||||||
|
shows the concrete answer the executor (T3) will send, not just an intent.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from warden.routing.catalog import load_catalog
|
||||||
|
|
||||||
|
matches = load_catalog().find(query, limit=1)
|
||||||
|
except Exception: # noqa: BLE001 — never let a lookup failure break planning
|
||||||
|
return ""
|
||||||
|
if not matches:
|
||||||
|
return f"No routing match for {query!r}; try `warden route list --all`."
|
||||||
|
e = matches[0]
|
||||||
|
role = "issue" if e.warden_executes else ("assist" if e.exec_capable else "route")
|
||||||
|
parts = [f"{e.id} — owner {e.owner_repo} ({e.subsystem}), warden role: {role}."]
|
||||||
|
if e.warden_executes and e.cert_command:
|
||||||
|
parts.append(f"Run: {e.cert_command}.")
|
||||||
|
elif e.has_native_exec:
|
||||||
|
parts.append(f"Primary: {e.exec_command}.")
|
||||||
|
elif e.exec_capable:
|
||||||
|
parts.append(f"Proxy: warden access {e.id} --fetch (as the caller).")
|
||||||
|
parts.append(f"See {e.wiki_ref}.")
|
||||||
|
return " ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
def build_plans(messages: List[dict], brain: Brain) -> List[WorkerPlan]:
|
def build_plans(messages: List[dict], brain: Brain) -> List[WorkerPlan]:
|
||||||
"""Plan every message and apply the guardrail pass. Pure — no execution."""
|
"""Plan every message, attach computed route answers, and apply the guardrail pass."""
|
||||||
return [_guardrail(brain.plan(m), m) for m in messages]
|
plans: List[WorkerPlan] = []
|
||||||
|
for m in messages:
|
||||||
|
plan = brain.plan(m)
|
||||||
|
for a in plan.actions:
|
||||||
|
if a.kind == "route_answer" and "answer" not in a.payload:
|
||||||
|
a.payload["answer"] = draft_route_answer(a.payload.get("query", m.get("subject", "")))
|
||||||
|
plans.append(_guardrail(plan, m))
|
||||||
|
return plans
|
||||||
|
|
||||||
|
|
||||||
def render_plans(plans: List[WorkerPlan]) -> str:
|
def render_plans(plans: List[WorkerPlan]) -> str:
|
||||||
@@ -167,6 +201,8 @@ def render_plans(plans: List[WorkerPlan]) -> str:
|
|||||||
for a in p.actions:
|
for a in p.actions:
|
||||||
mark = "→" if a.risk == "safe" else "⚠"
|
mark = "→" if a.risk == "safe" else "⚠"
|
||||||
lines.append(f" {mark} {a.kind}: {a.summary}")
|
lines.append(f" {mark} {a.kind}: {a.summary}")
|
||||||
|
if a.payload.get("answer"):
|
||||||
|
lines.append(f" draft: {a.payload['answer']}")
|
||||||
if a.risk == "escalate":
|
if a.risk == "escalate":
|
||||||
lines.append(f" escalated: {a.reason}")
|
lines.append(f" escalated: {a.reason}")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|||||||
@@ -92,6 +92,13 @@ def test_safe_reply_passes_guardrail():
|
|||||||
|
|
||||||
# --- rendering ---------------------------------------------------------------
|
# --- rendering ---------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_build_plans_attaches_route_answer():
|
||||||
|
# The npm question resolves against the real catalog → a concrete drafted answer.
|
||||||
|
[plan] = build_plans([_msg(subject="where do I get an npm token?")], RuleBrain())
|
||||||
|
assert plan.actions and plan.actions[0].kind == "route_answer"
|
||||||
|
assert plan.actions[0].payload.get("answer") # non-empty computed answer
|
||||||
|
|
||||||
|
|
||||||
def test_render_empty():
|
def test_render_empty():
|
||||||
assert "inbox empty" in render_plans([])
|
assert "inbox empty" in render_plans([])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user