How Deep Agents Implements Sub-Agent Delegation: Task Tool, State Isolation & System Prompt Injection

This article dissects Deep Agents' SubAgentMiddleware implementation, revealing how the task tool dynamically delegates complex work to isolated sub-agents with separate context windows, detailing dynamic tool construction, state isolation via excluded keys, system prompt injection for delegation guidance, and a Deep Research case study showing research-agent delegation in action.

Code Archaeology
Code Archaeology
Code Archaeology
How Deep Agents Implements Sub-Agent Delegation: Task Tool, State Isolation & System Prompt Injection

1. SubAgent & CompiledSubAgent

When creating an agent via create_deep_agent, you can pass a subagents parameter. Two types are supported: SubAgent (TypedDict): requires name, description, system_prompt; optional tools, model, middleware, interrupt_on, skills. CompiledSubAgent (TypedDict): requires name, description, runnable (a pre-compiled Runnable).

Both share name (unique identifier for invocation) and description (used by the main agent to decide delegation). create_deep_agent automatically prepends a general_purpose_spec to the all_subagents array.

_SubagentSpec

All subagents are normalized to _SubagentSpec (name, description, runnable) inside SubAgentMiddleware initialization via _get_subagents:

def _get_subagents(self) -> list[_SubagentSpec]:
    specs: list[_SubagentSpec] = []
    for spec in self._subagents:
        if "runnable" in spec:
            # CompiledSubAgent - use as-is
            compiled = cast("CompiledSubAgent", spec)
            specs.append({
                "name": compiled["name"],
                "description": compiled["description"],
                "runnable": compiled["runnable"]
            })
            continue
        # SubAgent
        specs.append(
            {
                "name": spec["name"],
                "description": spec["description"],
                "runnable": create_agent(
                    model,
                    system_prompt=spec["system_prompt"],
                    tools=spec["tools"],
                    middleware=middleware,
                    name=spec["name"],
                ),
            }
        )
    return specs

Key points: CompiledSubAgent maps directly to _SubagentSpec; for SubAgent, a new agent runnable is created using the provided system_prompt.

2. The Task Tool

SubAgentMiddleware

creates a single task tool, built dynamically via _build_task_tool because subagents are dynamic.

2.1 _build_task_tool Steps

Step 1: Build subagent dictionary

subagent_graphs: dict[str, Runnable] = {
    spec["name"]: spec["runnable"] for spec in subagents
}

Key is subagent name for fast lookup during tool invocation.

Step 2: Generate tool description

subagent_description_str = "
".join(
    f"- {s['name']}: {s['description']}" for s in subagents
)
if task_description is None:
    description = TASK_TOOL_DESCRIPTION.format(
        available_agents=subagent_description_str
    )
elif "{available_agents}" in task_description:
    description = task_description.format(
        available_agents=subagent_description_str
    )
else:
    description = task_description

The default description includes:

Purpose: launch ephemeral subagents for complex, multi-step independent tasks with isolated context windows.

Default general-purpose agent with all main-agent tools, suited for research, file search, multi-step tasks.

Requirement: must specify subagent_type parameter.

Usage notes: concurrent invocation allowed; subagent results are invisible to user — main agent must summarize; subagents are stateless and return once, so prompts must be detailed; trust subagent output; clarify task intent (creation, analysis, research); proactive use encouraged; default to general-purpose when only it exists to isolate context and tokens.

Step 3: Define return handler

def _return_command_with_state_update(result: dict, tool_call_id: str) -> Command:
    if "messages" not in result:
        error_msg = (
            "CompiledSubAgent must return a state containing a 'messages' key. "
            "Custom StateGraphs used with CompiledSubAgent should include 'messages' "
            "in their state schema to communicate results back to the main agent."
        )
        raise ValueError(error_msg)
    state_update = {
        k: v for k, v in result.items()
        if k not in _EXCLUDED_STATE_KEYS
    }
    message_text = result["messages"][-1].text.rstrip() if result["messages"][-1].text else ""
    return Command(
        update={
            **state_update,
            "messages": [ToolMessage(message_text, tool_call_id=tool_call_id)],
        }
    )

Validates messages key exists.

Filters excluded state keys.

Extracts last message, strips trailing whitespace.

Returns Command for state update with a ToolMessage.

Step 4: Define state preparation

