2025-08-08 20:36:44 -07:00
|
|
|
from typing import Any, ClassVar
|
|
|
|
|
|
2026-01-05 00:07:54 -08:00
|
|
|
from rich.text import Text
|
2025-08-08 20:36:44 -07:00
|
|
|
from textual.widgets import Static
|
|
|
|
|
|
|
|
|
|
from .base_renderer import BaseToolRenderer
|
|
|
|
|
from .registry import register_tool_renderer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@register_tool_renderer
|
|
|
|
|
class ViewAgentGraphRenderer(BaseToolRenderer):
|
|
|
|
|
tool_name: ClassVar[str] = "view_agent_graph"
|
|
|
|
|
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2026-01-06 14:10:45 -08:00
|
|
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
|
|
|
status = tool_data.get("status", "unknown")
|
|
|
|
|
|
2026-01-05 00:07:54 -08:00
|
|
|
text = Text()
|
2026-01-06 14:10:45 -08:00
|
|
|
text.append("◇ ", style="#a78bfa")
|
|
|
|
|
text.append("viewing agents graph", style="dim")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
2026-01-06 14:10:45 -08:00
|
|
|
css_classes = cls.get_css_classes(status)
|
2026-01-05 00:07:54 -08:00
|
|
|
return Static(text, classes=css_classes)
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@register_tool_renderer
|
|
|
|
|
class CreateAgentRenderer(BaseToolRenderer):
|
|
|
|
|
tool_name: ClassVar[str] = "create_agent"
|
|
|
|
|
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
|
|
|
args = tool_data.get("args", {})
|
2026-01-06 14:10:45 -08:00
|
|
|
status = tool_data.get("status", "unknown")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
task = args.get("task", "")
|
|
|
|
|
name = args.get("name", "Agent")
|
|
|
|
|
|
2026-01-05 00:07:54 -08:00
|
|
|
text = Text()
|
2026-01-06 14:10:45 -08:00
|
|
|
text.append("◈ ", style="#a78bfa")
|
|
|
|
|
text.append("spawning ", style="dim")
|
|
|
|
|
text.append(name, style="bold #a78bfa")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
if task:
|
2026-01-05 00:07:54 -08:00
|
|
|
text.append("\n ")
|
2026-01-05 09:52:05 -08:00
|
|
|
text.append(task, style="dim")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
2026-01-06 14:10:45 -08:00
|
|
|
css_classes = cls.get_css_classes(status)
|
2026-01-05 00:07:54 -08:00
|
|
|
return Static(text, classes=css_classes)
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@register_tool_renderer
|
|
|
|
|
class SendMessageToAgentRenderer(BaseToolRenderer):
|
|
|
|
|
tool_name: ClassVar[str] = "send_message_to_agent"
|
|
|
|
|
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
|
|
|
args = tool_data.get("args", {})
|
2026-01-06 14:10:45 -08:00
|
|
|
status = tool_data.get("status", "unknown")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
message = args.get("message", "")
|
Agents graph sweep: status taxonomy, stop_agent safety, skill validation
- view_agent_graph status summary now derives buckets from the canonical
Status literal via get_args, so adding a new status in core.agents
auto-flows into the summary. The previous hardcoded five-bucket list
silently omitted "failed" — buckets stopped summing to total whenever
an agent failed.
- stop_agent rejects targets that are already in a terminal status
(completed / stopped / crashed / failed) with a model-readable error
pointing at view_agent_graph and send_message_to_agent. request_stop
unconditionally overwrites status, so without this guard calling
stop_agent on a completed agent erased the "completed" history.
- StopAgentRenderer added — was falling back to the generic key/value
renderer; the rest of the agents_graph tools have purpose-built ones.
- agent_finish root-rejection payload trimmed from
{success, agent_completed, error, parent_notified} to {success, error}.
The lifecycle gate only reads success+agent_completed and they were
always False/False on this branch, so the extra fields were dead weight.
- wait_for_message renames its top-level outcome field from "status" to
"wait_outcome" — "status" overloaded with the coordinator's agent
status literal (which also has "stopped" as a value, different
meaning). Redundant "agent_waiting" boolean dropped (true iff
wait_outcome == "waiting"). Consumer at factory._wait_tool_parked
updated to match.
- send_message_to_agent now refuses self-send with a pointer at think /
agent_finish / finish_scan instead of looping a message into your
own session.
- SendMessageToAgentRenderer read args.get("agent_id") but the tool's
param is target_agent_id, so the TUI silently never showed the target.
Fixed.
- Restored skill validation lost during the SDK migration: skills
module re-exports get_all_skill_names and validate_requested_skills
(excluding internal scan_modes/coordination categories from the
user-selectable set). create_agent now validates skills before
spawning instead of silently accepting unknown names.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:35:57 -07:00
|
|
|
target_agent_id = args.get("target_agent_id", "")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
2026-01-05 00:07:54 -08:00
|
|
|
text = Text()
|
2026-01-06 14:10:45 -08:00
|
|
|
text.append("→ ", style="#60a5fa")
|
Agents graph sweep: status taxonomy, stop_agent safety, skill validation
- view_agent_graph status summary now derives buckets from the canonical
Status literal via get_args, so adding a new status in core.agents
auto-flows into the summary. The previous hardcoded five-bucket list
silently omitted "failed" — buckets stopped summing to total whenever
an agent failed.
- stop_agent rejects targets that are already in a terminal status
(completed / stopped / crashed / failed) with a model-readable error
pointing at view_agent_graph and send_message_to_agent. request_stop
unconditionally overwrites status, so without this guard calling
stop_agent on a completed agent erased the "completed" history.
- StopAgentRenderer added — was falling back to the generic key/value
renderer; the rest of the agents_graph tools have purpose-built ones.
- agent_finish root-rejection payload trimmed from
{success, agent_completed, error, parent_notified} to {success, error}.
The lifecycle gate only reads success+agent_completed and they were
always False/False on this branch, so the extra fields were dead weight.
- wait_for_message renames its top-level outcome field from "status" to
"wait_outcome" — "status" overloaded with the coordinator's agent
status literal (which also has "stopped" as a value, different
meaning). Redundant "agent_waiting" boolean dropped (true iff
wait_outcome == "waiting"). Consumer at factory._wait_tool_parked
updated to match.
- send_message_to_agent now refuses self-send with a pointer at think /
agent_finish / finish_scan instead of looping a message into your
own session.
- SendMessageToAgentRenderer read args.get("agent_id") but the tool's
param is target_agent_id, so the TUI silently never showed the target.
Fixed.
- Restored skill validation lost during the SDK migration: skills
module re-exports get_all_skill_names and validate_requested_skills
(excluding internal scan_modes/coordination categories from the
user-selectable set). create_agent now validates skills before
spawning instead of silently accepting unknown names.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:35:57 -07:00
|
|
|
if target_agent_id:
|
|
|
|
|
text.append(f"to {target_agent_id}", style="dim")
|
2026-01-06 14:10:45 -08:00
|
|
|
else:
|
|
|
|
|
text.append("sending message", style="dim")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
if message:
|
2026-01-05 00:07:54 -08:00
|
|
|
text.append("\n ")
|
2026-01-05 09:52:05 -08:00
|
|
|
text.append(message, style="dim")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
2026-01-06 14:10:45 -08:00
|
|
|
css_classes = cls.get_css_classes(status)
|
2026-01-05 00:07:54 -08:00
|
|
|
return Static(text, classes=css_classes)
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@register_tool_renderer
|
|
|
|
|
class AgentFinishRenderer(BaseToolRenderer):
|
|
|
|
|
tool_name: ClassVar[str] = "agent_finish"
|
|
|
|
|
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
|
|
|
args = tool_data.get("args", {})
|
|
|
|
|
|
|
|
|
|
result_summary = args.get("result_summary", "")
|
|
|
|
|
findings = args.get("findings", [])
|
|
|
|
|
success = args.get("success", True)
|
|
|
|
|
|
2026-01-05 00:07:54 -08:00
|
|
|
text = Text()
|
|
|
|
|
|
|
|
|
|
if success:
|
2026-01-19 16:42:38 -08:00
|
|
|
text.append("◆ ", style="#22c55e")
|
|
|
|
|
text.append("Agent completed", style="bold #22c55e")
|
2026-01-05 00:07:54 -08:00
|
|
|
else:
|
2026-01-19 16:42:38 -08:00
|
|
|
text.append("◆ ", style="#ef4444")
|
|
|
|
|
text.append("Agent failed", style="bold #ef4444")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
if result_summary:
|
2026-01-05 00:07:54 -08:00
|
|
|
text.append("\n ")
|
|
|
|
|
text.append(result_summary, style="bold")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
if findings and isinstance(findings, list):
|
2026-01-05 00:07:54 -08:00
|
|
|
for finding in findings:
|
|
|
|
|
text.append("\n • ")
|
|
|
|
|
text.append(str(finding), style="dim")
|
2025-08-08 20:36:44 -07:00
|
|
|
else:
|
2026-01-05 00:07:54 -08:00
|
|
|
text.append("\n ")
|
|
|
|
|
text.append("Completing task...", style="dim")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
css_classes = cls.get_css_classes("completed")
|
2026-01-05 00:07:54 -08:00
|
|
|
return Static(text, classes=css_classes)
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@register_tool_renderer
|
refactor(tools): split wait_for_message into respond_to_user + wait_for_agents
One tool was doing three jobs (wait on the user, wait on other agents, and
- wrongly - wait for a long-running command), so the driver had to guess which
one an agent meant and used parent_id as the proxy: the root waits for a human,
everyone else waits for agents. That proxy is wrong, since the user can message
any agent from the TUI's agent tree.
Tool identity now carries the intent, and the coordinator records it as a
wait_kind that survives snapshot/restore:
respond_to_user -> wait_kind="user", never auto-resumed (root or not)
wait_for_agents -> wait_kind="agents", auto-resumed on a 300s timer
recovery exhaust -> wait_kind="stalled"
respond_to_user fuses the message and the yield into one call, so there is no
way to answer and then forget to stop - the two-step that gpt-4o-mini skipped
2/2 in live testing. Plain text still renders as before.
Auto-resume is also bounded now: an agent that re-parks after every timeout
burned a model turn every 300s for the rest of the scan (and, since parked
children notify their parent, spammed the parent's inbox on the same cycle).
After _MAX_IDLE_AUTO_RESUMES it stays parked until a real message arrives.
2026-08-01 22:13:07 +00:00
|
|
|
class WaitForAgentsRenderer(BaseToolRenderer):
|
|
|
|
|
tool_name: ClassVar[str] = "wait_for_agents"
|
2025-08-08 20:36:44 -07:00
|
|
|
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
|
|
|
args = tool_data.get("args", {})
|
2026-01-06 14:10:45 -08:00
|
|
|
status = tool_data.get("status", "unknown")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
2026-01-06 14:10:45 -08:00
|
|
|
reason = args.get("reason", "")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
2026-01-05 00:07:54 -08:00
|
|
|
text = Text()
|
2026-01-06 14:10:45 -08:00
|
|
|
text.append("○ ", style="#6b7280")
|
|
|
|
|
text.append("waiting", style="dim")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
|
|
|
|
if reason:
|
2026-01-05 00:07:54 -08:00
|
|
|
text.append("\n ")
|
2026-01-05 09:52:05 -08:00
|
|
|
text.append(reason, style="dim")
|
2025-08-08 20:36:44 -07:00
|
|
|
|
2026-01-06 14:10:45 -08:00
|
|
|
css_classes = cls.get_css_classes(status)
|
2026-01-05 00:07:54 -08:00
|
|
|
return Static(text, classes=css_classes)
|
Agents graph sweep: status taxonomy, stop_agent safety, skill validation
- view_agent_graph status summary now derives buckets from the canonical
Status literal via get_args, so adding a new status in core.agents
auto-flows into the summary. The previous hardcoded five-bucket list
silently omitted "failed" — buckets stopped summing to total whenever
an agent failed.
- stop_agent rejects targets that are already in a terminal status
(completed / stopped / crashed / failed) with a model-readable error
pointing at view_agent_graph and send_message_to_agent. request_stop
unconditionally overwrites status, so without this guard calling
stop_agent on a completed agent erased the "completed" history.
- StopAgentRenderer added — was falling back to the generic key/value
renderer; the rest of the agents_graph tools have purpose-built ones.
- agent_finish root-rejection payload trimmed from
{success, agent_completed, error, parent_notified} to {success, error}.
The lifecycle gate only reads success+agent_completed and they were
always False/False on this branch, so the extra fields were dead weight.
- wait_for_message renames its top-level outcome field from "status" to
"wait_outcome" — "status" overloaded with the coordinator's agent
status literal (which also has "stopped" as a value, different
meaning). Redundant "agent_waiting" boolean dropped (true iff
wait_outcome == "waiting"). Consumer at factory._wait_tool_parked
updated to match.
- send_message_to_agent now refuses self-send with a pointer at think /
agent_finish / finish_scan instead of looping a message into your
own session.
- SendMessageToAgentRenderer read args.get("agent_id") but the tool's
param is target_agent_id, so the TUI silently never showed the target.
Fixed.
- Restored skill validation lost during the SDK migration: skills
module re-exports get_all_skill_names and validate_requested_skills
(excluding internal scan_modes/coordination categories from the
user-selectable set). create_agent now validates skills before
spawning instead of silently accepting unknown names.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:35:57 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@register_tool_renderer
|
|
|
|
|
class StopAgentRenderer(BaseToolRenderer):
|
|
|
|
|
tool_name: ClassVar[str] = "stop_agent"
|
|
|
|
|
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
|
|
|
args = tool_data.get("args", {})
|
|
|
|
|
result = tool_data.get("result")
|
|
|
|
|
status = tool_data.get("status", "unknown")
|
|
|
|
|
|
|
|
|
|
target_agent_id = args.get("target_agent_id", "")
|
|
|
|
|
cascade = args.get("cascade", True)
|
|
|
|
|
reason = args.get("reason", "")
|
|
|
|
|
|
|
|
|
|
text = Text()
|
|
|
|
|
text.append("◼ ", style="#ef4444")
|
|
|
|
|
text.append("stopping", style="dim")
|
|
|
|
|
if target_agent_id:
|
|
|
|
|
text.append(f" {target_agent_id}", style="bold #ef4444")
|
|
|
|
|
if cascade:
|
|
|
|
|
text.append(" + descendants", style="dim italic")
|
|
|
|
|
|
|
|
|
|
if reason:
|
|
|
|
|
text.append("\n ")
|
|
|
|
|
text.append(reason, style="dim")
|
|
|
|
|
|
|
|
|
|
if isinstance(result, dict) and result.get("success") is False and result.get("error"):
|
|
|
|
|
text.append("\n ")
|
|
|
|
|
text.append(str(result["error"]), style="#ef4444")
|
|
|
|
|
|
|
|
|
|
css_classes = cls.get_css_classes(status)
|
|
|
|
|
return Static(text, classes=css_classes)
|