diff --git a/manuals/spawn_agent.md b/manuals/spawn_agent.md index 57f7b77..a16cb4d 100644 --- a/manuals/spawn_agent.md +++ b/manuals/spawn_agent.md @@ -5,21 +5,33 @@ **One plan step = one spawn_agent call.** If your plan has three AGENT steps, make three separate calls. -**CRITICAL: spawn_agent is SYNCHRONOUS.** It blocks until the sub-agent fully completes (or times out after 5 minutes). +**SYNCHRONOUS** — blocks until the sub-agent fully completes or times out (5 minutes hard limit). ## Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `task` | yes | Goal for this one step, success criteria, expected output format. End with: "Complete ALL assigned work before responding. Your output is final." | -| `briefing` | no | Credentials, IPs, file paths, constraints, step-by-step instructions — injected as system-level context into the sub-agent. | +| `briefing` | no | Credentials, IPs, file paths, constraints, step-by-step instructions — injected into the sub-agent's system prompt as `## Task context`. | | `profile_id` | no | Which profile to use (`secretary`, `server_admin`, `developer`). Defaults to current session's profile. | -| `system_prompt` | no | Role specialisation for this task (e.g. "You are a security auditor. Report findings by severity."). Injected on top of the profile's built-in subagent prompt. | -| `max_iterations` | no | Tool-call iteration limit (default: 20). | +| `system_prompt` | no | Role specialisation injected into the sub-agent's system prompt between the executor persona and the briefing (e.g. "You are a security auditor. Report findings by severity."). | +| `max_iterations` | no | Tool-call iteration limit (default: **40**). | + +## Sub-agent system prompt structure + +The sub-agent receives a completely separate system prompt — no persona, no orchestrator instructions, no available-profiles block. It is built from up to three parts (separated by `---`): + +``` +1. profile.subagent_system_prompt ← focused executor persona (from subagent_system_prompt.txt) +2. system_prompt param ← role specialisation (if provided) +3. ## Task context\n\n{briefing} ← credentials, instructions (if briefing provided) +``` + +Fallback: if the profile has no `subagent_system_prompt.txt`, uses `profile.system_prompt`. ## Sub-agent tools -Sub-agents receive a focused tool set (no todo, switch_profile, email_manager, etc.): +Sub-agents receive a dedicated, focused tool set (defined in `subagent_tools` in profile config): | Profile | Sub-agent tools | |---------|----------------| @@ -27,44 +39,44 @@ | `server_admin` | scratchpad, reflect, web_search, http_request, filesystem, code_exec, terminal, ssh_exec, image_view, share_file | | `developer` | scratchpad, reflect, web_search, web_view, http_request, filesystem, code_exec, terminal, image_view, reload_tools, test_tool, share_file | +`spawn_agent` is always excluded — recursion is impossible. + ## Result format -The result header tells you how the sub-agent finished: +The result always starts with a header visible only to you (never repeat it to the user): - `[Sub-agent completed ...]` — finished normally; synthesise the findings. -- `[Sub-agent hit iteration limit ...]` — may be incomplete; note what's missing in your response. +- `[Sub-agent hit iteration limit ...]` — may be incomplete; note what's missing. -**Never repeat the result header to the user.** Synthesise findings in your own words. - -## Example: correct multi-agent execution +## Multi-agent execution pattern ``` -Plan step 2 → AGENT → spawn_agent(task="Research pricing for product X ...", briefing="...") -Plan step 3 → AGENT → spawn_agent(task="Research pricing for product Y ...", briefing="...") +Plan step 2 → AGENT → spawn_agent(task="Research pricing for X ...", briefing="...") +Plan step 3 → AGENT → spawn_agent(task="Research pricing for Y ...", briefing="...") Plan step 4 → SELF → synthesise both results, write final answer ``` -**Wrong:** `spawn_agent(task="Research X and Y pricing and write a comparison")` — two steps, one call. +**Wrong:** `spawn_agent(task="Research X and Y and compare")` — two steps, one call. ## Full example ```json { "task": "Check CPU temperature and memory usage. Return a table: metric, value, unit, status (ok/warn/crit). Complete ALL assigned work before responding. Your output is final.", - "briefing": "Host: 192.168.1.75\nUser: gmikcon\nPassword: getroot\nUse ssh_exec. Check temperature via 'sensors' or /sys/class/thermal. Check memory via 'free -h'.", + "briefing": "Host: 192.168.1.75\nUser: gmikcon\nPassword: getroot\nUse ssh_exec. Check temperature via 'sensors' or /sys/class/thermal/thermal_zone*/temp (divide by 1000). Check memory via 'free -h'.", "profile_id": "server_admin", - "system_prompt": "You are a system metrics collector. Report all values in a structured table." + "system_prompt": "You are a system metrics collector. Report all values in a structured table with columns: metric, value, unit, status." } ``` ## After the result arrives -The user cannot see sub-agent output — you must present the findings yourself. +The user cannot see sub-agent output — present findings yourself. -1. Read the result header to know if it completed or hit the limit. -2. Extract key findings and present them clearly to the user. -3. If incomplete, decide: retry with a more focused task, or handle inline. +1. If "hit iteration limit" — note what is missing in your response. +2. Synthesise key findings in your own words. +3. If result is insufficient, spawn again with a more focused task. ## What the sub-agent cannot do -- Spawn further sub-agents (recursion is blocked) -- Access your conversation history -- Use administrative tools: todo, switch_profile, list_profiles, email_manager, delete_tool +- Spawn further sub-agents (recursion blocked) +- Access conversation history (only context from the current call) +- Use: todo, switch_profile, list_profiles, email_manager, delete_tool, list_tools, tool_manual diff --git a/navi/api/websocket.py b/navi/api/websocket.py index aafc556..872bbfc 100644 --- a/navi/api/websocket.py +++ b/navi/api/websocket.py @@ -110,9 +110,9 @@ if isinstance(event, StreamStopped): return {"type": "stream_stopped"} if isinstance(event, PlanningStatus): - return {"type": "planning_status", "phase": event.phase, "label": event.label} + return {"type": "planning_status", "phase": event.phase, "label": event.label, "is_subagent": event.is_subagent} if isinstance(event, PlanReady): - return {"type": "plan_ready", "plan": event.plan} + return {"type": "plan_ready", "plan": event.plan, "is_subagent": event.is_subagent} return None diff --git a/navi/core/agent.py b/navi/core/agent.py index af5915e..79512ce 100644 --- a/navi/core/agent.py +++ b/navi/core/agent.py @@ -335,6 +335,7 @@ async for _ev in self._run_planning( context, profile, llm, mem, tool_schemas, system_prompt_override=subagent_sys_msg.content, + is_subagent=True, ): if isinstance(_ev, AIHelperTokensUsed): pass # token accounting only, not forwarded @@ -750,6 +751,7 @@ tool_schemas: list | None = None, messages: "list[Message] | None" = None, system_prompt_override: str | None = None, + is_subagent: bool = False, ): """ Three-phase planning (async generator): @@ -792,7 +794,7 @@ _stop = current_stop_event.get() # ── Phase 1: Task analysis (with reasoning) ──────────────────────────── - yield PlanningStatus(phase=1, label="Analysing task...") + yield PlanningStatus(phase=1, label="Analysing task...", is_subagent=is_subagent) _base_sys = system_prompt_override if system_prompt_override is not None else self._build_system_prompt(profile) phase1_system = Message( role="system", @@ -847,7 +849,7 @@ return # ── Phase 2: Execution plan ──────────────────────────────────────────── - yield PlanningStatus(phase=2, label="Building execution plan...") + yield PlanningStatus(phase=2, label="Building execution plan...", is_subagent=is_subagent) phase2_system = Message( role="system", content=( @@ -923,7 +925,7 @@ return # ── Phase 3: AIHelper plan critic ────────────────────────────────────── - yield PlanningStatus(phase=3, label="Reviewing plan...") + yield PlanningStatus(phase=3, label="Reviewing plan...", is_subagent=is_subagent) # Independent pass: validates and corrects executor assignments against # the actual tool list. Falls back to Phase 2 plan on any failure. if tool_names_list: @@ -990,7 +992,7 @@ if messages is not None: messages.append(Message(role="assistant", content=plan_text, is_plan=True)) log.debug("agent.plan_ready", phases=3, length=len(plan_text)) - yield PlanReady(plan=plan_text) + yield PlanReady(plan=plan_text, is_subagent=is_subagent) async def _run_workers( self, diff --git a/navi/core/events.py b/navi/core/events.py index 48c38f4..7636058 100644 --- a/navi/core/events.py +++ b/navi/core/events.py @@ -82,10 +82,12 @@ phase: 1 = Analysis, 2 = Execution plan, 3 = Plan review (AIHelper critic). label: short human-readable description shown next to the spinner. + is_subagent: True when emitted from inside run_ephemeral (subagent planning). """ phase: int label: str + is_subagent: bool = False @dataclass @@ -94,9 +96,11 @@ The plan text has already been injected into session.context as an assistant message so the LLM will see it and follow it during execution. + is_subagent: True when emitted from inside run_ephemeral (subagent planning). """ plan: str + is_subagent: bool = False @dataclass diff --git a/navi/tools/spawn_agent.py b/navi/tools/spawn_agent.py index 0c46d00..614d0d0 100644 --- a/navi/tools/spawn_agent.py +++ b/navi/tools/spawn_agent.py @@ -63,7 +63,7 @@ }, "max_iterations": { "type": "integer", - "description": "Maximum tool-call iterations for the sub-agent (default: 20).", + "description": "Maximum tool-call iterations for the sub-agent (default: 40).", }, }, "required": ["task"], @@ -91,7 +91,7 @@ task = params["task"].strip() briefing = (params.get("briefing") or "").strip() or None custom_system_prompt = (params.get("system_prompt") or "").strip() or None - max_iterations = int(params.get("max_iterations") or 20) + max_iterations = int(params.get("max_iterations") or 40) # task → user message (what to do) # briefing → system prompt (context, credentials, instructions) diff --git a/webclient/src/components/messages/ToolCard.vue b/webclient/src/components/messages/ToolCard.vue index ccb1707..94dbf21 100644 --- a/webclient/src/components/messages/ToolCard.vue +++ b/webclient/src/components/messages/ToolCard.vue @@ -27,10 +27,23 @@
{{ formatResult(tool.result) }}
+
+