def _validate_and_prepare_state(
    subagent_type: str,
    description: str,
    runtime: ToolRuntime
) -> tuple[Runnable, dict]:
    subagent = subagent_graphs[subagent_type]
    subagent_state = {
        k: v for k, v in runtime.state.items()
        if k not in _EXCLUDED_STATE_KEYS
    }
    subagent_state["messages"] = [HumanMessage(content=description)]
    return subagent, subagent_state

Creates new state dict to avoid mutating original (state isolation).

Initializes messages with a single HumanMessage containing the task description.

Step 5: Define task function

def task(
    description: Annotated[str, "任务描述"],
    subagent_type: Annotated[str, "Subagent 类型"],
    runtime: ToolRuntime,
) -> str | Command:
    if subagent_type not in subagent_graphs:
        allowed_types = ", ".join([f"`{k}`" for k in subagent_graphs])
        return f"We cannot invoke subagent {subagent_type} because it does not exist, the only allowed types are {allowed_types}"
    if not runtime.tool_call_id:
        raise ValueError("Tool call ID is required for subagent invocation")
    subagent, subagent_state = _validate_and_prepare_state(
        subagent_type, description, runtime
    )
    result = subagent.invoke(subagent_state)
    return _return_command_with_state_update(result, runtime.tool_call_id)

Core logic: validate subagent exists, validate tool_call_id, prepare isolated state, invoke subagent, return formatted result.

2.2 State Isolation

Subagent and main agent states are isolated. The following keys are excluded when passing state to the subagent: messages — Main agent's full conversation history should not leak; subagent receives only the current task description as a clean HumanMessage entry. skills_metadata — Subagents (e.g., general-purpose) load their own skills via SkillsMiddleware; should not inherit main agent's skill metadata. memory_contents — Subagents should load their own memory, not the main agent's memory context. todos / structured_response — Main agent's task list/structured response is irrelevant to the sub-task.

When the subagent returns, the same keys are excluded to prevent child-state pollution: messages — Not passed through directly; only the last message is taken, wrapped as a ToolMessage so the main agent receives a concise tool result, not the subagent's full dialogue history. todos — Subagent's todos lack a reducer (cannot be merged); writing them back would cause LangGraph state update errors. structured_response — No defined semantics for the main agent. skills_metadata / memory_contents — Already excluded via PrivateStateAttr annotation, but explicit filtering acts as a second safeguard.

3. SubAgentMiddleware Hooks

Implements two hooks; sync/async merged into one: wrap_model_call.

3.1 wrap_model_call: System Prompt Injection

Injects two parts into the system prompt:

How to use the task tool (detailed guidance).

List of all available subagents with names and descriptions.

Part 1 (default):

## `task` (subagent spawner)

You have access to a `task` tool to launch short-lived subagents that handle isolated tasks. These agents are ephemeral — they live only for the duration of the task and return a single result.

When to use the task tool:
- When a task is complex and multi-step, and can be fully delegated in isolation
- When a task is independent of other tasks and can run in parallel
- When a task requires focused reasoning or heavy token/context usage that would bloat the orchestrator thread
- When sandboxing improves reliability (e.g. code execution, structured searches, data formatting)
- When you only care about the output of the subagent, and not the intermediate steps (ex. performing a lot of research and then returned a synthesized report, performing a series of computations or lookups to achieve a concise, relevant answer.)

Subagent lifecycle:
1. **Spawn** → Provide clear role, instructions, and expected output
2. **Run** → The subagent completes the task autonomously
3. **Return** → The subagent provides a single structured result
4. **Reconcile** → Incorporate or synthesize the result into the main thread

When NOT to use the task tool:
- If you need to see the intermediate reasoning or steps after the subagent has completed (the task tool hides them)
- If the task is trivial (a few tool calls or simple lookup)
- If delegating does not reduce token usage, complexity, or context switching
- If splitting would add latency without benefit

## Important Task Tool Usage Notes to Remember
- Whenever possible, parallelize the work that you do. This is true for both tool_calls, and for tasks. Whenever you have independent steps to complete - make tool_calls, or kick off tasks (subagents) in parallel to accomplish them faster. This saves time for the user, which is incredibly important.
- Remember to use the `task` tool to silo independent tasks within a multi-part objective.
- You should use the `task` tool whenever you have a complex task that will take multiple steps, and is independent from other tasks that the agent needs to complete. These agents are highly competent and efficient.

