@standardagents/skill 0.40.13 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@standardagents/skill",
3
- "version": "0.40.13",
3
+ "version": "0.41.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -20,6 +20,7 @@ For deeper reference material that this skill links to, see:
20
20
 
21
21
  - `agents/agents/AGENTS.md` — agent definition reference (created by `agents scaffold`)
22
22
  - `agents/prompts/AGENTS.md` — prompt and tool config reference
23
+ - `agents/handlers/AGENTS.md` — handler reference (`defineHandler`, deterministic side backings)
23
24
  - `agents/tools/AGENTS.md` — tool-writing patterns and `ThreadState` examples
24
25
  - `agents/models/AGENTS.md` — recommended model list (authoritative)
25
26
  - `agents/hooks/AGENTS.md` — hook reference
@@ -53,11 +54,11 @@ Most failed Standard Agent projects fail in the same seven ways. Read this secti
53
54
  **1. Architect the agent graph before picking types.**
54
55
  List the domains the system touches. Determine the tree (coordinator → domain agents → sub-domain agents). *Then* decide what each node is. Do not start by writing one agent and bolting tools onto it. → [Architecture & decomposition](#architecture--decomposition)
55
56
 
56
- **2. `dual_ai` is the default. `ai_human` is the exception.**
57
- `ai_human` is correct only when the thread itself is the chat surface (chat UI, website widget, AgentBuilder admin, direct API chat). For Slack, email, SMS, Discord, webhooks, polled inboxes, schedulers, or any other mediated channel, the human is just a tool target — the agent is `dual_ai`. The whole graph being `dual_ai` is fine and often cleaner. → [Interaction type](#interaction-type-dual_ai-is-the-default)
57
+ **2. Decide what backs each side. Prompt × prompt is the default worker shape; a human backing is the exception.**
58
+ Every agent has exactly two explicit sides, and each side is backed by exactly one of a human, a prompt, or a handler. A human backing is correct only when the thread itself is the chat surface (chat UI, website widget, AgentBuilder admin, direct API chat). For Slack, email, SMS, Discord, webhooks, polled inboxes, schedulers, or any other mediated channel, the human is just a tool target — back both sides with prompts. The whole graph being prompt × prompt is fine and often cleaner. → [What backs each side?](#what-backs-each-side)
58
59
 
59
- **3. Every `dual_ai` agent must have explicit session boundaries.**
60
- Name its `sessionStop` tool. Name its `sessionFail` tool. Set a finite `maxSessionTurns`. Give side_b a real, non-redundant job. If you cannot say in one sentence what tool call ends the session, the agent is not designed yet. → [Session boundary discipline](#session-boundary-discipline)
60
+ **3. Every prompt × prompt agent must have explicit session boundaries.**
61
+ Name its `sessionStop` tool. Name its `sessionFail` tool. Set a finite `maxSessionTurns` (the spec makes the cap optional; this skill makes it required practice for prompt × prompt). Give side B a real, non-redundant job. If you cannot say in one sentence what tool call ends the session, the agent is not designed yet. → [Session boundary discipline](#session-boundary-discipline)
61
62
 
62
63
  **4. Pick models from `current-models` only.**
63
64
  Never invent model strings from memory. Run `pnpm exec agents current-models`, choose by category, then run `pnpm exec agents available-models --provider=<name>` to confirm the exact string. → [Model selection](#model-selection)
@@ -79,27 +80,27 @@ In the Standard Agent paradigm, agents are the atomic unit of an AI system, and
79
80
 
80
81
  A "gmail" agent will outperform a "google apps" agent. A higher-level "communications" coordinator composes the gmail, slack, and SMS agents. A still-higher "personal assistant" coordinator composes communications, research, scheduling, and finance. This is fractal: the same shape scales up to teams, departments, and entire products. These agent-graphs solve real industry problems — progressive tool discovery, model/prompt tuning, context dilution, task resumability, and compaction.
81
82
 
82
- Just because subagents *can* compose complex behavior doesn't mean every step needs one. A subagent is a two-sided conversation where each side may take multiple steps per turn. Sometimes you only need a feature of a different model — generating an image, rewriting a paragraph in a more eloquent voice. For those, use a **subprompt** (a single LLM step exposed as a tool), not a subagent. Use a **subagent** when you need iteration, QA, reflection, or long-lived addressable behavior.
83
+ Just because subagents *can* compose complex behavior doesn't mean every step needs one. A subagent is a two-sided conversation where a prompt-backed side may take multiple steps per turn. Sometimes you only need a feature of a different model — generating an image, rewriting a paragraph in a more eloquent voice. For those, use a **subprompt** (a single LLM step exposed as a tool), not a subagent. Use a **subagent** when you need iteration, QA, reflection, or long-lived addressable behavior.
83
84
 
84
85
  When a subagent does QA on another model's output, prefer a *different provider* on the reviewing side. Same-lab models tend to rate their own output too generously.
85
86
 
86
87
  ### Example graph
87
88
 
88
89
  ```
89
- personal_assistant_coordinator (dual_ai, top-level reasoning model)
90
- ├── communications_coordinator (dual_ai, resumable, explicit parent communication, note: children are explicit because they receive inbound messages and should filter out noise before returning)
91
- │ ├── gmail_agent (dual_ai, resumable, immediate, explicit parent communication)
92
- │ ├── slack_agent (dual_ai, resumable, immediate, explicit parent communication)
93
- │ └── sms_agent (dual_ai, resumable, immediate, explicit parent communication)
94
- ├── research_assistant (dual_ai, resumable, explicit parent communication)
95
- │ ├── browser_use_agent (dual_ai, resumable, implicit parent communication)
96
- │ ├── google_drive_agent (dual_ai, resumable, implicit parent communication)
97
- │ └── notes_agent (dual_ai, resumable, implicit parent communication)
90
+ personal_assistant_coordinator (prompt × prompt, top-level reasoning model)
91
+ ├── communications_coordinator (prompt × prompt child, explicit parent communication, note: children are explicit because they receive inbound messages and should filter out noise before waking the parent)
92
+ │ ├── gmail_agent (prompt × prompt child, immediate, explicit parent communication)
93
+ │ ├── slack_agent (prompt × prompt child, immediate, explicit parent communication)
94
+ │ └── sms_agent (prompt × prompt child, immediate, explicit parent communication)
95
+ ├── research_assistant (prompt × prompt child, explicit parent communication)
96
+ │ ├── browser_use_agent (prompt × prompt child, implicit parent communication)
97
+ │ ├── google_drive_agent (prompt × prompt child, implicit parent communication)
98
+ │ └── notes_agent (prompt × prompt child, implicit parent communication)
98
99
  ├── scheduling_coordinator (subprompt)
99
- └── finance_ops_coordinator (dual_ai)
100
+ └── finance_ops_coordinator (prompt × prompt child)
100
101
  ```
101
102
 
102
- Note: no `ai_human` anywhere. The "human" enters via the gmail/slack/sms tool calls. The whole graph is autonomous.
103
+ Note: no human backing anywhere. The "human" enters via the gmail/slack/sms tool calls. The whole graph is autonomous, and every child is a persistent thread the parent can message again later — persistence is universal.
103
104
 
104
105
  ## The Standard Agent stack
105
106
 
@@ -107,9 +108,10 @@ The spec defines an agent as a composition of:
107
108
 
108
109
  - **Providers** — how to talk to LLM providers and model variants
109
110
  - **Models** — named model definitions referencing a provider
110
- - **Prompts** — system instructions plus the tools, subprompts, and subagents available at that step
111
+ - **Prompts** — system instructions plus the tools and subprompts available at that step
111
112
  - **Tools** — functions the agent can call to interact with the world (and other agents)
112
- - **Agents** — bind it all together: name, type, sides, session bindings
113
+ - **Handlers** — deterministic code that owns a side's turns (defined via `defineHandler` in `agents/handlers/`)
114
+ - **Agents** — bind it all together: two sides, each backed by a human, a prompt, or a handler, plus session bindings and side-owned subagent relationships
113
115
  - **Hooks** — custom code that intercepts lifecycle events (history filtering, message injection, tool result transforms)
114
116
  - **Effects** — custom code scheduled to run later
115
117
  - **Threads** — runtime instances of an agent, each with its own state, message history, and filesystem
@@ -123,8 +125,8 @@ The spec defines an agent as a composition of:
123
125
 
124
126
  1. List the domains the system actually touches.
125
127
  2. Draw the tree on paper or in a comment block. Coordinators on top, leaf agents at the bottom.
126
- 3. Pick interaction types. (Default `dual_ai`. Use `ai_human` only at chat-surface entry points.)
127
- 4. Name session boundaries for every `dual_ai` node.
128
+ 3. Pick backings for each side of each node. (Default prompt × prompt for workers. A human backing only at chat-surface entry points; a handler backing for deterministic, model-less surfaces.)
129
+ 4. Name session boundaries for every prompt × prompt node.
128
130
  5. Pick a model category for every node.
129
131
  6. List the third-party APIs each leaf needs and queue them for research.
130
132
  7. Map each capability to a `ThreadState` primitive before writing any custom plumbing.
@@ -147,7 +149,7 @@ Other smells that mean "decompose now":
147
149
  **Wrong** — one mega-agent with tools from unrelated domains:
148
150
 
149
151
  ```
150
- personal_assistant (ai_human)
152
+ personal_assistant (human × prompt)
151
153
  tools: send_gmail, read_gmail, search_ebay_listings, place_ebay_order,
152
154
  track_ebay_shipment, create_calendar_event, list_calendar_events,
153
155
  post_slack_message, read_slack_channel, query_stock_price,
@@ -159,7 +161,7 @@ Gmail, eBay, calendar, Slack, stock trading, recipes, image gen — seven unrela
159
161
  **Right** — coordinator + domain subagents:
160
162
 
161
163
  ```
162
- personal_assistant_coordinator (dual_ai, reasoning model)
164
+ personal_assistant_coordinator (prompt × prompt, reasoning model)
163
165
  ├── gmail_agent tools: send_gmail, read_gmail
164
166
  ├── ebay_agent tools: search_ebay_listings, place_ebay_order,
165
167
  │ track_ebay_shipment
@@ -184,81 +186,133 @@ Deeper graphs add latency and create more inter-agent communication that can fai
184
186
 
185
187
  ---
186
188
 
187
- ## Interaction type: `dual_ai` is the default
189
+ ## What backs each side?
188
190
 
189
- > **`dual_ai` is the default agent shape. `ai_human` is the exception**, used only when the thread itself is the chat surface — chat UI, website widget, AgentBuilder admin, or an API the user is talking to directly.
191
+ Every agent has **exactly two sides** — `sideA` and `sideB` — and each side is **backed by** exactly one of three things:
190
192
 
191
- For **Slack, email, SMS, Discord, webhooks, polled inboxes, schedulers, or any other mediated channel**, the human is just a tool target. The agent on the framework side is `dual_ai`. The whole graph being `dual_ai` is fine — often cleaner.
193
+ - `{ human: true }` — a person takes this side's turns
194
+ - `{ prompt: 'name', ... }` — a model, driven by a prompt
195
+ - `{ handler: 'name', ... }` — deterministic code (`defineHandler` in `agents/handlers/`)
196
+
197
+ There is no agent `type` field. Chat surface, composability, and termination are all **derived from the backings**. Both sides are always explicit — if a person takes side B's turns, write `sideB: { human: true }`; there is no implicit default side.
198
+
199
+ > **Prompt × prompt is the default worker shape. A human backing is the exception**, used only when the thread itself is the chat surface — chat UI, website widget, AgentBuilder admin, or an API the user is talking to directly.
200
+
201
+ For **Slack, email, SMS, Discord, webhooks, polled inboxes, schedulers, or any other mediated channel**, the human is just a tool target. Back both sides with prompts. The whole graph being prompt × prompt is fine — often cleaner.
192
202
 
193
203
  ### The single decision
194
204
 
195
- Ask: **"Is the human typing directly into this thread?"**
205
+ Ask, for each side: **"Who takes this side's turns?"**
196
206
 
197
- - **Yes** → the top-level agent is `ai_human`.
198
- - **No** → the top-level agent is `dual_ai`. The human enters the graph via a tool somewhere (e.g., a `send_slack_message` call inside a slack subagent).
207
+ - **A person typing directly into this thread** → `{ human: true }`. (Conventionally the person is side B, but a human on side A is legal.)
208
+ - **A model that must reason** → `{ prompt: '...' }`.
209
+ - **Deterministic code — routing, forwarding, protocol glue, no model judgment** → `{ handler: '...' }`.
199
210
 
200
- Do not default to `ai_human` just because a human is eventually involved. Ask where messages physically arrive.
211
+ Do not reach for a human backing just because a human is eventually involved. Ask where messages physically arrive. And do not burn a model on a side whose behavior is a five-line function — that's a handler.
201
212
 
202
- ### The two shapes
213
+ ### The common shapes
203
214
 
204
- **Shape A — chat surface (`ai_human` at top):**
215
+ **Human × prompt — chat surface:**
205
216
 
206
217
  ```
207
- website_chatbot (ai_human) ← user types in a chat widget
218
+ website_chatbot (human × prompt) ← user types in a chat widget
208
219
  tools: search_products, lookup_order, escalate_to_human
209
220
  ```
210
221
 
211
- **Shape B — mediated (`dual_ai` everywhere):**
222
+ **Prompt × prompt — autonomous worker/reviewer:**
212
223
 
213
224
  ```
214
- slack_research_assistant (dual_ai, reasoning model)
215
- ├── slack_agent (dual_ai, resumable) ← messages arrive via tool calls,
216
- │ not as thread messages
217
- └── research_agent (dual_ai)
225
+ slack_research_assistant (prompt × prompt, reasoning model)
226
+ ├── slack_agent (prompt × prompt child) ← messages arrive via tool calls,
227
+ │ not as thread messages
228
+ └── research_agent (prompt × prompt child)
218
229
  ```
219
230
 
220
- In Shape B, side_a of `slack_research_assistant` plans the work and dispatches subagents. Side_b reviews and decides when the work is done. The "user" is a Slack channel reachable through `slack_agent`'s tools.
231
+ Side A of `slack_research_assistant` plans the work and dispatches subagents. Side B reviews and decides when the work is done. The "user" is a Slack channel reachable through `slack_agent`'s tools.
232
+
233
+ **Handler × human — deterministic, model-less surface:**
234
+
235
+ A person talks to a thread on which **zero model work** happens: a handler forwards every inbound message to a composed child and relays answers back through userland messaging. See the [relay worked example](#worked-example-a-model-less-relay-handler--human) below.
236
+
237
+ ### Handlers: deterministic turn owners
238
+
239
+ A handler is a function the runtime calls to take a side's turns — not a hook (hooks decorate turns owned by something else; their failures are non-fatal), not a tool (tools are capabilities a turn-owner chooses to invoke), and not an effect (effects run outside the conversation flow). Handlers live in `agents/handlers/` and take their name from the filename:
240
+
241
+ ```ts
242
+ // agents/handlers/relay_pipe.ts
243
+ import { defineHandler } from '@standardagents/spec';
244
+
245
+ export default defineHandler({
246
+ description: 'Forwards inbound user traffic to the assistant child and relays answers back.',
247
+ execute: async (state, message) => {
248
+ // full ThreadState — forward, respond, spawn, stop, read/write KV
249
+ },
250
+ });
251
+ ```
221
252
 
222
- ### Handoffs (a special case of `ai_human`)
253
+ Rules to internalize:
223
254
 
224
- When an `ai_human` agent calls another `ai_human` agent as a tool, the runtime does **not** spawn a new thread — it changes which prompt "owns" the existing thread. This is a *handoff*. Useful for stepwise human flows: an `onboarding_agent` collects information, then hands off to a `scheduling_agent` that books a meeting. The user keeps talking to the same thread.
255
+ - **Delivery is exactly-once per message**, in persistence order, never concurrent per thread. A message that arrives while a handler turn is in flight waits in the queue. A handler that awaits a `blocking` child session holds that delivery slot for the whole child session — relay-style handlers should forward without blocking (`queueMessage` toward the child) and treat the child's replies as ordinary later turns.
256
+ - **A throwing handler is fatal to the turn, and only the turn.** The session continues; the next inbound message gets a fresh invocation. There is no automatic redelivery — retry is a userland decision.
257
+ - **No `maxSteps`, no stop bindings, no session bindings** on handler sides — a handler turn has no steps to cap and ends when `execute` returns. Handlers end sessions via `state.stopSession()`.
258
+ - **Handlers may not appear in prompt `tools` arrays, hook lists, or effect schedules.** They are referenced from exactly one place: the `handler` property of a side.
259
+ - **No auto-injected reply channel.** *The runtime routes inbound; userland decides outbound* — if a handler should speak to a user or an external surface, that is userland code calling the messaging surface it chooses (the `message_user` pattern).
260
+
261
+ ### Handoffs
262
+
263
+ When an agent with `exposeAsTool: true` is referenced **directly in a prompt's `tools` array**, the tool call is a *handoff*: the runtime does **not** spawn a new thread — it transfers control of the *same thread* to the target agent, preserving history. The target must have a human side matching the thread's conversational surface. Useful for stepwise human flows: an `onboarding_agent` collects information, then hands off to a `scheduling_agent` that books a meeting. The user keeps talking to the same thread.
264
+
265
+ Composition into a **child thread** is never a `tools` entry — it is a `SubagentRelationship` declared on a side's `subagents` array. Handoffs are the only agents-as-tools references.
225
266
 
226
267
  ### Subprompts vs. subagents vs. handoffs
227
268
 
228
- - **Subprompt** — one LLM step exposed as a tool. Use to switch models for a focused task: image generation, polished writing, JSON extraction.
229
- - **Subagent** — full child thread with its own `ThreadState`. Use when you need iteration, QA, reflection, or long-lived addressable behavior. Always `dual_ai`.
230
- - **Handoff** — `ai_human` → `ai_human`, swaps prompt ownership of the same thread. Use for stepwise human-driven flows.
269
+ - **Subprompt** — one LLM step exposed as a tool. Use to switch models for a focused task: image generation, polished writing, JSON extraction. Listed in a prompt's `tools` array.
270
+ - **Subagent** — *any* agent composed as a child via a side's `subagents` array. Full child thread with its own `ThreadState`. Use when you need iteration, QA, reflection, or long-lived addressable behavior. Composing a child with a human side puts a person's step inside the composition — the approval-gate pattern; the child parks with status `awaiting_human` until the person responds.
271
+ - **Handoff** — an `exposeAsTool` agent in a prompt's `tools` array; swaps control of the same thread. Use for stepwise human-driven flows.
231
272
 
232
- A subagent can be:
273
+ **Persistence is universal.** Every child instance is a persistent thread — there are no disposable instances and no opt-in persistence wrapper. Session end (stop, fail, error, or turn-cap) never terminates the thread; it transitions registry status, and a later message starts a new session with history intact. Only explicit `terminate()` destroys an instance.
233
274
 
234
- - **Blocking + non-resumable** — the parent waits, the child runs once, returns, and is gone (tool-call style)
235
- - **Blocking + resumable** — the parent waits but the child remains addressable for future calls
236
- - **Non-blocking + non-resumable** — fire-and-forget one-shot
237
- - **Non-blocking + resumable** — long-lived addressable child the parent can re-message later (e.g., a Slack monitor that lives as long as the parent)
275
+ `blocking` is purely a *waiting style* — whether `subagent_create` awaits the child session's close and returns its result — and is orthogonal to persistence:
276
+
277
+ - **Blocking** — the parent waits for the child session to close and receives its result payload (tool-call feel). With `maxInstances: 1` and an existing instance, `subagent_create` behaves as **create-or-continue**: it messages the existing instance and awaits *that* session's close.
278
+ - **Non-blocking** — fire-and-forget spawn; the child's completion arrives later as a message (or not at all, with `parentCommunication: 'explicit'`).
279
+
280
+ Either way the child remains addressable via `subagent_message` afterward.
238
281
 
239
282
  ---
240
283
 
241
284
  ## Session boundary discipline
242
285
 
243
- This section is a hard rule, not a suggestion. **Every `dual_ai` agent in the graph must explicitly define its session boundaries.** This is what makes whole-graph `dual_ai` safe.
286
+ This section is a hard rule, not a suggestion. **Every composed prompt × prompt agent must explicitly define its session boundaries.** Two prompt-backed sides can ping-pong indefinitely — nothing external stops two models — so the spec keeps the caps voluntary and puts termination discipline on prompt and handler authors. This discipline is what makes whole-graph prompt × prompt safe.
287
+
288
+ ### Required practice for every composed prompt × prompt child
244
289
 
245
- ### Required for every `dual_ai` agent
290
+ - **`sessionStop` binding** (object form: `{ name, messageProperty }`) — names the tool call that ends the session with a result. Side A or side B invokes it when the work is done. Common patterns: `report_findings`, `submit_to_parent`, `final_answer`, `done`. For composed children, the mapped payload is the canonical child → parent result.
291
+ - **`sessionFail` binding** — names the tool call that ends the session with a failure the parent should see. Common patterns: `escalate_blocked`, `report_unresolvable`, `give_up_with_reason`.
292
+ - **`maxSessionTurns`** — optional on *every* agent shape in the spec, with uniform exchange counting across backings (a handler turn counts like a prompt turn). Treat it as required practice for prompt × prompt agents: a finite integer sized to the realistic upper bound for the task. A research subagent might be 20; an asset QA loop might be 6; a tight reflection loop might be 3. It is a voluntary safety valve — the session tools are the *designed* ending; the cap is the backstop.
293
+ - **A real job for side B** — reflection, QA, judging, alternative-perspective driving, devil's advocate. If side B is "another instance of side A," you don't have a prompt × prompt agent — collapse it to a single-side prompt or a coordinator pattern.
294
+ - **Cross-provider QA** — when side B reviews side A's output, pick a model from a *different provider* than side A. Same-lab models systematically over-rate their own work.
246
295
 
247
- - **`sessionStop` tool binding** — names the tool call that ends the session with a result. Side_a or side_b invokes it when the work is done. Common patterns: `report_findings`, `submit_to_parent`, `final_answer`, `done`.
248
- - **`sessionFail` tool binding** — names the tool call that ends the session with a failure the parent should see. Common patterns: `escalate_blocked`, `report_unresolvable`, `give_up_with_reason`.
249
- - **`maxSessionTurns`** — a finite integer sized to the realistic upper bound for the task. Never omit this. A research subagent might be 20; an asset QA loop might be 6; a tight reflection loop might be 3.
250
- - **A real job for side_b** — reflection, QA, judging, alternative-perspective driving, devil's advocate. If side_b is "another instance of side_a," you don't have a `dual_ai` agent — collapse it to a single-side prompt or a coordinator pattern.
251
- - **Cross-provider QA** — when side_b reviews side_a's output, pick a model from a *different provider* than side_a. Same-lab models systematically over-rate their own work.
296
+ Session bindings are legal **only on prompt-backed sides** — a session binding, `maxSteps`, or stop binding on a human or handler side is a definition-time error.
297
+
298
+ ### Handler termination discipline
299
+
300
+ A handler runs only when a message arrives, so it cannot run away on its own — but a handler that *replies to every inbound message* paired with a prompt-backed side livelocks exactly like two models. Handler authors own their termination discipline:
301
+
302
+ - reply conditionally, not unconditionally;
303
+ - or set `maxSessionTurns` on the agent (available on every shape) as a hard safety valve;
304
+ - or end the session explicitly with `state.stopSession()` when the work is done.
252
305
 
253
306
  ### The smell test
254
307
 
255
- > If you cannot articulate in one sentence what tool call ends the session, the agent is not designed yet.
308
+ > If you cannot articulate in one sentence what tool call (or `stopSession` call) ends the session, the agent is not designed yet.
256
309
 
257
310
  Examples that pass:
258
311
 
259
- - "Side_a calls `submit_video_assets` once side_b approves the renders."
260
- - "Side_b calls `report_findings` after the research turn count exceeds 5 or it judges the topic exhausted."
312
+ - "Side A calls `submit_video_assets` once side B approves the renders."
313
+ - "Side B calls `report_findings` after the research turn count exceeds 5 or it judges the topic exhausted."
261
314
  - "Either side calls `escalate_to_parent` if the customer policy question is unresolvable."
315
+ - "The relay handler calls `state.stopSession()` when the child reports the workflow finished."
262
316
 
263
317
  Examples that fail:
264
318
 
@@ -267,7 +321,7 @@ Examples that fail:
267
321
 
268
322
  ### Why this matters
269
323
 
270
- A `dual_ai` agent with no `sessionStop`, no `sessionFail`, and no `maxSessionTurns` will burn turns until it hits a runtime cap, then fail in a way the parent can't interpret. Coding agents that have been bitten by this once will start defaulting to `ai_human` to "feel safer" — and then we're back to mega-agents and chat-surface confusion. Boundary discipline is what keeps `dual_ai` the default.
324
+ A prompt × prompt agent with no `sessionStop`, no `sessionFail`, and no `maxSessionTurns` will burn turns until it hits a runtime cap, then fail in a way the parent can't interpret. Coding agents that have been bitten by this once will start bolting a human backing onto workers to "feel safer" — and then we're back to mega-agents and chat-surface confusion. Boundary discipline is what keeps prompt × prompt the default worker shape.
271
325
 
272
326
  ---
273
327
 
@@ -283,9 +337,9 @@ The most common quality failure when a coding agent creates a Standard Agent is
283
337
  | `description` | One concrete sentence: what it does, for whom, with what. Shown in UIs. | "An agent that helps with email." | "Triages the support inbox, drafts replies for known issues, and escalates unknowns to a human queue." |
284
338
  | `toolDescription` | Written for the **calling model**, not a human: when to choose this tool, what the key arguments are, what comes back. Required when `exposeAsTool: true`. | "Research agent." | "Research a topic with web search and source-backed URLs; returns a cited answer after an internal sufficiency review. Use for questions that need current or verifiable facts." |
285
339
  | `icon` | Always set one. See below. | omitted | inline SVG data URI or hosted URL |
286
- | `sideA.label` / `sideB.label` | Always set both on `dual_ai`. Name the **roles in the conversation** — they appear in the UI and logs. | "Side A" / "Side B", "AI 1" / "AI 2" | "Researcher" / "Reviewer", "Pilot" / "ATC", "Drafter" / "Editor", "Support" / "Customer" |
340
+ | `sideA.label` / `sideB.label` | Always set both on prompt × prompt agents. Name the **roles in the conversation** — they appear in the UI and logs. | "Side A" / "Side B", "AI 1" / "AI 2" | "Researcher" / "Reviewer", "Pilot" / "ATC", "Drafter" / "Editor", "Support" / "Customer" |
287
341
 
288
- Side labels are also a design check: if you can't name the two roles, side_b probably doesn't have a real job yet — go back to [Session boundary discipline](#session-boundary-discipline).
342
+ Side labels are also a design check: if you can't name the two roles, side B probably doesn't have a real job yet — go back to [Session boundary discipline](#session-boundary-discipline).
289
343
 
290
344
  ### Icons
291
345
 
@@ -310,7 +364,7 @@ A session-ending tool is not "configured" until it appears in **all three** of t
310
364
 
311
365
  1. **`agents/tools/<name>.ts`** — a real `defineTool` whose args schema carries the payload fields (e.g. `final_answer`, `failure_summary`). The tool body can be a simple formatter; its job is to be callable and carry structured args.
312
366
  2. **The side's prompt `tools` array** — the model can only call tools listed at the active step. A `sessionStop` binding to a tool the prompt never offers means the session *can never end on purpose*; it burns `maxSessionTurns` and fails opaquely.
313
- 3. **The `SideConfig` binding** — `sessionStop: { name: 'complete_research', messageProperty: 'final_answer' }`. Prefer the object form: `messageProperty` (and `attachmentsProperty` when files are returned) define the canonical payload the parent receives from a subagent. A bare string binding with no mapped property returns a vague payload upstream.
367
+ 3. **The prompt side's binding** — `sessionStop: { name: 'complete_research', messageProperty: 'final_answer' }`. Bindings are object-form only (bare strings are rejected at definition time), and they are legal only on prompt-backed sides. `messageProperty` (and `attachmentsProperty` when files are returned) define the canonical payload the parent receives from a subagent. A binding with no mapped `messageProperty` returns a vague payload upstream.
314
368
 
315
369
  And a fourth, softer place: **the prompt text must tell the model when to call it** ("When the renders pass review, call `submit_video_assets` with ..."). Models do not infer termination protocol from tool names.
316
370
 
@@ -327,9 +381,9 @@ Quick failure-mode table:
327
381
 
328
382
  A two-sentence system prompt is a skeleton, not a prompt. Every side's prompt should cover, roughly in this order:
329
383
 
330
- 1. **Role and counterpart.** Who this side is *and who it is talking to*: "You are the researcher. The other participant is the user who needs the question answered." In `dual_ai`, each side must understand the other side's role or the conversation degrades into two assistants being polite to each other.
384
+ 1. **Role and counterpart.** Who this side is *and who it is talking to*: "You are the researcher. The other participant is the user who needs the question answered." In prompt × prompt, each side must understand the other side's role or the conversation degrades into two assistants being polite to each other.
331
385
  2. **Objective and definition of done.** One sentence the model can test against: "Done means every factual claim has a source URL and the reviewer has approved."
332
- 3. **Tool guidance.** When to use which tool — including the session tools: name them, say exactly what triggers calling them, and what belongs in each argument. For resumable subagents, distinguish `subagent_create` (new topic/instance) from `subagent_message` (follow-up to an existing instance).
386
+ 3. **Tool guidance.** When to use which tool — including the session tools: name them, say exactly what triggers calling them, and what belongs in each argument. For sides that own subagent relationships, distinguish `subagent_create` (new topic/instance) from `subagent_message` (follow-up to an existing instance), and tell the model to check `subagents` before creating so existing instances are reused.
333
387
  4. **Edge cases.** This is what separates a thoughtful prompt from a placeholder. Walk the domain and enumerate what realistically goes wrong, with an instruction for each. The recurring ones:
334
388
  - *Empty or low-quality results* — retry differently, broaden, or report honestly? How many attempts before giving up?
335
389
  - *Auth / permission / quota errors from tools* — never retry blindly; call `sessionFail` with what the operator must fix.
@@ -348,17 +402,19 @@ Anti-patterns: a prompt that just restates the tool list ("You have tools to sea
348
402
 
349
403
  When the new agent is a child in a graph, creating the agent file is half the job. Verify the full wiring:
350
404
 
351
- - [ ] Child is `dual_ai` with `exposeAsTool: true` and a calling-model-grade `toolDescription`
352
- - [ ] Child has finite `maxSessionTurns`, and `sessionStop` + `sessionFail` bound on the side(s) that own those decisions
405
+ - [ ] Child has `exposeAsTool: true` and a calling-model-grade `toolDescription` (the relationship's `description` can override it per-composition)
406
+ - [ ] Prompt × prompt children have finite `maxSessionTurns`, and `sessionStop` + `sessionFail` bound on the prompt side(s) that own those decisions
353
407
  - [ ] The session tools pass the [three-places check](#session-tools-must-exist-in-three-places)
354
- - [ ] The **parent prompt's `tools` array** has the subagent entry — an agent nobody references is unreachable. Choose `blocking` / `resumable` deliberately ([the four combinations](#subprompts-vs-subagents-vs-handoffs)), and map `initUserMessageProperty` (and `initAttachmentsProperty` for files) so the child starts with real context instead of an empty first turn
355
- - [ ] The **parent prompt text** says when to delegate to this child and, if resumable, explains `subagent_create` vs `subagent_message`
356
- - [ ] Side_b reviewers use a model from a *different provider* than side_a ([why](#session-boundary-discipline))
357
- - [ ] Run the dev server and check the composition view: the parent→child edge should be visible, and creating a test thread should show the child spawn and the completion payload arrive back
408
+ - [ ] The **owning parent side's `subagents` array** has the `SubagentRelationship` — an agent nobody declares is unreachable, and the declaration is the grant. Never put the relationship in a prompt's `tools` array (definition-time error). Declare it on the side whose turns should receive the child's traffic
409
+ - [ ] `blocking` and `maxInstances` are chosen deliberately ([waiting style vs. lifetime](#subprompts-vs-subagents-vs-handoffs)), and `initUserMessageProperty` (and `initAttachmentsProperty` for files) are mapped so the child starts with real context instead of an empty first turn
410
+ - [ ] The relationship name (`as`, defaulting to `agent`) doesn't collide with any tool name or the reserved lifecycle names (`subagents`, `subagent_create`, `subagent_message`), and appears on only one side
411
+ - [ ] The **parent prompt text** (for prompt-backed owning sides) says when to delegate to this child and explains `subagent_create` vs `subagent_message`
412
+ - [ ] Side B reviewers use a model from a *different provider* than side A ([why](#session-boundary-discipline))
413
+ - [ ] Run the dev server and check the composition view: the parent→child edge should be visible, and creating a test thread should show the child spawn and the completion payload arrive back on the owning side
358
414
 
359
415
  ### Worked example: a complete minimal agent
360
416
 
361
- This is the shape every new `dual_ai` agent should have on day one — identity, icon, labels, both session tools, wired prompts. (Compressed for space; real prompts should be longer per the rubric above.)
417
+ This is the shape every new prompt × prompt agent should have on day one — identity, icon, labels, both session tools, wired prompts. (Compressed for space; real prompts should be longer per the rubric above.)
362
418
 
363
419
  ```ts
364
420
  // agents/tools/complete_audit.ts
@@ -376,9 +432,10 @@ export default defineTool({
376
432
 
377
433
  ```ts
378
434
  // agents/agents/invoice_audit_agent.ts
435
+ import { defineAgent } from '@standardagents/spec';
436
+
379
437
  export default defineAgent({
380
438
  name: 'invoice_audit_agent',
381
- type: 'dual_ai',
382
439
  description: 'Audits uploaded invoices against purchase orders and reports discrepancies.',
383
440
  icon: invoiceAuditIcon, // inline SVG data URI, see Icons above
384
441
  maxSessionTurns: 16,
@@ -401,7 +458,48 @@ export default defineAgent({
401
458
  });
402
459
  ```
403
460
 
404
- Both prompts list their session tools in `tools` and the reviewer prompt instructs: *"Approve by calling `complete_audit` with the full findings in `findings` only after every line item has been checked; if documents are unreadable or POs are missing after one clarification round, call `fail_audit` and name exactly what the requester must provide."* The parent prompt adds `{ name: 'invoice_audit_agent', initUserMessageProperty: 'request', initAttachmentsProperty: 'invoice_files' }` to its `tools` and explains when to delegate.
461
+ Both prompts list their session tools in `tools` and the reviewer prompt instructs: *"Approve by calling `complete_audit` with the full findings in `findings` only after every line item has been checked; if documents are unreadable or POs are missing after one clarification round, call `fail_audit` and name exactly what the requester must provide."* The parent declares the composition on its owning side — `subagents: [{ agent: 'invoice_audit_agent', blocking: true, initUserMessageProperty: 'request', initAttachmentsProperty: 'invoice_files' }]` — and the parent prompt text explains when to delegate.
462
+
463
+ ### Worked example: a model-less relay (handler × human)
464
+
465
+ When a surface needs routing but no model judgment — a front door that pipes user traffic to a composed assistant and relays answers back — don't fake it with a prompt told to "just forward everything." Back the side with a handler. Zero LLM calls happen on this thread:
466
+
467
+ ```ts
468
+ // agents/handlers/relay_pipe.ts
469
+ import { defineHandler } from '@standardagents/spec';
470
+
471
+ export default defineHandler({
472
+ description: 'Pipe user traffic to the assistant child; relay answers outward.',
473
+ execute: async (state, message) => {
474
+ const child = state.children.find(c => c.relationship === 'assistant_agent');
475
+ if (child) {
476
+ const thread = await state.getChildThread(child.reference);
477
+ await thread?.queueMessage({ role: 'user', content: message.content ?? '' });
478
+ }
479
+ },
480
+ });
481
+ ```
482
+
483
+ ```ts
484
+ // agents/agents/relay_agent.ts
485
+ import { defineAgent } from '@standardagents/spec';
486
+
487
+ export default defineAgent({
488
+ name: 'relay_agent',
489
+ sideA: {
490
+ handler: 'relay_pipe',
491
+ subagents: [{
492
+ agent: 'assistant_agent',
493
+ maxInstances: 1,
494
+ immediate: true, // spawned at thread creation, before any side takes a turn
495
+ parentCommunication: 'explicit',
496
+ }],
497
+ },
498
+ sideB: { human: true },
499
+ });
500
+ ```
501
+
502
+ The child's answers arrive as inbound messages on side A — the relationship's owning side — and get their own handler turns, where the handler calls the project's outbound messaging tool. The handler forwards with `queueMessage` rather than a blocking spawn: a handler that awaits a blocking child session holds its thread's one-at-a-time delivery slot for the whole session. Note the handler side declares its child but needs no tool grants — the side's declaration is the grant, and the handler reaches children through `ThreadState` directly.
405
503
 
406
504
  ---
407
505
 
@@ -426,7 +524,7 @@ If you have multiple configured providers, pass `--provider=<name>` explicitly.
426
524
  | Domain subagent (gmail, slack, etc.) | `fast_tool_calls` | High volume, narrow tool set, latency matters. |
427
525
  | Eloquent text generation | `writing` | Use as a subprompt from a fast-tool-calling agent. |
428
526
  | Image generation | `image_generation` | Use as a subprompt. |
429
- | QA / reviewer side_b | `extra_reasoning` from a *different provider* than side_a | Same-lab QA is biased. |
527
+ | QA / reviewer side B | `extra_reasoning` from a *different provider* than side A | Same-lab QA is biased. |
430
528
  | Cheap classification, tagging, routing | `tiny` | Where speed and cost dominate quality. |
431
529
 
432
530
  ### Fallback strategy
@@ -594,14 +692,14 @@ By default, `runCode()` executes the `default` export with no args. Use `execute
594
692
 
595
693
  ### Notes on a few that are easy to misuse
596
694
 
597
- - **`state.arguments`** is read-only invocation/creation data. Resumable `subagent_create.arguments` persists with the child thread, is visible to both child sides, and is also available as prompt variables.
695
+ - **`state.arguments`** is read-only invocation/creation data. `subagent_create` arguments are validated against the receiving child's side A prompt `requiredSchema`, persist for the life of the child thread, are hydrated for both child sides on every activation, and are also available as prompt variables.
598
696
  - **`state.context`** is in-memory scratch data for the *current execution*. It is not durable across thread restarts and should not be used for invocation or subagent creation arguments. For durable structured state, use `state.getValue` / `state.setValue`.
599
697
  - **`state.getValue` / `state.setValue`** are durable per-thread JSON storage. Use them for small structured state; use files for larger content or artifacts.
600
698
  - **`state.env` / `state.envType` / `state.setEnv`** are for runtime configuration and secrets. `state.env(name)` resolves thread -> account -> instance -> agent -> prompt. `state.envType(name)` returns `'secret'` by default; `'text'` means the value may be shown in tool output. `state.setEnv(name, value, { type: 'text' | 'secret' })` writes thread-scoped env and propagates to active descendants. Omit `type` only when preserving the existing type is intentional; new keys default to secret.
601
699
  - **`state.runCode`** runs JavaScript or TypeScript in an isolated Dynamic Worker sandbox. The sandbox does not receive `ThreadState`, env, files, network, or host globals implicitly. Pass exact capabilities through `imports`, `globals`, `modules`, and `execute`. It executes exported values/functions; code such as `console.log(fibonacci(5))` can log, but it must still `export default ...` or export a named function/value for the host to receive a result.
602
700
  - **`state.scheduleEffect`** runs a named effect after a delay. It survives restarts. This is your cron, your queue, and your retry timer all in one.
603
701
  - **`state.invokeTool` vs `state.queueTool`** — `invokeTool` runs synchronously and returns the result; `queueTool` schedules the call to run later in the normal tool-call flow. Prefer `queueTool` when the model should see the result as a regular tool call.
604
- - **`state.notifyParent`** — for resumable subagents with `parentCommunication: 'explicit'`, this is the main way the child talks to the parent. Use it sparingly; every delivered notification interrupts the parent. If there is no parent, nothing is queued externally.
702
+ - **`state.notifyParent`** — for children whose relationship sets `parentCommunication: 'explicit'`, this is the main way the child talks to the parent. Use it sparingly; every delivered notification interrupts the parent. If there is no parent, nothing is queued externally.
605
703
  - **`state.stopSession`** — terminally ends the active agent session. Use `state.stopSession({ result, notifyParent, status })` when a tool should return a blocking result, optionally notify the parent, and mark the child status without requiring a session tool call.
606
704
  - **File attachments** use the path convention `/attachments/{filename}.{ext}`. Always use this path when passing files between agents — the runtime copies them across thread filesystems automatically.
607
705
 
@@ -629,11 +727,16 @@ Also tell it what **not** to do: do not read `process.env`, do not call `env()`
629
727
 
630
728
  ## Tools
631
729
 
632
- A "tool" is anything an agent can call. There are three kinds:
730
+ A "tool" is anything a prompt can call. There are three kinds of `tools` entries:
633
731
 
634
- 1. **Callables** — TypeScript functions defined via `defineTool` in `agents/tools/`. The primary way to interface with the outside world: APIs, databases, business logic, and (sometimes) other agents. Each callable receives a `ThreadState`. Do not assume Node APIs are available — your code may run on the edge.
732
+ 1. **Callables** — TypeScript functions defined via `defineTool` in `agents/tools/`. The primary way to interface with the outside world: APIs, databases, business logic. Each callable receives a `ThreadState`. Do not assume Node APIs are available — your code may run on the edge.
635
733
  2. **Subprompts** — prompts exposed as tools via their `toolDescription`. A single-step LLM call. Use for switching models on a focused task (image generation, polished writing, JSON extraction).
636
- 3. **Subagents** — full agents exposed as tools via `exposeAsTool: true` on the agent definition. Use when you need iteration, QA, reflection, or long-lived addressable behavior. Always `dual_ai`.
734
+ 3. **Handoff agents** — agents with `exposeAsTool: true` referenced directly by name. The call transfers control of the *same thread* to the target agent ([handoffs](#handoffs)).
735
+
736
+ Two things that are deliberately **not** tools:
737
+
738
+ - **Subagent relationships** are declared on a side's `subagents` array, never in a prompt's `tools` array — a `tools` entry attempting to configure one is a definition-time error. The prompt backing an owning side automatically receives the injected lifecycle tools (`subagents`, `subagent_create`, `subagent_message`) for that side's non-hidden relationships; their names are reserved.
739
+ - **Handlers** own a side's turns and may not be referenced by prompt `tools` arrays, hook lists, or effect schedules.
637
740
 
638
741
  ### Provider-visible argument schemas
639
742
 
@@ -661,8 +764,10 @@ PromptDefinition
661
764
  toolChoice 'auto' | 'none' | 'required' tool calling strategy (default 'auto')
662
765
  requiredSchema ZodSchema validate args when called as a tool
663
766
  variables VariableDefinition[] declared text/secret variables
664
- tools (string | SubpromptConfig | PromptToolConfig | SubagentToolConfig)[]
665
- tools available at this step
767
+ tools (string | SubpromptConfig | PromptToolConfig)[]
768
+ tools available at this step: callables,
769
+ subprompts, handoff agents — never subagent
770
+ relationships (those live on sides) or handlers
666
771
  env Record<string, string> prompt-level env values (lowest precedence)
667
772
  reasoning ReasoningConfig extended thinking config (for models that support it)
668
773
  recentImageThreshold number (default 10) how many recent messages keep real images
@@ -717,97 +822,134 @@ Use `{ type: 'env', property: 'PRODUCT_INVENTORY' }` parts to inject runtime val
717
822
 
718
823
  ### `AgentDefinition` cheat sheet
719
824
 
720
- The agent definition binds prompts and sides together. Set on each file in `agents/agents/`. For full signatures, see `agents/agents/AGENTS.md`.
825
+ The agent definition binds two backed sides together. Set on each file in `agents/agents/`. For full signatures, see `agents/agents/AGENTS.md`.
721
826
 
722
827
  ```
723
828
  AgentDefinition
724
829
  name string unique snake_case identifier
725
- type 'ai_human' | 'dual_ai' default 'ai_human' — but see "dual_ai is the default"
726
- maxSessionTurns number REQUIRED for dual_ai. Finite turn cap.
727
- sideA SideConfig AI side (or first AI in dual_ai)
728
- sideB SideConfig second AI side; required for dual_ai
729
- exposeAsTool boolean (default false) enables this agent to be called as a tool by other prompts
730
- toolDescription string required if exposeAsTool: true — write it for the calling model
830
+ sideA SideConfig REQUIRED — HumanSide | PromptSide | HandlerSide
831
+ sideB SideConfig REQUIRED — HumanSide | PromptSide | HandlerSide
832
+ maxSessionTurns number optional on every shape; uniform exchange counting
833
+ across backings — required practice for prompt × prompt
834
+ title string human-readable label
731
835
  description string brief human description for UIs — always set on new agents
732
836
  icon string URL or inline SVG data URI — always set on new agents
837
+ exposeAsTool boolean (default false) composability contract: handoff target and default
838
+ description source when composed as a child
839
+ toolDescription string required if exposeAsTool: true — write it for the calling model
733
840
  env Record<string, string> agent-level env values
734
841
  hooks HookId[] agent-scoped hooks (when prompts don't specify their own)
842
+ parentHooks ParentHookRequirement[] required hooks auto-contributed to a direct parent
843
+ composition; legal on any agent with exposeAsTool: true
735
844
  ```
736
845
 
737
- `SideConfig` is where you set the side's `label` and `prompt` and bind the **session lifecycle tools**:
846
+ Each side is discriminated by its backing — exactly one of `human`, `prompt`, or `handler`:
738
847
 
739
- - `label` — the role name shown in the UI and logs ("Researcher", "Reviewer") — always set on `dual_ai` sides
740
- - `prompt` — the prompt this side runs (references `agents/prompts/<name>`)
741
- - `sessionStop` — tool whose call ends the session with a result; prefer `{ name, messageProperty }` so the payload is explicit
742
- - `sessionFail` — tool whose call ends the session with a failure; same object form
743
- - `sessionStatus` — optional, a tool used to publish status updates mid-run without ending the session
744
- - `maxSteps` — per-side step cap within one turn
848
+ ```
849
+ HumanSide
850
+ human true required discriminator
851
+ label string role name shown in the UI and logs
852
+
853
+ PromptSide
854
+ prompt string the prompt this side runs (agents/prompts/<name>)
855
+ label string role name ("Researcher", "Reviewer") — always set on
856
+ prompt × prompt sides
857
+ subagents SubagentRelationship[] side-owned child relationships (see cheat sheet below)
858
+ stopOnResponse boolean (default true) stop side turn on text response without tool calls
859
+ stopTool string stop side turn when this tool is called
860
+ stopToolResponseProperty string extracted property for stop tool outcomes
861
+ maxSteps number per-side step cap within one turn
862
+ sessionStop { name, messageProperty?, attachmentsProperty? } ends session with a result
863
+ sessionFail { name, messageProperty?, attachmentsProperty? } ends session with a failure
864
+ sessionStatus { name, messageProperty?, attachmentsProperty? } publishes status without ending
865
+
866
+ HandlerSide
867
+ handler string the handler backing this side (agents/handlers/<name>)
868
+ label string role name shown in the UI and logs
869
+ subagents SubagentRelationship[] side-owned child relationships
870
+ ```
745
871
 
746
- These are the bindings the [Session boundary discipline](#session-boundary-discipline) section refers to. Every `dual_ai` agent must set them, and every session tool must pass the [three-places check](#session-tools-must-exist-in-three-places).
872
+ Session bindings are **object form only** and are the bindings [Session boundary discipline](#session-boundary-discipline) refers to; every session tool must pass the [three-places check](#session-tools-must-exist-in-three-places). Handler sides get no `maxSteps`, stop bindings, or session bindings — a handler turn is a single invocation, and handlers end sessions via `state.stopSession()`.
747
873
 
748
874
  Packaging fields (`packageName`, `version`, `author`, `license`) exist for the agent packing system; ignore them unless you're publishing.
749
875
 
750
- ### `SubagentToolConfig` cheat sheet
876
+ Definition-time errors to know (old shapes fail to load with migration-pointing errors — there is no compatibility window): both sides human (reserved); `subagents` on a human side; a side with zero or multiple backings; an undefined prompt/handler reference; duplicate, colliding, or reserved relationship names; session bindings, `maxSteps`, or stop bindings on a non-prompt side; a non-object session binding.
751
877
 
752
- This is where parent/child architecture is actually configured — it lives on the *parent prompt's* `tools` array. Knowing every field exists is essential; full signatures live in the spec types (`node_modules/@standardagents/spec/dist/`, or `packages/spec/src/` on GitHub).
878
+ ### `SubagentRelationship` cheat sheet
879
+
880
+ This is where parent/child architecture is actually configured — it lives on the **owning side's `subagents` array** (prompt- or handler-backed; never a human side, never a prompt's `tools`). A relationship belongs to the side that declares it, and the side's declaration is the grant. Knowing every field exists is essential; full signatures live in the spec types (`node_modules/@standardagents/spec/dist/`, or `packages/spec/src/` on GitHub).
753
881
 
754
882
  ```
755
- SubagentToolConfig — entry on a parent prompt's `tools` array
756
- name string dual_ai agent to invoke (must have exposeAsTool: true)
757
- blocking boolean (default true) parent waits for result, vs. fire-and-forget
758
- immediate bool | object spawn child when prompt activates, before any LLM step
883
+ SubagentRelationship — entry in a side's `subagents` array
884
+ agent string agent definition to instantiate — ANY agent is
885
+ eligible; exposeAsTool: true marks it as designed
886
+ for composition and supplies the default description
887
+ as string relationship name — tool-surface name, registry
888
+ identity, reference name; defaults to `agent`.
889
+ Two relationships may target the same agent under
890
+ different `as` names (aliases), each with its own
891
+ config — e.g. work_calendar / personal_calendar
892
+ with different scopedEnv
893
+ description string model-facing override; falls back to the child's
894
+ toolDescription
895
+ maxInstances number cap on simultaneous instances (unset = unbounded);
896
+ at the cap, creation errors and suggests messaging
897
+ an existing instance
898
+ immediate bool | object spawn at THREAD CREATION, before any side takes a
899
+ turn; recursive through the child's own immediates.
759
900
  object form: { nameEnv, descriptionEnv, scopedEnv }
760
- scopedEnv values are copied into the child but NOT
761
- exposed to the bootstrap model unless named in
762
- nameEnv/descriptionEnv
763
- resumable false | object false = tool-call style (default)
764
- object = long-lived addressable child; runtime
765
- exposes built-in subagent_create / subagent_message
766
- receives_messages 'side_a' | 'side_b' which side hears parent messages
767
- side_a = queued as 'user'
768
- side_b = queued as 'assistant'
769
- maxInstances number cap on concurrent instances; when reached the tool
770
- is hidden and new messages route to existing instances
771
- parentCommunication 'implicit' | 'explicit' implicit (default) auto-queues completion to parent
772
- explicit requires state.notifyParent(),
773
- state.setStatus(), or state.stopSession()
774
- in code
775
- initUserMessageProperty schema field name tool arg used as child's first user message
776
- initAttachmentsProperty schema field name tool arg holding /attachments/* paths to copy to child
777
- initAgentNameProperty schema field name tool arg used to tag the child thread (UI label)
778
- optional string (env name) subagent disabled unless this env resolves truthy
779
- parentCommunication 'implicit' | 'explicit' top-level form for NON-resumable subagents; explicit
780
- means completion is not auto-queued to the parent
781
- (so the parent is not woken). Use for background
782
- children that deliver results via parent thread KV.
783
- resumable.parentCommunication takes precedence.
901
+ — scopedEnv stays runtime-only; only nameEnv /
902
+ descriptionEnv may be model-visible at bootstrap
903
+ blocking boolean subagent_create awaits the child session's close and
904
+ returns its result. blocking + maxInstances: 1 with
905
+ an existing instance = create-or-continue
906
+ parentCommunication 'implicit' | 'explicit' implicit (default) auto-queues completion/failure to
907
+ the parent on the owning side; explicit means the
908
+ child's finish does NOT wake the parent — escalation
909
+ is deliberate (notifyParent / setStatus /
910
+ stopSession), results delivered via e.g. parent KV
911
+ scopedEnv string[] env values copied into the child thread; runtime-only
912
+ initUserMessageProperty schema field name creation arg used as child's first user message
913
+ initAttachmentsProperty schema field name creation arg holding /attachments/* paths to copy
914
+ initAgentNameProperty schema field name creation arg used to tag the child thread (UI label)
915
+ optional string (env name) relationship enabled only when this env flag resolves
916
+ truthy (true/1/yes)
784
917
  hidden boolean relationship stays declared and spawnable by runtime
785
- code (invokeTool/queueTool from hooks), but the tool
786
- is excluded from every LLM request — the model can
918
+ code (invokeTool/queueTool from hooks), but is
919
+ excluded from every LLM request — the model can
787
920
  never call it (infrastructure children, e.g. a
788
921
  background compaction agent)
789
922
  ```
790
923
 
791
- For resumable lifecycle creation, `subagent_create.arguments` is shaped by the prompt schema on the receiving child side (`resumable.receives_messages`). The runtime persists those values with the child thread and restores them as `ThreadState.arguments` for both child sides; top-level keys are also prompt variables during render, including included prompts.
924
+ Rules that follow from ownership:
925
+
926
+ - **The owning side is where child traffic lands.** `notifyParent` content, implicit completion/failure messages, and status traffic from the relationship's instances are delivered on the owning side's perspective — queued in the role that prompts the owning side's next turn. Declare the relationship on the side whose turns should react to the child.
927
+ - **A relationship name may appear on at most one side**, must not collide with any tool name, and must not use the reserved lifecycle names `subagents`, `subagent_create`, `subagent_message`. All violations are definition-time errors.
928
+ - **Prompt-backed owning sides get the lifecycle tools auto-injected** for their non-hidden relationships: `subagents` (pull-based registry snapshot — call it before creating so existing instances are reused), `subagent_create` (requires a non-empty instance `name`; its `agent` argument enumerates **relationship names, not agent names**), and `subagent_message` (queue a message to an existing instance by reference).
929
+ - **Handler-backed owning sides need no tool grant** — the declaration establishes existence and lifecycle, and the handler reaches children through `ThreadState` directly.
930
+
931
+ `subagent_create` arguments are shaped by the `requiredSchema` of the receiving child's **side A prompt** (parent traffic enters the child in the side B perspective, `role: 'user'`, so side A acts on it). The runtime validates and persists those values for the life of the child thread, hydrates them into `ThreadState.arguments` and prompt variables for both child sides on every activation, and rejects creation with a structured error when required arguments are missing.
792
932
 
793
- A second tool-config form exists for plain callables (`{ name, env, options }`), and a third for subprompts (`{ name, includeTextResponse, includeToolCalls, includeErrors, initUserMessageProperty, initAttachmentsProperty }`). Both are documented in full at `agents/prompts/AGENTS.md`.
933
+ Tool-config forms still exist in prompt `tools` for plain callables (`{ name, env, options }`) and subprompts (`{ name, includeTextResponse, includeToolCalls, includeErrors, initUserMessageProperty, initAttachmentsProperty }`). Both are documented in full at `agents/prompts/AGENTS.md`.
794
934
 
795
935
  ### Inter-agent communication rules
796
936
 
797
937
  These are correct as written in the spec — internalize them.
798
938
 
799
939
  1. **Parents always create children.**
800
- - Explicitly via the built-in `subagent_create` tool, which requires a non-empty `name` for the child instance.
801
- - Implicitly via `immediate: { ... }` — the child spawns the moment the parent prompt activates, before any LLM step.
802
- 2. **Children only communicate back to their parents.** Two flavors:
803
- - **Implicit**: the child auto-queues a message to the parent when the session ends (via `sessionStop`, `sessionFail`, or `maxSessionTurns`). Default for all subagents.
804
- - **Explicit**: only when code calls `state.notifyParent()`, `state.setStatus()`, or `state.stopSession({ result, notifyParent, status })`. Set with `resumable.parentCommunication: 'explicit'` (resumable) or top-level `parentCommunication: 'explicit'` (non-resumable). Use for high-traffic resumables (e.g., a Slack monitor) where you want control over when the parent is interrupted, or for non-resumable background workers (e.g., a compaction agent) whose completion should not wake the parent because results are delivered via the parent's thread KV.
805
- 3. **Resumable subagents can receive messages from their parents.** `resumable.receives_messages` chooses which side hears them.
806
- 4. **When a child sends back to the parent, it MUST indicate whether it needs a response** — in plain English, e.g. "After researching, you must respond so I can continue with the email." A research subagent providing FYI info doesn't need a response; a gmail subagent waiting on guidance does.
807
- 5. **File attachments use path strings.** `/attachments/{filename}.{ext}`. Use this convention everywhere — the runtime copies attachments across thread filesystems automatically when listed in `initAttachmentsProperty`.
808
- 6. **Resumable agents communicate via "silent" user messages** that carry the source instance UUID. The receiving agent decides whether to respond or just absorb the information.
809
- 7. **Messages to busy agents are queued** and delivered when the agent is free.
810
- 8. **Prompt language must distinguish `subagent_create` from `subagent_message`.** When writing prompts that include resumable subagents, explicitly tell the model: *"To research a new topic, use `subagent_create`. To follow up on a topic an existing instance is already researching, use `subagent_message`."* Without this guidance, models will pick wrong.
940
+ - Explicitly via the built-in `subagent_create` tool, which requires a non-empty `name` for the child instance and whose `agent` argument enumerates **relationship names, not agent names**.
941
+ - From runtime code (`invokeTool` / `queueTool` on the lifecycle surface) in tools, hooks, and handlers.
942
+ - Implicitly via `immediate` — the child spawns at **thread creation**, before any side takes a turn (recursive through the child's own immediate relationships; works even when the owning side is a handler with no prompt to activate).
943
+ 2. **Routing is structural in both directions.** Parent→child traffic enters the child in the **side B perspective** (`role: 'user'`), so the child's side A acts on it exactly as for an external caller. Child→parent delivery — `notifyParent` content, implicit completion/failure, status traffic — lands on the **relationship's owning side's** perspective, queued in the role that prompts that side's next turn. The runtime routes inbound; userland decides outbound.
944
+ 3. **Child completion has two flavors**, chosen by `parentCommunication`:
945
+ - **Implicit** (default): the runtime auto-queues a silent completion/failure message to the parent when the child session ends (via `sessionStop`, `sessionFail`, or a turn cap), which triggers a parent turn.
946
+ - **Explicit**: the child's finish does **not** wake the parent at all. Escalation is deliberate — `state.notifyParent()`, `state.setStatus()`, or `state.stopSession({ result, notifyParent, status })` — and results are delivered through another channel (e.g., the parent's thread KV), which the parent picks up on its next natural turn. Use for high-traffic children (a Slack monitor) where you control when the parent is interrupted, or background workers (a compaction agent) whose completion should not wake the parent.
947
+ 4. **Every child stays messageable.** Persistence is universal: session end never terminates the thread, so `subagent_message` (or `queueMessage` from code) can always continue an existing instance with history intact — until explicit `terminate()`.
948
+ 5. **When a child sends back to the parent, it MUST indicate whether it needs a response** — in plain English, e.g. "After researching, you must respond so I can continue with the email." A research subagent providing FYI info doesn't need a response; a gmail subagent waiting on guidance does.
949
+ 6. **File attachments use path strings.** `/attachments/{filename}.{ext}`. Use this convention everywhere — the runtime copies attachments across thread filesystems automatically when listed in `initAttachmentsProperty`, and rewrites them to destination-local paths.
950
+ 7. **Cross-thread traffic arrives as "silent" messages with provenance metadata.** The runtime sets `metadata.subagent_id` (which child instance a message relates to) and `metadata.forwarded_from` (origin of forwarded content) — use these conventions instead of ad-hoc bracket-tag prefixes in relay code. The receiving side decides whether to respond or just absorb the information.
951
+ 8. **Messages to busy agents are queued** and delivered in order when the agent is free.
952
+ 9. **Prompt language must distinguish the lifecycle tools.** When writing prompts for sides that own relationships, explicitly tell the model: *"Call `subagents` to see existing instances. To research a new topic, use `subagent_create`. To follow up on a topic an existing instance is already researching, use `subagent_message`."* Without this guidance, models will pick wrong and duplicate children.
811
953
 
812
954
  ---
813
955
 
@@ -936,7 +1078,7 @@ All three are gated by the `logs` permission on hosted deployments.
936
1078
 
937
1079
  ## Implementation checking
938
1080
 
939
- When you edit `agents/`, `prompts/`, `models/`, `tools/`, `hooks/`, or `worker/`, validate before claiming the change is done.
1081
+ When you edit `agents/`, `prompts/`, `handlers/`, `models/`, `tools/`, `hooks/`, or `worker/`, validate before claiming the change is done.
940
1082
 
941
1083
  Validation order:
942
1084