Subagent lifecycle

Corresponding source

SubjectSource
State, spawning, cancellation, and result injectionnanobot/agent/subagent.py::SubagentStatus, SubagentManager
Subagent hooknanobot/agent/subagent.py::_SubagentHook
System-prompt and completion-notice templatesnanobot/templates/agent/subagent_system.md, subagent_announce.md
Subagent Tool registrationnanobot/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

ItemBehavior
Providershares the parent Agent’s current Provider/model by default
AgentRunner implementationreuses the same AgentRunner class
ToolRegistrycreated separately for the Subagent
Tool scopescope="subagent"
workspaceshared by default; workspace scope may constrain it
Session historydoes not directly load parent Session history
system promptuses a Subagent-specific template and Skill summary
resultreturns to the parent Agent as a new InboundMessage

Subagent input is primarily:

Subagent system prompt
  + task description

It is not a copy of the parent Agent’s complete current context.

3. Creation flow

spawn():

  1. generates a short task ID;
  2. creates SubagentStatus;
  3. stores origin Channel/chat/Session;
  4. calls asyncio.create_task(_run_subagent(...));
  5. records the running Task;
  6. records the Session-to-task-ID mapping;
  7. registers a done callback to clean up in-memory state;
  8. 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=True
  • finalize_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():

  1. renders the result with subagent_announce.md;
  2. creates an InboundMessage;
  3. sets channel="system";
  4. sets sender_id="subagent";
  5. marks injected_event=subagent_result in metadata;
  6. uses session_key_override to align with the parent Agent’s effective Session;
  7. 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

  1. Start one read-only Subagent.
  2. Record the returned task ID.
  3. Inspect its Tool set in _build_tools().
  4. Compare parent and Subagent system prompts.
  5. Inspect the result InboundMessage.
  6. Compare result delivery while the parent Turn is active and idle.
  7. Keep concurrency at 1 while learning the lifecycle; do not stress-test it.

Source and tests

  • nanobot/agent/subagent.py
  • nanobot/templates/agent/subagent_system.md
  • nanobot/templates/agent/subagent_announce.md
  • nanobot/agent/tools/loader.py
  • tests/agent/test_subagent.py
  • tests/agent/test_subagent_lifecycle.py
  • tests/agent/tools/test_subagent_tools.py

Self-check

  1. Why does a Subagent not load parent Session history directly?
  2. Why does it create a separate ToolRegistry?
  3. Why does its result become InboundMessage instead of OutboundMessage?
  4. Why is the default concurrency limit only 1?
  5. 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.