Covers: suitable scenarios, lifecycle (Spawn/Run/Return/Reconcile), unsuitable scenarios, core principles (parallelize, silo independent tasks, use for complex independent multi-step work).

Part 2: Available subagents list

Available subagent types:
- general-purpose: General-purpose agent for researching complex questions, searching for files and content, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. This agent has access to all tools as the main agent.

Default includes a general-purpose agent.

4. Deep Research Example: Sub-Agent Delegation in Action

Custom research-agent subagent defined:

# Create research sub-agent
research_sub_agent = {
    "name": "research-agent",
    "description": "Delegate research to the sub-agent researcher. Only give this researcher one topic at a time.",
    "system_prompt": RESEARCHER_INSTRUCTIONS.format(date=current_date),
    "tools": [tavily_search, think_tool],
}

Trace shows delegation to research-agent:

{
  "args": {
    "description": "Research OpenAI's harness engineering comprehensively. Focus on:

1. What is harness engineering? Provide a clear definition and overview.
2. What is the purpose and goals of harness engineering at OpenAI?
3. What are the key concepts, methodologies, and techniques involved?
4. How does harness engineering relate to AI safety and alignment?
5. Are there any specific implementations, case studies, or examples from OpenAI?
6. What is the current status and recent developments in harness engineering?
7. How does it compare to other AI safety approaches?
8. What are the challenges and limitations of harness engineering?

Please search for recent information, technical papers, blog posts, or documentation from OpenAI about harness engineering. Look for primary sources from OpenAI researchers and official publications.

Return your findings with clear citations using [number] format for each source. Include URLs for all sources. Structure your response with clear sections covering the above points.",
    "subagent_type": "research-agent"
  },
  "id": "call_00_OJ6kGJxS6JvwDYG6xOdluJLn",
  "name": "task",
  "type": "tool_call"
}

Subagent input is the description field of the tool call.

Subagent output (last message) returned as ToolMessage:

{
"content": "I have completed comprehensive research on OpenAI's harness engineering. My findings cover all requested aspects:

1. **Definition and overview** - Harness engineering is an emerging methodology for creating structured environments for AI agents, originating from Mitchell Hashimoto's concept.

2. **Purpose and goals at OpenAI** - To enable large-scale AI-driven development, shift engineer roles, increase throughput, ensure maintainability, and scale human judgment.

3. **Key concepts and methodologies** - Architectural constraints as guardrails, tools as foundation and feedback, documentation as system of record, context engineering, and depth-first development.

4. **Relationship to AI safety and alignment** - Through constrained solution spaces, mechanical enforcement, feedback loops, and addressing verification challenges.

5. **Specific implementations** - OpenAI's 5-month project building a million-line product with zero hand-written code, plus detailed implementation patterns.

6. **Current status and developments** - Industry convergence across OpenAI, Stripe, and OpenClaw; emerging standards; recent critiques from Martin Fowler.

7. **Comparison to other approaches** - Different from traditional software engineering, shares principles with constrained optimization and scalable oversight in AI safety.

8. **Challenges and limitations** - Technical challenges (entropy, verification), organizational challenges (cultural adoption), and inherent limitations (solution space constraints).

The research draws from four high-quality sources including technical analyses, industry reporting, and expert critique, providing a comprehensive view of this emerging discipline at the intersection of software engineering and AI safety.",
"response_metadata": {
  "finish_reason": "stop",
  "model_name": "deepseek-chat",
  "system_fingerprint": "fp_eaab8d114b_prod0820_fp8_kvcache",
  "model_provider": "deepseek"
},
"type": "ai",
"id": "lc_run--019cd85b-0fe6-7ed3-a64c-a8a4236f6a65",
"usage_metadata": {
  "input_tokens": 34699,
  "output_tokens": 300,
  "total_tokens": 34999,
  "input_token_details": {
    "cache_read": 34496
  },
  "output_token_details": {}
},
"tool_calls": [],
"invalid_tool_calls": []
}

The subagent's output becomes the task tool's final message, wrapped as a ToolMessage for the main agent.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AI engineeringLLM AgentsState IsolationLangGraphDeep AgentsTask ToolAgent DelegationSubAgentMiddleware
Code Archaeology
Written by

Code Archaeology

Learn from the past to understand the present

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.