Subagents
Subagents let one agent route delegate a bounded task to another agent route.
When at least one child is dispatchable, the parent gets an internal task({ subagent, input }) mechanism and a # Subagents prompt section. The child remains a real Dawn route with its own prompt, tools, state, memory, planning, skills, and subagents.
Use subagents when a specialist should own a piece of work instead of becoming another helper function in the parent prompt.
Quick start
Put child agent routes under the parent's subagents/ directory:
src/app/support/[tenant]/
index.ts
subagents/
research/
index.ts
tools/
searchDocs.tsThe child route should export an agent() descriptor. Give it a description; the parent uses that description to decide when to delegate.
import { agent } from "@dawn-ai/sdk"
export default agent({
model: "gpt-5-mini",
description: "Find relevant policy and product documentation before a support reply.",
systemPrompt: "You research internal documentation and return concise findings.",
})When the parent route runs, Dawn exposes a task tool. The model can call:
task({
subagent: "research",
input: "Find the current refund policy for annual plans.",
})The parent receives the child's final text as the tool result.
A subagent can also declare its own tools: { approve: [...] } to require human approval per call on any of its tools. The resulting kind: "tool" interrupt surfaces on the parent's stream, alongside the other subagent.* events, not on a separate child stream. See Per-tool approval.
Convention discovery
Dawn discovers immediate child routes at:
<parent route>/subagents/<leaf>/index.tsThe model-facing subagent value is the child folder name, such as "research".
Only immediate children count for the parent prompt. A child can have its own subagents/ directory, but those are available to that child, not directly to the original parent.
If a child route has no description, Dawn lists it as No description provided. The route still works, but selection quality will be worse.
Convention-only children receive the parent's delegation.default rule. They cannot have named exceptions because named rules are typed from explicit registration keys. Import and register a convention child when it needs its own rule.
Keyed registration
Register descriptors in a keyed object when you need a parent-local name or a named policy rule:
import { agent } from "@dawn-ai/sdk"
import researcher from "./shared-researcher/index.js"
import writer from "./subagents/writer/index.js"
export default agent({
model: "gpt-5-mini",
systemPrompt: "You coordinate customer support work.",
subagents: {
policyResearch: researcher,
draftWriter: writer,
},
delegation: {
default: "deny",
rules: {
policyResearch: { action: "allow" },
draftWriter: {
action: "approve",
reason: "Draft generation requires review.",
},
},
},
})The object key is the name shown to the parent model and the identity used by delegation policy and permission persistence. It is local to that parent, so different parents can expose the same child descriptor under different names.
Named delegation.rules keys are restricted by TypeScript to the keys in subagents. Registration names must match ^[A-Za-z0-9][A-Za-z0-9_-]*$; Dawn uses the exact spelling without trimming or case folding.
If an explicit descriptor points to a convention-discovered child, its explicit key replaces the folder-name identity for that parent. The convention name does not remain as an alias. This lets an explicit alias carry the named rule without leaving an ungoverned path to the same child.
Array-form registration is removed. Use a keyed registry such as subagents: { researcher, writer }; Dawn has no array compatibility path.
Delegation policy
Each parent owns the policy for its direct outbound dispatches:
delegation: {
default: "deny",
rules: {
researcher: { action: "allow" },
writer: { action: "deny", reason: "Drafting is disabled." },
reviewer: { action: "approve", reason: "Review external input." },
},
}default can be "allow", "deny", or "approve" and defaults to "allow". A named rule can use one of four actions:
| Action | Result |
|---|---|
allow | Dispatch immediately |
deny | Return [DAWN_E3002] with the configured or default reason |
approve | Pause for a kind: "subagent" permission decision |
constrain | Evaluate the current input and return allow, deny, or approval |
A constraint receives { input } plus the live parent route, child name and route, thread, route parameters, and cancellation signal:
import { agent, type DelegationConstraintPredicate } from "@dawn-ai/sdk"
import researcher from "./shared-researcher/index.js"
const restrictResearch: DelegationConstraintPredicate = ({ input }, context) => {
if (context.params?.tenant === "blocked") return "Tenant cannot delegate research."
if (input.includes("external")) {
return { approve: true, reason: "External research requires review." }
}
return true
}
export default agent({
model: "gpt-5-mini",
systemPrompt: "Coordinate support work.",
subagents: { researcher },
delegation: {
rules: {
researcher: { action: "constrain", predicate: restrictResearch },
},
},
})true allows the dispatch, a string denies it with that reason, and { approve: true, reason? } enters the approval gate. A predicate that throws or returns any other value fails closed, and the child does not start. Constraints inspect the input but cannot rewrite it.
Policy is evaluated again at every level. A parent rule governs only that parent's direct children; it does not authorize a child to dispatch a grandchild. The child's own delegation policy controls that next edge.
Approval decisions also follow the exact edge. always persists the tuple of parent route id and parent-local subagent name. Approving researcher under /support does not approve a child with the same name under /finance, another child of /support, or a deeper dispatch. See Subagent approval.
What the model sees
Dawn renders a prompt fragment like:
# Subagents
The following subagents are available. Call `task({ subagent, input })`
to dispatch a sub-task. Use the description to choose the right subagent
for each piece of work.
- **research** - Find relevant policy and product documentation before a support reply.The runtime task schema uses an enum of available parent-local names, so the model is constrained to known subagents. Statically denied children are omitted from both the prompt and schema. Allowed, approval-gated, and constrained children stay visible because a valid call may dispatch them.
Runtime behavior
At runtime, Dawn resolves the canonical registry and applies the parent's policy at the final dispatch boundary. The child receives the task as a user message:
{ messages: [{ role: "user", content: input }] }Each dispatch runs as a per-invocation LangGraph subgraph that inherits the root thread's checkpointer. Nested and parallel children keep independent checkpoint namespaces while remaining resumable through the root thread.
Dawn's dispatcher preserves depth metadata across nested calls and enforces its maximum depth of 3. Explicit registration cycles resolve lazily and are stopped by this runtime guard.
Dispatch failures
Delegation failures return coded results to the parent model:
DAWN_E3002means policy, a constraint, an approval decision, or non-interactive mode denied the dispatch.DAWN_E5003means the requested identity is unavailable, stale, or could not be started.
Invalid registration or policy configuration fails route checking or preparation with DAWN_E1004; it never falls back to allow. Underlying constraint exceptions are hidden from the model. Set DAWN_DEBUG_CONSTRAINTS=1 locally to log those details with the parent and child identities.
Streaming
When the parent is run through runs/stream, child activity is forwarded with subagent.* events:
subagent.startsubagent.tool_callsubagent.tool_resultsubagent.message- capability events such as
subagent.plan_update subagent.end
Each forwarded event includes a generated call_id. subagent.start also includes the subagent name, route id, and depth. subagent.end includes either final_message or error.
Dawn suppresses duplicate parent token events while a child run is active, so child tokens should appear as subagent.message rather than also leaking into the parent stream as ordinary message chunks.
If a child reaches a tool, path, command, memory, or delegation approval, Dawn surfaces a top-level interrupt on the root parent's stream. Resume the root thread with the complete ID-addressed pending interrupt set; do not start or resume the child separately. See Resuming an interrupted run.
Names and reserved policy fields
Convention and explicit subagents share one parent-local namespace. Route preparation reports DAWN_E1004 for invalid names, unresolved or ambiguous descriptors, duplicate explicit registrations of one route, and unresolved name collisions.
task is Dawn's internal model-facing dispatch mechanism, not a public tool-policy resource. It is invalid in tools.allow, tools.deny, tools.approve, and tools.constrain; the parent's delegation policy is the only dispatch authority. A subagent registration key may itself be task because registration values use a separate namespace.
Do not create a route tool named task. The scalar interrupt resume body is also removed; use the complete multi-entry resume envelope documented in Permissions.