Subagent lifecycle
Corresponding source
| Subject | Source |
|---|---|
| State, spawning, cancellation, and result injection | nanobot/agent/subagent.py::SubagentStatus, SubagentManager |
| Subagent hook | nanobot/agent/subagent.py::_SubagentHook |
| System-prompt and completion-notice templates | nanobot/templates/agent/subagent_system.md, subagent_announce.md |
| Subagent Tool registration | nanobot/agent/tools/loader.py, nanobot/agent/tools/spawn.py |
Learning objectives
After this lesson, you should be able to:
- explain which components a Subagent shares and which state it isolates;
- explain how a Subagent result returns to the parent Session;
- describe concurrency limits and cancellation;
- avoid presenting the current implementation as a general multi-agent orchestration platform.
Prerequisites
- Understand AgentRunner.
- Understand MessageBus and the pending queue.
- Understand Tool scope.
1. SubagentManager responsibilities
Location:
nanobot/agent/subagent.py::SubagentManager
It manages:
- background Tasks;
- task IDs and status;
- sets of tasks associated with each Session;
- the Subagent ToolRegistry;
- independent AgentRunner execution;
- result announcements;
- Session-level cancellation.
2. Shared and isolated elements
| Item | Behavior |
|---|---|
| Provider | shares the parent Agent’s current Provider/model by default |
| AgentRunner implementation | reuses the same AgentRunner class |
| ToolRegistry | created separately for the Subagent |
| Tool scope | scope="subagent" |
| workspace | shared by default; workspace scope may constrain it |
| Session history | does not directly load parent Session history |
| system prompt | uses a Subagent-specific template and Skill summary |
| result | returns to the parent Agent as a new InboundMessage |
Subagent input is primarily:
Subagent system prompt
+ task descriptionIt is not a copy of the parent Agent’s complete current context.
3. Creation flow
spawn():
- generates a short task ID;
- creates
SubagentStatus; - stores origin Channel/chat/Session;
- calls
asyncio.create_task(_run_subagent(...)); - records the running Task;
- records the Session-to-task-ID mapping;
- registers a done callback to clean up in-memory state;
- immediately returns “started” text to the parent Agent.
flowchart TD MAIN["Parent Agent calls spawn"] --> ID["Create task_id/status"] ID --> TASK["Background asyncio Task"] TASK --> TOOLS["Independent Subagent ToolRegistry"] TOOLS --> RUN["AgentRunner.run"] RUN --> ANN["Announce result"] ANN --> BUS["MessageBus.publish_inbound"] BUS --> MAIN2["Parent Agent in the same Session"]
4. Subagent ToolRegistry
_build_tools():
- creates a new Registry;
- creates a new ToolContext;
- inherits required exec/web/file configuration;
- applies workspace restrictions;
- calls
ToolLoader.load(..., scope="subagent").
Not every parent-Agent Tool is exposed automatically.
This prevents:
- product-layer capabilities such as MessageTool from being misused by a background task;
- a Subagent from spawning indefinitely;
- Tools unsuitable for background execution from being selected.
Verify concrete scope in ToolLoader and each Tool’s configuration.
5. Runner configuration differences
The Subagent AgentRunSpec uses stricter choices in several places:
fail_on_tool_error=Truefinalize_on_max_iterations=False- custom maximum-iteration message
error_message=None- checkpoints update only
SubagentStatus - LLM timeout policy corresponds to the origin Session
- independent Hooks record stages and iterations
Reusing the same AgentRunner does not mean identical behavior.
6. How the result returns to the parent Agent
_announce_result():
- renders the result with
subagent_announce.md; - creates an
InboundMessage; - sets
channel="system"; - sets
sender_id="subagent"; - marks
injected_event=subagent_resultin metadata; - uses
session_key_overrideto align with the parent Agent’s effective Session; - publishes to MessageBus inbound.
If the parent Session is still running:
- the result enters that Session’s pending queue;
- Runner receives it as a mid-turn injection.
If the parent Session is idle:
- the result is handled as a new system-originated inbound Turn.
7. Why the result is not sent as OutboundMessage
The result returns to the parent Agent before being displayed, allowing the parent to:
- read the result;
- combine it with the original task;
- continue calling Tools;
- explain a partial failure;
- decide the final user response.
Direct outbound delivery would bypass parent-Agent integration and Session semantics.
8. Concurrency limit
Configuration:
agents.defaults.maxConcurrentSubagents- current default: 1
At the limit, the spawn Tool returns an error so the parent Agent can wait or change its approach.
The limit protects:
- local-model KV cache;
- Provider concurrency quotas;
- concurrent workspace side effects;
- user control over background task count.
9. Cancellation and status
SubagentManager maintains:
_running_tasks_task_statuses_session_tasks
It supports:
- observation by task ID;
- cancellation of every Subagent for one Session;
- done-callback cleanup.
This state is mainly in process memory, not a persistent task queue. A process restart does not automatically resume background Subagents.
10. Current capability boundary
The current implementation is most accurately described as:
A parent Agent can start a limited number of background, tool-capable AgentRunner tasks and inject their results back into the original Session.
It is not:
- a persistent multi-agent workflow engine;
- a DAG scheduler;
- a shared blackboard for multiple agents;
- an automatic consensus protocol;
- a cross-process task system.
Observation task
- Start one read-only Subagent.
- Record the returned task ID.
- Inspect its Tool set in
_build_tools(). - Compare parent and Subagent system prompts.
- Inspect the result
InboundMessage. - Compare result delivery while the parent Turn is active and idle.
- Keep concurrency at 1 while learning the lifecycle; do not stress-test it.
Source and tests
nanobot/agent/subagent.pynanobot/templates/agent/subagent_system.mdnanobot/templates/agent/subagent_announce.mdnanobot/agent/tools/loader.pytests/agent/test_subagent.pytests/agent/test_subagent_lifecycle.pytests/agent/tools/test_subagent_tools.py
Self-check
- Why does a Subagent not load parent Session history directly?
- Why does it create a separate ToolRegistry?
- Why does its result become InboundMessage instead of OutboundMessage?
- Why is the default concurrency limit only 1?
- Which facts show that this is not a persistent multi-agent orchestration platform?
Next step
Continue to Runtime Surfaces to learn how Channel, Gateway, API, SDK, and WebUI surround the same core runtime.