Agents
An agent is the top-level conversation orchestrator in Standard Agents. An agent has exactly two sides, and each side is backed by exactly one of three things: a human, a prompt, or a handler. There is no agent “type”: everything about how an agent converses, composes, and terminates is derived from the backings of its two sides.
A consistent division of responsibility runs through this page and every page it links to: prompts and handlers own turns, tools provide capabilities, hooks decorate turns, effects defer work, and subagent relationships define composition.
1. Sides and Backings
1.1 The Three Backings
| Backing | Who takes this side’s turns | Declared as |
|---|---|---|
| Human | A person, via whatever interface the runtime provides | { human: true } |
| Prompt | A model, driven by a prompt | { prompt: 'name', ... } |
| Handler | Deterministic code — a handler | { handler: 'name', ... } |
A handler is a function the runtime calls to take that side’s turns — not a hook, not a tool, and not an effect. See Handlers.
For a human backing, the specification defines only the waiting state: when it is a human-backed side’s turn, the session parks awaiting human input, and the turn is fulfilled by an inbound message on that side’s perspective. The specification deliberately does not define who the person is or how their input arrives — identity, notification, and reply surfaces are runtime and interface concerns.
1.2 SideConfig
A side’s configuration is discriminated by its backing: exactly one of human, prompt, or handler MUST be present.
HumanSide
| Property | Type | Description |
|---|---|---|
human | true | Required discriminator |
label | string | Optional UI/log label |
No other properties are legal on a human side. In particular, subagents is a definition-time error (nothing on a human side can invoke them), and stop/session bindings are meaningless.
PromptSide
| Property | Type | Default | Description |
|---|---|---|---|
prompt | string | Required | Prompt name for this side |
label | string | - | UI/log label |
subagents | SubagentRelationship[] | - | Side-owned child relationships. See Subagents |
stopOnResponse | boolean | true | Stop side turn on text response without tool calls |
stopTool | string | - | Stop side turn when this tool is called |
stopToolResponseProperty | string | - | Extracted property for stop tool outcomes |
maxSteps | number | - | Per-side step safety limit |
sessionStop | SessionToolBinding | - | End session successfully (object form only, see §3) |
sessionFail | SessionToolBinding | - | End session with failure |
sessionStatus | SessionToolBinding | - | Publish status updates |
HandlerSide
| Property | Type | Description |
|---|---|---|
handler | string | Required — handler name for this side |
label | string | Optional UI/log label |
subagents | SubagentRelationship[] | Side-owned child relationships. See Subagents |
Handler sides have no maxSteps, stopOnResponse, or session bindings: a handler turn is a single invocation, and handlers end sessions through ThreadState APIs (state.stopSession()) rather than declarative bindings.
1.3 Derivations
The following rules are derived from the sides’ backings and are never declared separately:
- Chat surface — the primary conversational interface renders wherever a
humanbacking is. A human on side A is legal. - Perspective — role mapping is the single source of perspective truth:
role: 'user'is the side B perspective,role: 'assistant'is the side A perspective — for queueing, injection, and rendering alike. See Messages. - Subagent eligibility — any agent may be composed as a subagent. Composing a child with a human side puts a person’s step inside the composition: the child session pauses until that person responds (§7).
- Termination discipline —
maxSessionTurnsis optional on every agent shape. Exchange counting is uniform across backings: a handler turn counts like a prompt turn when a cap is set. Two prompt-backed sides can ping-pong indefinitely, and a handler that replies to every inbound message can livelock against a prompt side just as hard — the cap is a voluntary safety valve available everywhere, and prompt and handler authors own their termination discipline.
2. AgentDefinition
2.1 Required
| Property | Type | Description |
|---|---|---|
name | string | Unique agent identifier |
sideA | SideConfig | Side A configuration |
sideB | SideConfig | Side B configuration |
Both sides are explicit. There is no implicit “missing side B is a human” default — if a person takes side B’s turns, write sideB: { human: true }.
2.2 Optional
| Property | Type | Default | Description |
|---|---|---|---|
maxSessionTurns | number | - | Session exchange safety cap (§1.3) |
title | string | - | Human-readable label |
description | string | - | Agent summary |
icon | string | - | Display icon |
exposeAsTool | boolean | false | Expose callable entrypoint (§5) |
toolDescription | string | - | Callable description |
env | Record<string, string> | - | Agent-level default variable values |
hooks | string[] | - | Fallback hook IDs |
parentHooks | ParentHookRequirement[] | - | Required hooks automatically contributed to a direct parent composition (§6) |
AgentDefinition also accepts optional packaging metadata — packageName, version, author, license — consumed only when the agent is packed for distribution. See Packaging.
3. SessionToolBinding
Session lifecycle bindings use the object form:
interface SessionToolBinding {
name: string;
messageProperty?: string;
attachmentsProperty?: string;
}
name: tool that controls the lifecycle actionmessageProperty: which arg field becomes lifecycle message textattachmentsProperty: which arg field contains attachment path(s)
For composed children, mapped stop/fail payloads are the canonical child → parent result/failure payload.
4. Stop Semantics
Runtime stop evaluation order for prompt-backed sides:
- session-level terminal bindings (
sessionStop/sessionFail) - side-level stop bindings (
stopTool) - response stop (
stopOnResponse) - safety limits (
maxSteps,maxSessionTurns)
A handler-backed side’s turn ends when its handler invocation returns (Handlers §3). A human-backed side’s turn ends when the human’s inbound message is persisted.
5. Tool Exposure
When exposeAsTool: true, an agent referenced directly in a prompt’s tools array is a handoff: the tool call transfers control of the same thread to the target agent, preserving history. Handoff requires the target agent to have a human side matching the thread’s conversational surface.
Composition into a child thread is not a prompt-tools concern: it is declared as a SubagentRelationship on the composing side. See Subagents. exposeAsTool + toolDescription are the composability contract — toolDescription is the default model-facing description when the agent is composed, and any agent may be composed.
6. Required Parent Hooks
An exposed agent MAY declare thread-scoped hooks that are required for the composed relationship to work correctly:
interface ParentHookRequirement {
hook: string;
reason: string;
stage?: 'before_parent' | 'after_parent';
}
defineAgent({
name: 'gmail_agent',
exposeAsTool: true,
parentHooks: [{
hook: 'gmail_context',
reason: 'Makes newly synchronized Gmail context visible to the coordinator.',
stage: 'before_parent',
}],
// ...
});
Downloading or installing a package is inert. Declaring a SubagentRelationship targeting the agent (or adding it to an enabled parent prompt as a handoff) is what composes it, and composing it MUST activate every parentHooks requirement automatically. A second accept-list is not required: composition UIs MUST disclose the hook ID, reason, and stage before the relationship is added or enabled.
The following rules are normative:
- Any agent with
exposeAsTool: trueMAY declareparentHooks— any agent is composable, so any exposed agent may carry composition requirements. - Requirements apply only to direct parent relationships; they do not propagate transitively through the child graph.
optionalrelationships contribute hooks only while their flag is enabled.hidden: trueaffects model visibility, not composition, so hidden enabled relationships still contribute their required hooks.- Each requirement names a thread-scoped hook in the child’s own package namespace. User-scoped hooks cannot be required on a parent.
- The hook receives the parent’s
ThreadState, not the child thread’s state, and remains active for the lifetime of the enabled relationship. - Missing, invalid, or unloadable required hooks MUST fail composition closed. Runtimes must not silently run a partially installed child.
- Duplicate hook IDs within one child’s
parentHooksdeclaration are invalid.
Required parent hooks are part of the child’s public behavioral contract, not an installer side effect. Optional child features that would change this contract require a separately configured child definition; absence from a required list does not mean “best effort.” Ordering and provenance are defined in Hooks §1.3.
7. Human-in-the-Loop Composition
Because any agent may be composed as a child, composing an agent with a human side puts a person’s step inside an otherwise automated composition. When execution reaches the human-backed side’s turn, the child session pauses: the child’s registry status becomes awaiting_human, and the session resumes when a human’s inbound message arrives on that side’s perspective.
This is a deliberate capability, not an accident of the model. The canonical use is an approval gate:
// agents/agents/approval_gate.ts — a child that asks a person before proceeding
export default defineAgent({
name: 'approval_gate',
exposeAsTool: true,
toolDescription: 'Request human approval for a proposed action. Returns the decision.',
sideA: {
prompt: 'approval_presenter', // renders the request, records the outcome
sessionStop: { name: 'record_decision', messageProperty: 'decision' },
},
sideB: { human: true }, // the approver
});
An orchestrator declares this child on one of its sides (see Subagents), spawns it with the proposed action, and the child parks in awaiting_human until the approver responds. The runtime does not define who sees the parked session or through which interface they answer — only the waiting state and its fulfillment.
Definition-time note: subagents on the human side of any agent remains an error — the human side of an approval gate cannot own children.
8. Definition-Time Errors
Implementations MUST reject the following at definition time:
- Both sides human — reserved. The mediated human↔human thread is coherent and the restriction may lift in a later version, but 0.2.0 runtimes MUST reject it.
subagentsdeclared on a human-backed side.- A side with zero backings, or more than one of
human/prompt/handler. - A handler side referencing an undefined handler; a prompt side referencing an undefined prompt.
- A relationship name (
as) appearing on both sides of one agent, colliding with another tool name, or colliding with a reserved lifecycle tool name — see Subagents §2. - Session bindings (
sessionStop/sessionFail/sessionStatus),maxSteps, or stop bindings on a non-prompt side. - A session binding that is not in object form (§3).
Validation issues SHOULD be reported as a structured list (code, severity, message, path) so tooling can surface every problem in one pass.
9. Examples
9.1 Assistant (human × prompt)
The everyday user-facing assistant — a person on side B, a model on side A:
defineAgent({
name: 'assistant',
sideA: { prompt: 'assistant' },
sideB: { human: true },
});
9.2 Worker / Reviewer (prompt × prompt)
Two models converse; each side sees itself as assistant and the other as user. maxSessionTurns is optional but strongly recommended here — nothing external stops two models:
defineAgent({
name: 'asset_subagent',
maxSessionTurns: 40,
exposeAsTool: true,
toolDescription: 'Generate and QA top-down game assets.',
sideA: {
label: 'Worker',
prompt: 'asset_worker',
stopOnResponse: true,
sessionFail: {
name: 'fail_asset',
messageProperty: 'reason',
attachmentsProperty: 'attachments',
},
},
sideB: {
label: 'Reviewer',
prompt: 'asset_reviewer',
stopOnResponse: false,
sessionStop: {
name: 'approve_asset',
messageProperty: 'summary',
attachmentsProperty: 'attachments',
},
sessionStatus: {
name: 'update_asset_status',
messageProperty: 'status',
},
},
});
9.3 Relay (handler × human)
A deterministic, model-less surface: a person talks to a handler that forwards traffic to a composed child and relays answers back. Zero LLM calls happen on this thread. The full pattern is worked through in Handlers §9:
defineAgent({
name: 'relay_agent',
sideA: {
handler: 'relay_pipe',
subagents: [{ agent: 'assistant_agent', maxInstances: 1 }],
},
sideB: { human: true },
});
10. TypeScript Reference
type SideConfig = HumanSide | PromptSide | HandlerSide;
interface HumanSide {
human: true;
label?: string;
}
interface PromptSide {
prompt: string; // StandardAgentSpec.Prompts
label?: string;
subagents?: SubagentRelationship[]; // see /0.2.0/specification/subagents
stopOnResponse?: boolean;
stopTool?: string;
stopToolResponseProperty?: string;
maxSteps?: number;
sessionStop?: SessionToolBinding;
sessionFail?: SessionToolBinding;
sessionStatus?: SessionToolBinding;
}
interface HandlerSide {
handler: string; // StandardAgentSpec.Handlers
label?: string;
subagents?: SubagentRelationship[];
}
interface SessionToolBinding {
name: string;
messageProperty?: string;
attachmentsProperty?: string;
}
interface ParentHookRequirement {
hook: string;
reason: string;
stage?: 'before_parent' | 'after_parent';
}
interface AgentDefinition<N extends string = string> {
name: N;
sideA: SideConfig;
sideB: SideConfig;
maxSessionTurns?: number;
title?: string;
description?: string;
icon?: string;
exposeAsTool?: boolean;
toolDescription?: string;
env?: Record<string, string>;
hooks?: string[];
parentHooks?: ParentHookRequirement[];
// Packaging metadata: packageName, version, author, license
}
function defineAgent<N extends string>(
options: AgentDefinition<N>
): AgentDefinition<N>;