diff --git a/manuals/spawn_agent.md b/manuals/spawn_agent.md index 3a31b9e..ec4d096 100644 --- a/manuals/spawn_agent.md +++ b/manuals/spawn_agent.md @@ -10,23 +10,24 @@ | Parameter | Required | Description | |-----------|----------|-------------| -| `task` | yes | Self-contained task description including all context, credentials, expected output format. End with: "Complete ALL assigned work before responding. Your output is final." | +| `task` | yes | What to accomplish: exact goal, success criteria, expected output format. End with: "Complete ALL assigned work before responding. Your output is final." | +| `briefing` | no | All context the sub-agent needs (IPs, credentials, file paths, prior findings, step-by-step instructions). Sub-agent starts blank — include everything. | | `profile_id` | no | Which profile to use (`secretary`, `server_admin`, `developer`). Defaults to current session's profile. | -| `system_prompt` | no | Custom system prompt injected into the sub-agent on top of the profile's built-in subagent prompt. Use to set a specific role, output format, or constraints for this task. | +| `system_prompt` | no | Role definition injected on top of the profile's built-in subagent prompt. Use to specialise the agent for this task (e.g. "You are a security auditor. Report findings by severity."). | | `max_iterations` | no | Tool-call iteration limit (default: 20). | ## Context transfer — automatic -Before spawning, write key context to your scratchpad section `context_transfer`: +Before spawning, write key working state to your scratchpad `context_transfer` section: ``` -scratchpad(op="write", section="context_transfer", content="Host: 192.168.1.75\nUser: gmikcon\n...") +scratchpad(op="write", section="context_transfer", content="...") ``` This section is **automatically injected** into the sub-agent at the start of its context. -No need to repeat it in `task`. +Use it to pass findings from earlier tool calls or parallel agents without duplicating them in `briefing`. ## Sub-agent tools -Sub-agents receive a focused tool set (not all profile tools): +Sub-agents receive a focused tool set (not all profile tools — no todo, switch_profile, email_manager, etc.): | Profile | Sub-agent tools | |---------|----------------| @@ -36,39 +37,30 @@ ## Result format -The result always starts with a status line: -``` -[STATUS: completed] ← sub-agent finished normally -[STATUS: limit_reached] ← hit max_iterations without a final response -``` +The result header tells you how the sub-agent finished: +- `[Sub-agent completed ...]` — finished normally; synthesise the findings. +- `[Sub-agent hit iteration limit ...]` — may be incomplete; note what's missing in your response. -Read the status before deciding what to do next. If `limit_reached`, the partial result may still be useful, or you may need to spawn again with a more focused task. +**Never repeat the result header to the user.** Synthesise findings in your own words. -## Writing a good task - -Bad: -> "Check the server security." - -Good: -> "Audit SSH configuration on 192.168.1.75 (user: gmikcon, password: getroot). -> Check: PasswordAuthentication, PermitRootLogin, AllowUsers, Port. -> Return a list of findings with severity (critical/warning/info) and suggested fix for each. -> Complete ALL assigned work before responding. Your output is final." - -Include: -- Exactly what to do (not vague goals) -- What to return and in what format -- What "done" looks like - -## Using system_prompt - -Use `system_prompt` to give the sub-agent a specific role or output contract: +## Writing a good task + briefing ```json { - "task": "Check CPU temperature and memory usage on the host.", + "task": "Check CPU temperature and memory usage. Return a structured 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 to connect. Check temperature via 'sensors' or /sys/class/thermal. Check memory via 'free -h'.", + "profile_id": "server_admin" +} +``` + +## Using system_prompt for role specialisation + +```json +{ + "task": "Audit the SSH configuration for security issues.", + "briefing": "Host: 192.168.1.75, user: gmikcon, password: getroot.", "profile_id": "server_admin", - "system_prompt": "You are a system metrics collector. Report all values in a structured table. Include: metric name, current value, unit, status (ok/warn/crit). No prose." + "system_prompt": "You are a security auditor. For each finding report: setting name, current value, risk level (critical/warning/info), recommended fix." } ``` @@ -76,11 +68,11 @@ The user cannot see sub-agent output — you must present the findings yourself. -1. Check `[STATUS: ...]` first. +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 `limit_reached`, decide: retry with a smaller task, or handle inline. +3. If incomplete, decide: retry with a more focused task, or handle inline. ## What the sub-agent cannot do - Spawn further sub-agents (recursion is blocked) -- Access your conversation history (only context_transfer scratchpad section) -- Use administrative tools: todo, switch_profile, list_profiles, reload_tools (except developer), delete_tool, email_manager +- Access your conversation history (only context_transfer scratchpad section is passed) +- Use administrative tools: todo, switch_profile, list_profiles, email_manager, delete_tool diff --git a/navi/profiles/developer/config.json b/navi/profiles/developer/config.json index 0a1c6be..6dce26c 100644 --- a/navi/profiles/developer/config.json +++ b/navi/profiles/developer/config.json @@ -13,7 +13,7 @@ "temperature": 0.2, "max_iterations": 35, "planning_enabled": true, - "subagent_planning_enabled": false, + "subagent_planning_enabled": true, "subagent_tools": [ "scratchpad", "reflect", "web_search", "web_view", "http_request", diff --git a/navi/profiles/secretary/config.json b/navi/profiles/secretary/config.json index 9363c4f..f389901 100644 --- a/navi/profiles/secretary/config.json +++ b/navi/profiles/secretary/config.json @@ -13,7 +13,7 @@ "temperature": 0.5, "max_iterations": 25, "planning_enabled": true, - "subagent_planning_enabled": false, + "subagent_planning_enabled": true, "subagent_tools": [ "scratchpad", "reflect", "web_search", "web_view", "http_request", diff --git a/navi/profiles/server_admin/config.json b/navi/profiles/server_admin/config.json index 9ce7728..562a717 100644 --- a/navi/profiles/server_admin/config.json +++ b/navi/profiles/server_admin/config.json @@ -13,7 +13,7 @@ "temperature": 0.2, "max_iterations": 20, "planning_enabled": true, - "subagent_planning_enabled": false, + "subagent_planning_enabled": true, "subagent_tools": [ "scratchpad", "reflect", "web_search", "http_request", diff --git a/navi/tools/spawn_agent.py b/navi/tools/spawn_agent.py index f8ada8d..b58b94e 100644 --- a/navi/tools/spawn_agent.py +++ b/navi/tools/spawn_agent.py @@ -24,10 +24,6 @@ "USE when a task requires 2+ tool calls forming one logical unit: " "research a topic, audit a module, configure a server, process a file set.\n" "DO NOT USE for a single tool call — call the tool directly.\n\n" - "The task field must be fully self-contained: the sub-agent knows nothing about " - "your conversation. Include IPs, credentials, file paths, prior results, " - "expected output format. End with: " - "'Complete ALL assigned work before responding. Your output is final.'\n\n" "Context transfer: before spawning, write key context to your scratchpad " "section 'context_transfer' — it is automatically injected into the sub-agent." ) @@ -37,12 +33,19 @@ "task": { "type": "string", "description": ( - "Self-contained description of what the sub-agent must accomplish. " - "Include: exact goal, success criteria, expected output format, " - "all credentials / file paths / prior findings needed. " + "What the sub-agent must accomplish: exact goal, success criteria, " + "expected output format. " "End with: 'Complete ALL assigned work before responding. Your output is final.'" ), }, + "briefing": { + "type": "string", + "description": ( + "All context the sub-agent needs — it has zero knowledge of your conversation. " + "Include: IPs, credentials, file paths, prior findings, constraints, " + "step-by-step instructions if helpful." + ), + }, "profile_id": { "type": "string", "description": ( @@ -54,10 +57,10 @@ "system_prompt": { "type": "string", "description": ( - "Optional custom system prompt injected into the sub-agent in addition " - "to the profile's default. Use to set a specific role, constraints, or " - "output format requirements for this particular task. " - "If omitted, the profile's built-in subagent prompt is used." + "Optional role definition for this sub-agent, injected as a system-level " + "instruction on top of the profile default. Use to specialise the agent: " + "e.g. 'You are a security auditor. Report findings by severity.' " + "or 'You are a metrics collector. Return all values in a structured table.'" ), }, "max_iterations": { @@ -88,9 +91,20 @@ from navi.tools.scratchpad import get_section task = params["task"].strip() + briefing = (params.get("briefing") or "").strip() custom_system_prompt = (params.get("system_prompt") or "").strip() or None max_iterations = int(params.get("max_iterations") or 20) + # Compose user message: briefing provides context, task states the goal. + if briefing: + user_message = ( + f"## Context\n\n{briefing}\n\n" + f"---\n\n" + f"## Task\n\n{task}" + ) + else: + user_message = task + # Resolve profile: explicit override → parent session's profile → first available profile_id = params.get("profile_id", "").strip() if not profile_id: @@ -101,7 +115,8 @@ context_transfer = get_section(parent_sid, "context_transfer") if parent_sid else "" log.info("spawn_agent.start", profile_id=profile_id, max_iterations=max_iterations, - task_preview=task[:80], has_context_transfer=bool(context_transfer), + task_preview=task[:80], has_briefing=bool(briefing), + has_context_transfer=bool(context_transfer), has_custom_prompt=bool(custom_system_prompt)) agent = Agent( @@ -115,7 +130,7 @@ try: result_text, completed = await agent.run_ephemeral( - user_message=task, + user_message=user_message, profile_id=profile_id, max_iterations=max_iterations, exclude_tools=["spawn_agent"], # prevent recursion @@ -123,17 +138,28 @@ context_transfer=context_transfer or None, timeout_seconds=300.0, ) - status = "completed" if completed else "limit_reached" - log.info("spawn_agent.done", profile_id=profile_id, status=status, + log.info("spawn_agent.done", profile_id=profile_id, completed=completed, result_len=len(result_text)) - output = ( - f"[STATUS: {status}]\n" - "[Sub-agent result — the USER CANNOT SEE THIS. " - "You must present the key findings in your own final response.]\n\n" - + result_text + + if completed: + output = ( + "[Sub-agent completed — USER CANNOT SEE THIS. " + "Synthesise the findings into your own response.]\n\n" + + result_text + ) + else: + output = ( + "[Sub-agent hit iteration limit — result may be incomplete. " + "USER CANNOT SEE THIS. " + "Synthesise what was found and note what is missing.]\n\n" + + result_text + ) + + return ToolResult( + success=completed, + output=output, + metadata={"completed": completed, "result_len": len(result_text)}, ) - return ToolResult(success=completed, output=output, - metadata={"status": status, "result_len": len(result_text)}) except Exception as e: log.error("spawn_agent.error", error=str(e), exc_info=True) return ToolResult(success=False, output=f"Sub-agent failed: {e}", error=str(e))