Skip to main content
A sub-agent is a separate agent - its own instructions, model, tools and skills - that a lead agent hands a self-contained task to. The sub-agent starts with a clean context (it sees only the task prompt, never the lead’s conversation), works through as many steps as it needs, and its final answer comes back to the lead as one tool result.

Sub-agents, skills or flows?

Quick start

Give each sub-agent a description (the lead model reads it to choose), then pass them to the lead as subagents:
AgentExecutor.execute() takes the same subagents and maxSubagentDepth options. Use it when you need hooks or tracing on the lead run - sub-agents inherit those (see Inheritance). Approvals work from createAgent() too (see Approvals).

How it works

With subagents, the lead agent gets:
  1. An Available sub-agents block in its system prompt: one line per sub-agent (- name: description), plus a sentence saying that a sub-agent sees only the prompt it is given.
  2. ONE tool, task, with the input { agent: <one of the names>, prompt: string, description: string, background?: boolean, taskId?: string, mode?: 'new' | 'resume' | 'fork' } (description is a 3-5 word label used in events and hooks; taskId and mode continue an earlier task, see Continuing a task).
  3. Three tools for background tasks: agent_status, agent_await and agent_cancel (see Background sub-agents).
When the lead calls task, the named sub-agent runs on prompt alone. The tool result is the sub-agent’s final text plus a small footer the lead can use:
If the sub-agent does not finish - it throws, runs out of maxSteps, or is aborted - the lead gets an error result (isError: true) that says why, for example Sub-agent 'researcher' used all 10 of its steps (maxSteps) without giving a final answer. An unknown agent name is also an error result listing the valid names. Sub-agent token usage is added to the lead’s result.usage. Errors at setup time are thrown with a fix: a sub-agent without a description, a value that is not a createAgent() agent, or a tool of your own already named task (or agent_status, agent_await, agent_cancel).

Continuing a task

Each task call runs in a child conversation with a taskId (task_1, task_2, … in the footer). The lead can come back to it: The model learns this from the task tool description, so asking a follow-up needs no code. A lead that keeps its work across turns or restarts needs a store and a session:
Where the conversations live:
  • With createAgent({ store }) (store.sessions) and a lead run that has a sessionId (a checkpointed send(message, { sessionId }) or a session turn), each task is saved in store.sessions after its run, under a key hashed from the lead session id and the taskId. They outlive the lead run: later turns of the same session, a lead resumed with agent.resume() or approvals.resolve(), and a new process on the same durable store can continue them. A taskId from another lead session never matches. The session store has no list API, so these entries are not listed or deleted with the lead session; delete the store’s data to drop them. For AgentExecutor.execute(), set the store with withSubagentOptions(subagents, { sessions }).
  • Otherwise (no session store, or a lead run without a sessionId) a task lives in memory for that one execute() / send() call: a run that pauses for approval and is resumed starts with none.
Only tasks that finished (stop, length or max-steps) are saved, so a task that failed or was cancelled cannot be continued. Errors reach the lead as structured tool errors: an unknown taskId, one of another lead session, or one that belongs to another sub-agent fails with LOUSHO_SUBAGENT_TASK_NOT_FOUND; a task that is still running (a background task that has not ended) fails with LOUSHO_SUBAGENT_TASK_BUSY, telling the model to agent_await or agent_cancel it first. A resumed or forked task is a normal task call otherwise: it can run in the background (it keeps its taskId), an approval inside it pauses and resumes the lead like any sub-agent approval, and its usage counts toward the lead’s usage and limits. The child sees its whole transcript again on every resume, so a long-lived task costs more tokens each time.

Parallel tasks

Tool calls of one model turn run concurrently (see toolConcurrency), so when the lead calls task several times in one turn, those sub-agents run in parallel. Results reach the lead’s transcript in the order the model made the calls. Cap it with toolConcurrency on the lead (1 runs them one at a time).

Background sub-agents

With background: true, task starts the sub-agent and returns at once with { taskId, status: 'running', agent }, so the lead can keep working (or start more tasks) while it runs. The lead then uses: At most maxConcurrent background sub-agents of one lead run run at once (default 3); further background: true calls get status: 'queued' and start as slots free. Set it with subagentOptions on createAgent():
For AgentExecutor.execute(), attach the same options to the subagents value with withSubagentOptions(subagents, options), which returns it. On createAgent(), subagentOptions override options attached that way. A background sub-agent inherits from the lead run like a synchronous one (hooks, tracing, event listeners, usage roll-up), and maxSubagentDepth applies the same way. Aborting the lead’s signal cancels its background sub-agents, queued or running.

When the lead run ends

Background tasks belong to one run (one execute() / send() / stream()). When that run ends, however it ends, the tasks still queued or running are cancelled by default (their runs are aborted). With awaitBackgroundOnFinish: true the run instead waits for them to finish before it resolves (an aborted or failed lead run still cancels them). Either way the run resolves only once their runs have stopped, so no sub-agent event reaches the run’s listeners after it; with awaitBackgroundOnFinish their events arrive before the lead’s run.done. The result lists every background task of the run with its final status:
result.backgroundTasks holds { taskId, agent, status, elapsedMs, error?, approvalId?, toolName? } per task (the agent_status shape) and is absent when the run started none. Use agent_await in the lead (the system prompt tells it to) to get their answers into the conversation. The executor hook behind this is public: ExecuteOptions.onRunEnd is called exactly once per run, with { result } (whatever the finishReason) or { error }; the run settles after it returns. Background tasks are reported on result before your onRunEnd sees it. Limits:
  • A lead run that pauses for approval (finishReason: 'awaiting-approval') ends there: its background tasks are cancelled (or awaited) like at any other end, and the resumed run starts with none. The resumed run is a separate run with no link to the paused one’s tasks (it may even run in another process), so they cannot be kept alive across the pause.
  • A background sub-agent that needs approval stops with status: 'awaiting-approval' (with the child’s approvalId and toolName); its call does not run, and it cannot be resumed through the approval store. Run that task synchronously (background: false) when it may need approval.

Dynamic catalogs

Instead of a fixed record, pass a catalog with list() and resolve(name). list() is called at the start of every run that offers the task tool, so the set can change between runs; resolve() is called when the lead picks a name (return undefined for an unknown one).

Remote sub-agents

remoteAgent() uses an agent you already deployed (lousho deploy: the node server, Docker or a Cloudflare Worker) as a sub-agent. It goes in subagents next to local ones, and the lead delegates to it with the same task tool, including background: true.
Each task opens a new session on the remote agent (POST <url>/chat with { sessionId, input }, read as the SSE event stream, see Deployment), sends the task prompt and returns the remote agent’s final text (its object as JSON when the deployed agent has an output schema, see Structured output), followed by a footer naming the session and the taskId. A task call that resumes that taskId posts to the same remote session, so the remote agent continues with its own history (keep it in a store on the remote side); a remote task cannot be forked. The remote agent sees only that prompt, and runs with its own model, tools and limits, so the lead’s runtime (hooks, sandbox, approval store) does not reach it. The lead run’s abort signal aborts the request. Options: url, auth, name, description, headers, and fetch (inject one in tests; the serveFetch() routes of an in-process agent work as a fake deployment). Failures (the agent is unreachable, a 401 or other non-2xx answer, a malformed stream, or a remote run that ends in an error) reach the lead as the structured tool error with the code LOUSHO_REMOTE_REQUEST_FAILED (LOUSHO_REMOTE_UNAUTHORIZED for a 401); the token is never part of an error or an event.

Remote approvals

If the remote run pauses for an approval, the lead run pauses on it exactly as for a local sub-agent: result.finishReason is 'awaiting-approval', and the pending approval in agent.approvals.list() (and in channel buttons, the dev chat and ACP permission requests) has the remote tool’s name and input, with subagentPath: ['researcher']. A remote ask_question call arrives as a question (kind: 'question') that agent.approvals.answer() answers. Deciding it on the lead (resolve, streamResolve, answer) posts the decision to the remote agent’s approvals route (POST <url>/chat/<session>/approvals/<id>), reads the continuation, and its final answer becomes the task result; a continuation that pauses again pauses the lead again.
The lead’s approval snapshot keeps only the remote session id, the remote approval id, the taskId and the sub-agent’s name, so a fresh lead process on the same store can decide it; the bearer token is never stored (it is read from the remoteAgent() options again). A failure while deciding (a 401, any other non-2xx answer such as the remote’s 404 for an approval no longer pending, a network error) is the structured tool error of that task call with the codes above, and the lead run continues. Only a run without an approval store (a bare AgentExecutor) still fails the task with LOUSHO_SESSION_AWAITING_APPROVAL, naming the remote session and approval id to decide on the remote agent.

Depth

maxSubagentDepth (default 1) bounds how deep sub-agents nest. With the default, the lead can call sub-agents but they cannot call sub-agents of their own: a run at the limit is simply not offered the task tool, even if it was created with subagents. maxSubagentDepth: 2 lets the lead’s sub-agents delegate once more. The top-level run’s value applies to the whole tree; a sub-agent’s own maxSubagentDepth only matters when it runs as a top-level agent.

What a sub-agent inherits

The sub-agent keeps its own instructions, model/provider, tools, skills and maxSteps, and its context is isolated. From the run that called it, it inherits: createDelegateTool() children inherit the same way.

Hooks inside sub-agents

A parent’s hooks apply to its sub-agents by default. ctx.subagent tells a hook it is running inside one - the sub-agent’s name, its depth (1 for a sub-agent of the top-level run), the lead’s toolCallId that started it, and the enclosing sub-agent as parent when nested deeper. A hook that should only see the top-level run returns early:

Events

agent.stream() / AgentExecutor.stream() on the lead stream each sub-agent’s run inside the same stream - steps, text deltas, tool events and errors - with a subagent field on every one of its events, so a UI can nest sub-agent activity under the lead’s task call (subagent.toolCallId). See Streaming: sub-agents. A listener on the lead (createAgent({ onEvent }) or onAgentEvent) gets the same events, sub-agents’ included, as they happen. See Listening without iterating.

Approvals inside a sub-agent

When a sub-agent calls a tool that needsApproval, the whole run pauses: the lead’s execute() resolves with finishReason: 'awaiting-approval' and an approvalId, and the approval store holds ONE record whose pending call is the sub-agent’s call (toolName, args) with subagentPath naming the sub-agents it runs inside (e.g. ['researcher']). Approve or reject it with resumeAfterApproval(), passing the same subagents option as the paused run:
Resuming runs (or rejects) the sub-agent’s pending call, lets the sub-agent finish, returns its final answer to the lead as the task result, and then continues the lead. If the sub-agent needs another approval, the run pauses again with a new approvalId. This works at any depth and for createDelegateTool() children (pass the registry that holds the delegate tool). With a createAgent() lead, lead.send() pauses the same way and lead.approvals.resolve({ id, approved }) resumes it (the lead already knows its subagents); the lead’s approve option decides its sub-agents’ calls too. Guarantees and limits:
  • No call runs twice and none is lost: the sub-agent’s paused state is stored inside the lead’s approval record (plain JSON, so any ApprovalStore works), and the approved call runs exactly once, on resume.
  • Other tool calls of the same lead turn that finished keep their results and are not run again.
  • One approval is pending per run. If two sub-agents pause in the same turn, the run pauses on the first (in call order); the second sub-agent stops and the lead gets an error result for that task call saying its call was not run, so it can ask again after the approval.
  • A task call’s pre/post tool hooks fire again on resume, like the hooks of any approved tool.
  • Approvals need an approvalStore on the lead run, which createAgent() does not take yet; use AgentExecutor.execute() for the lead. Without one, the sub-agent’s call becomes an error result and nothing runs.
  • Token usage keeps adding up across the pause: the sub-agent’s usage before the pause is in the paused result, and what it spends after the resume is added to the resumed lead’s result.usage.

createDelegateTool()

createDelegateTool({ agent, provider, toolRegistry }) wraps one child agent as a tool you name and register yourself, with its own maxDepth guard. It runs on the same delegation core as task, so it inherits the parent runtime the same way, and its result shape ({ text, usage }) is unchanged. Prefer subagents for new code: one tool, a prompt listing, parallel tasks and depth limits come for free.