@standardagents/agentbuilder-template 0.40.12 → 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/dist/index.js CHANGED
@@ -18,7 +18,7 @@ Model \u2192 Prompt \u2192 Agent \u2192 Thread
18
18
 
19
19
  **Models** define which LLM to use (provider, model ID, pricing, fallbacks).
20
20
  **Prompts** define what the LLM should do (system instructions, available tools).
21
- **Agents** orchestrate conversations (which prompt, stop conditions, turn limits).
21
+ **Agents** orchestrate conversations. An agent has exactly two sides; each side is backed by a human (\`{ human: true }\`), a prompt (\`{ prompt: 'name' }\`), or a handler (\`{ handler: 'name' }\` \u2014 deterministic code from \`agents/handlers/\`).
22
22
  **Threads** are individual conversation instances with persistent storage.
23
23
 
24
24
  ## Packability and Runtime Portability
@@ -96,6 +96,9 @@ agents/
96
96
  \u251C\u2500\u2500 prompts/ # Prompt definitions (definePrompt)
97
97
  \u2502 \u251C\u2500\u2500 AGENTS.md
98
98
  \u2502 \u2514\u2500\u2500 CLAUDE.md -> AGENTS.md
99
+ \u251C\u2500\u2500 handlers/ # Handler definitions (defineHandler)
100
+ \u2502 \u251C\u2500\u2500 AGENTS.md
101
+ \u2502 \u2514\u2500\u2500 CLAUDE.md -> AGENTS.md
99
102
  \u251C\u2500\u2500 models/ # Model configurations (defineModel)
100
103
  \u2502 \u251C\u2500\u2500 AGENTS.md
101
104
  \u2502 \u2514\u2500\u2500 CLAUDE.md -> AGENTS.md
@@ -137,7 +140,7 @@ interface ThreadState {
137
140
  agentId: string; // Agent identifier
138
141
  userId: string | null; // User identifier (if set)
139
142
  createdAt: number; // Creation timestamp
140
- children: SubagentRegistryEntry[]; // Resumable child registry
143
+ children: SubagentRegistryEntry[]; // Child instance registry
141
144
  terminated: number | null; // Soft-termination timestamp
142
145
  arguments: Record<string, unknown>; // Invocation or subagent creation args
143
146
  context: Record<string, unknown>; // Current-execution scratch data
@@ -182,16 +185,16 @@ reconnecting.
182
185
 
183
186
  ## Subagents
184
187
 
185
- \`dual_ai\` agents can be used as subagents from prompt tool configs:
188
+ A subagent is any agent composed as a child of another agent. Relationships are declared in the \`subagents\` array of a prompt- or handler-backed agent side (never inside a prompt's \`tools\` array):
186
189
 
187
190
  - Each subagent runs in its own thread (\`DurableThread\`)
188
- - Parent/child linkage is tracked in thread metadata
189
- - Resumable children are visible in \`state.children\`
190
- - Parent threads create/message resumable children via built-ins (\`subagent_create\`, \`subagent_message\`)
191
- - \`subagent_create.arguments\` is validated from the receiving child side's prompt schema and persists as \`ThreadState.arguments\` plus prompt variables for the life of the child thread
191
+ - Parent/child linkage is tracked in thread metadata; instances are visible in \`state.children\`
192
+ - The relationship's owning side is where child traffic lands; parent\u2192child traffic enters in the child's side B perspective (\`role: 'user'\`), so the child's side A acts on it
193
+ - For a prompt-backed owning side, the runtime injects the lifecycle tools \`subagents\`, \`subagent_create\`, and \`subagent_message\`; handler-backed sides reach children through \`ThreadState\` directly
194
+ - \`subagent_create.arguments\` is validated from the receiving child's side A prompt schema and persists as \`ThreadState.arguments\` plus prompt variables for the life of the child thread
192
195
  - Child completion results are queued back to the parent when \`parentCommunication\` is \`implicit\`
193
196
  - Explicit relationships can stay idle until code calls \`state.notifyParent()\` or \`state.setStatus()\`; use \`state.stopSession({ result, notifyParent, status })\` when a tool should also terminally end the child session
194
- - A resumable child's session ending never terminates its thread. The same reference remains addressable for later \`subagent_message\` calls; only explicit \`state.terminate()\` tears the thread down.
197
+ - Persistence is universal: a child's session ending never terminates its thread. The same reference remains addressable for later \`subagent_message\` calls; only explicit \`state.terminate()\` tears the thread down.
195
198
 
196
199
  Access in tools:
197
200
  \`\`\`typescript
@@ -224,6 +227,7 @@ export default defineHook({
224
227
  | \`defineModel()\` | Configure LLM provider | \`agents/models/\` |
225
228
  | \`defineProvider()\` | Custom provider endpoints and overrides | \`agents/providers/\` |
226
229
  | \`definePrompt()\` | System instructions + tools | \`agents/prompts/\` |
230
+ | \`defineHandler()\` | Deterministic side turns | \`agents/handlers/\` |
227
231
  | \`defineAgent()\` | Conversation orchestration | \`agents/agents/\` |
228
232
  | \`defineTool()\` | Custom tool functions | \`agents/tools/\` |
229
233
  | \`defineHook()\` | Lifecycle interception | \`agents/hooks/\` |
@@ -357,7 +361,7 @@ other provider behavior.
357
361
 
358
362
  \`\`\`typescript
359
363
  // agents/providers/darkbloom.ts
360
- import { defineProvider, providerEnv } from '@standardagents/builder';
364
+ import { defineProvider, providerEnv } from '@standardagents/spec';
361
365
  import { openai } from '@standardagents/openai';
362
366
 
363
367
  export default defineProvider({
@@ -441,7 +445,7 @@ package with connection config and optional method overrides.
441
445
  Use \`defineProvider()\` and export the result as the default export:
442
446
 
443
447
  \`\`\`typescript
444
- import { defineProvider, providerEnv, providerValue } from '@standardagents/builder';
448
+ import { defineProvider, providerEnv, providerValue } from '@standardagents/spec';
445
449
  import { openai } from '@standardagents/openai';
446
450
 
447
451
  export default defineProvider({
@@ -516,7 +520,7 @@ Prompts define system instructions, model selection, and available tools for LLM
516
520
  | \`toolDescription\` | \`string\` | Yes | Description shown when used as a tool |
517
521
  | \`prompt\` | \`string \\| StructuredPrompt\` | Yes | System instructions |
518
522
  | \`model\` | \`string\` | Yes | Model name to use (references \`agents/models/\`) |
519
- | \`tools\` | \`(string \\| ToolConfig \\| SubagentToolConfig)[]\` | No | Available tools (include ai_human agent names for handoffs and dual_ai subagent configs) |
523
+ | \`tools\` | \`(string \\| ToolConfig)[]\` | No | Available tools: tool names, sub-prompt names, provider tools, and exposed agent names for handoffs. Subagent relationships never appear here \u2014 they are declared on the agent side's \`subagents\` array |
520
524
  | \`includeChat\` | \`boolean\` | No | Include full conversation history |
521
525
  | \`includePastTools\` | \`boolean\` | No | Include previous tool results |
522
526
  | \`parallelToolCalls\` | \`boolean\` | No | Allow multiple concurrent tool calls |
@@ -595,60 +599,40 @@ provider-reported calls in request logs; it does not execute those tools locally
595
599
  Generated prompt files may store these selections as \`provider:<toolName>\` to
596
600
  distinguish provider tools from local tools with the same name.
597
601
 
598
- ### Subagent Tool Configuration
602
+ ### Subagents Come From the Side, Not the Tools Array
599
603
 
600
- Use \`SubagentToolConfig\` when the tool is a \`dual_ai\` agent:
604
+ Composition into a child thread is not a prompt-tools concern. A subagent relationship is part of the agent's structure: it is declared in the \`subagents\` array of the agent side this prompt backs, never inside the prompt's \`tools\` array (a \`tools\` entry attempting to configure a subagent relationship is a definition-time error). The side's declaration is the grant:
601
605
 
602
606
  \`\`\`typescript
603
- tools: [
604
- // Blocking, non-resumable (default)
605
- { name: 'data_processor_agent' },
607
+ // agents/agents/assistant_agent.ts
608
+ sideA: {
609
+ prompt: 'coordinator',
610
+ subagents: [
611
+ { agent: 'calendar_agent', as: 'work_calendar', scopedEnv: ['WORK_CAL_ID'] },
612
+ { agent: 'calendar_agent', as: 'personal_calendar', scopedEnv: ['PERSONAL_CAL_ID'] },
613
+ { agent: 'research_agent', blocking: true, maxInstances: 1 },
614
+ { agent: 'compaction_agent', hidden: true },
615
+ { agent: 'browser_agent', optional: 'ENABLE_BROWSER_AGENT' },
616
+ ],
617
+ },
618
+ \`\`\`
606
619
 
607
- // The same agent exposed under a task-specific tool name: \`name\` is what
608
- // the model calls; \`agent\` is the dual_ai agent that implements it.
609
- { name: 'summarize_report', agent: 'data_processor_agent', blocking: true },
620
+ For a prompt-backed side with non-hidden relationships, the runtime automatically injects three lifecycle tools scoped to that side's declarations:
621
+
622
+ - \`subagents\` \u2014 pull-based visibility: returns the registry snapshot (reference, relationship name, agent name, status, initial arguments, creation time). Call it before \`subagent_create\` so existing instances are reused rather than duplicated.
623
+ - \`subagent_create\` \u2014 creates (or continues) an instance. Its \`agent\` argument enumerates the side's enabled relationship *names* (\`as\`, defaulting to \`agent\`), never internal agent identifiers. It requires a non-empty \`name\` for the spawned instance; AgentBuilder stores that as a \`name:<value>\` thread tag for human-friendly labels.
624
+ - \`subagent_message\` \u2014 queues a message to an existing instance by reference.
625
+
626
+ These three names are reserved; a relationship \`as\` that collides with them (or with any other tool name) is a definition-time error.
627
+
628
+ \`subagent_create.arguments\` is 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 exactly as for an external caller). AgentBuilder validates and persists those arguments for the life of the child thread, exposes them as \`ThreadState.arguments\` and prompt variables to **both** child sides, and rehydrates them on every later activation. Missing required arguments fail creation with a structured \`subagent_arguments_required\` error.
629
+
630
+ Persistence is universal: every child instance is a persistent thread. Session end never terminates a child \u2014 a later \`subagent_message\` starts a new session with history intact; only explicit \`state.terminate()\` destroys an instance. \`blocking: true\` makes \`subagent_create\` await the child session's close and return its result; combined with \`maxInstances: 1\` and an existing instance it behaves as create-or-continue.
610
631
 
611
- // Non-blocking, resumable with a single instance
612
- {
613
- name: 'browser_agent',
614
- blocking: false,
615
- immediate: {
616
- nameEnv: 'BROWSER_AGENT_NAME',
617
- descriptionEnv: 'BROWSER_AGENT_DESCRIPTION',
618
- scopedEnv: ['BROWSER_AGENT_API_KEY'],
619
- },
620
- optional: 'ENABLE_BROWSER_AGENT',
621
- initAgentNameProperty: 'name',
622
- resumable: {
623
- receives_messages: 'side_a',
624
- maxInstances: 1,
625
- parentCommunication: 'explicit',
626
- },
627
- },
628
- ]
629
- \`\`\`
630
-
631
- When prompt-configured resumable dual_ai subagents exist, the runtime auto-injects:
632
- - \`subagent_create\` for spawning new child instances
633
- - \`subagent_message\` for messaging active resumable instances
634
-
635
- \`subagent_create\` requires a non-empty \`name\` for human-friendly child thread labels.
636
- AgentBuilder stores that argument as a \`name:<value>\` thread tag.
637
- \`subagent_create.arguments\` is shaped by the prompt schema on the child side selected by \`resumable.receives_messages\`.
638
- AgentBuilder persists those initial arguments with the child thread, exposes them as \`ThreadState.arguments\` and prompt variables, and keeps them visible to both child sides for later resumptions.
639
-
640
- Set \`immediate: true\` to run a tool automatically when the prompt activates.
641
- Use \`immediate: { nameEnv, descriptionEnv, scopedEnv }\` when you want safe per-instance bootstrap hints while keeping copied scoped env runtime-only.
642
- Set \`optional: 'ENV_NAME'\` to enable a subagent branch only when env resolves to \`true\`, \`1\`, or \`yes\`.
643
- Set \`resumable.parentCommunication: 'explicit'\` when the child should stay quiet until its own tools or hooks call \`state.notifyParent()\`, \`state.setStatus()\`, or \`state.stopSession({ result, notifyParent, status })\`.
644
- Set top-level \`parentCommunication: 'explicit'\` on a NON-resumable subagent when its completion should not be auto-queued to the parent (so it never wakes the parent) \u2014 for background children that deliver results through another channel, e.g. the parent's thread KV. \`resumable.parentCommunication\` takes precedence when both are set.
645
- Set \`hidden: true\` to keep a subagent relationship declared and spawnable by runtime code (\`state.invokeTool()\`/\`queueTool()\` from hooks) while excluding its tool from every LLM request \u2014 the model can never call it (for infrastructure children like a background compaction agent).
646
- Adding an enabled child relationship automatically activates the child's
647
- disclosed \`parentHooks\` on this parent. Disabled optional relationships do
648
- not; hidden enabled relationships do. \`name\` is the relationship provenance
649
- and \`agent ?? name\` identifies the implementing child.
650
632
  If required scoped vars are missing, the runtime returns \`error_code: "subagent_env_required"\` with a temporary \`GET/POST /api/threads/:threadId/variables/:requestId\` endpoint in \`error_data.endpoint\`.
651
633
 
634
+ See \`agents/agents/AGENTS.md\` for the full \`SubagentRelationship\` option list (\`immediate\`, \`optional\`, \`hidden\`, \`parentCommunication\`, init mappings) and the ownership rules.
635
+
652
636
  ## Input Validation
653
637
 
654
638
  Validate inputs when prompt is called as a tool:
@@ -726,135 +710,207 @@ Full reference: https://docs.standardagentbuilder.com/api-reference/define/promp
726
710
  // src/docs/agents.ts
727
711
  var AGENT_DEFINITIONS_AGENTS_MD = `# Agent Definitions
728
712
 
729
- Agents orchestrate conversations by defining prompts, stop conditions, and turn limits.
713
+ An agent is the top-level conversation orchestrator. 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" \u2014 everything about how an agent converses, composes, and terminates is derived from the backings of its two sides.
714
+
715
+ | Backing | Who takes this side's turns | Declared as |
716
+ |---------|-----------------------------|-------------|
717
+ | **Human** | A person, via whatever interface the runtime provides | \`{ human: true }\` |
718
+ | **Prompt** | A model, driven by a prompt from \`agents/prompts/\` | \`{ prompt: 'name', ... }\` |
719
+ | **Handler** | Deterministic code from \`agents/handlers/\` | \`{ handler: 'name', ... }\` |
720
+
721
+ Both sides are explicit. There is no implicit "missing side B is a human" default \u2014 if a person takes side B's turns, write \`sideB: { human: true }\`.
730
722
 
731
723
  ## Agent Properties
732
724
 
733
725
  | Property | Type | Required | Description |
734
726
  |----------|------|----------|-------------|
735
727
  | \`name\` | \`string\` | Yes | Unique identifier for this agent |
736
- | \`type\` | \`'ai_human' \\| 'dual_ai'\` | No | Conversation type (default: \`ai_human\`) |
737
- | \`sideA\` | \`SideConfig\` | Yes | Configuration for side A (AI in ai_human) |
738
- | \`sideB\` | \`SideConfig\` | No | Configuration for side B (required for dual_ai) |
739
- | \`maxSessionTurns\` | \`number\` | No | Max total turns across both sides |
740
- | \`exposeAsTool\` | \`boolean\` | No | Allow other prompts to hand off to this agent |
728
+ | \`sideA\` | \`SideConfig\` | Yes | Side A configuration (backing-discriminated) |
729
+ | \`sideB\` | \`SideConfig\` | Yes | Side B configuration (backing-discriminated) |
730
+ | \`maxSessionTurns\` | \`number\` | No | Session exchange safety cap \u2014 optional on every agent shape |
731
+ | \`title\` | \`string\` | No | Human-readable label |
732
+ | \`description\` | \`string\` | No | Agent summary |
733
+ | \`icon\` | \`string\` | No | Display icon (see Icons below) |
734
+ | \`exposeAsTool\` | \`boolean\` | No | Expose a callable entrypoint for handoffs and composition |
741
735
  | \`primary\` | \`boolean\` | No | Default true. Set false on internal handoff-target agents (e.g. domain-gearbox variants) to hide them from top-level UI agent lists; they stay fully addressable for handoffs and thread resumption |
742
- | \`toolDescription\` | \`string\` | No | Description when used as tool (required if exposeAsTool) |
736
+ | \`toolDescription\` | \`string\` | No | Default model-facing description when this agent is composed or handed off to (required if exposeAsTool) |
743
737
  | \`env\` | \`Record<string, string>\` | No | Agent-level default variable values |
744
738
  | \`hooks\` | \`string[]\` | No | Hook IDs to run for this agent (fallback if prompt has no hooks) |
745
- | \`parentHooks\` | \`ParentHookRequirement[]\` | No | Hooks an exposed dual-AI child automatically contributes to each direct parent |
739
+ | \`parentHooks\` | \`ParentHookRequirement[]\` | No | Required hooks automatically contributed to a direct parent composition (legal on any exposed agent) |
746
740
  | \`tags\` | \`string[]\` | No | Tags for categorization and filtering |
747
741
 
748
742
  ## Side Configuration
749
743
 
744
+ A side's configuration is discriminated by its backing: exactly one of \`human\`, \`prompt\`, or \`handler\` must be present.
745
+
746
+ **Human side** \u2014 \`{ human: true, label? }\`. No other properties are legal on a human side. In particular, \`subagents\` on a human side is a definition-time error (nothing there can invoke them), and stop/session bindings are meaningless.
747
+
748
+ **Prompt side**
749
+
750
750
  | Property | Type | Required | Description |
751
751
  |----------|------|----------|-------------|
752
- | \`label\` | \`string\` | No | Display name for this side |
753
752
  | \`prompt\` | \`string\` | Yes | Prompt name to use (references \`agents/prompts/\`) |
754
- | \`stopOnResponse\` | \`boolean\` | No | Stop turn when LLM returns text (default: true) |
753
+ | \`label\` | \`string\` | No | Display name for this side |
754
+ | \`subagents\` | \`SubagentRelationship[]\` | No | Side-owned child relationships (see Subagent Relationships below) |
755
+ | \`stopOnResponse\` | \`boolean\` | No | Stop side turn on text response without tool calls (default: true) |
755
756
  | \`stopTool\` | \`string\` | No | Tool that stops this side's turn |
756
757
  | \`stopToolResponseProperty\` | \`string\` | No | Property to extract from stop tool result |
757
758
  | \`maxSteps\` | \`number\` | No | Maximum step budget for this side |
758
- | \`sessionStop\` | \`string \\| SessionToolBinding\` | No | Terminal success lifecycle binding |
759
- | \`sessionFail\` | \`string \\| SessionToolBinding\` | No | Terminal failure lifecycle binding |
760
- | \`sessionStatus\` | \`string \\| SessionToolBinding\` | No | Non-terminal status lifecycle binding |
761
- | \`manualStopCondition\` | \`boolean\` | No | Enable custom stop handling via hooks |
759
+ | \`sessionStop\` | \`SessionToolBinding\` | No | Terminal success lifecycle binding (object form only) |
760
+ | \`sessionFail\` | \`SessionToolBinding\` | No | Terminal failure lifecycle binding (object form only) |
761
+ | \`sessionStatus\` | \`SessionToolBinding\` | No | Non-terminal status lifecycle binding (object form only) |
762
+
763
+ **Handler side**
764
+
765
+ | Property | Type | Required | Description |
766
+ |----------|------|----------|-------------|
767
+ | \`handler\` | \`string\` | Yes | Handler name for this side (references \`agents/handlers/\`) |
768
+ | \`label\` | \`string\` | No | Display name for this side |
769
+ | \`subagents\` | \`SubagentRelationship[]\` | No | Side-owned child relationships |
770
+
771
+ Handler sides have no \`maxSteps\`, \`stopOnResponse\`, or session bindings: a handler turn is a single invocation, and handlers end sessions through \`state.stopSession()\` rather than declarative bindings. See \`agents/handlers/AGENTS.md\`.
762
772
 
763
- ## Basic Example (AI-Human)
773
+ Session lifecycle bindings use the object form only:
774
+
775
+ \`\`\`typescript
776
+ sessionStop: {
777
+ name: 'complete_work', // tool that controls the lifecycle action
778
+ messageProperty: 'summary', // arg field that becomes lifecycle message text
779
+ attachmentsProperty: 'files', // arg field containing attachment path(s)
780
+ }
781
+ \`\`\`
782
+
783
+ ## Derivations
784
+
785
+ These follow from the backings and are never declared separately:
786
+
787
+ - **Chat surface** \u2014 the primary conversational interface renders wherever a \`human\` backing is.
788
+ - **Perspective** \u2014 \`role: 'user'\` is the side B perspective, \`role: 'assistant'\` is the side A perspective, for queueing, injection, and rendering alike.
789
+ - **Subagent eligibility** \u2014 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 parks \`awaiting_human\` until that person responds).
790
+ - **Termination discipline** \u2014 \`maxSessionTurns\` is optional on every agent shape, with uniform exchange counting across backings (a handler turn counts like a prompt turn). Two prompt-backed sides can ping-pong indefinitely \u2014 the cap is a voluntary safety valve, and prompt and handler authors own their termination discipline.
791
+
792
+ ## Example: Assistant (human \xD7 prompt)
793
+
794
+ The everyday user-facing assistant \u2014 a person on side B, a model on side A:
764
795
 
765
796
  \`\`\`typescript
766
797
  import { defineAgent } from '@standardagents/builder';
767
798
 
768
799
  export default defineAgent({
769
800
  name: 'support_agent',
770
- type: 'ai_human',
771
801
  sideA: {
772
802
  label: 'Support',
773
803
  prompt: 'customer_support',
774
804
  stopOnResponse: true,
775
- sessionStop: 'close_ticket',
805
+ sessionStop: { name: 'close_ticket' },
776
806
  maxSteps: 50,
777
807
  },
808
+ sideB: { human: true },
778
809
  tags: ['support', 'tier-1'],
779
810
  });
780
811
  \`\`\`
781
812
 
782
- ## Dual-AI Example
813
+ ## Example: Worker / Reviewer (prompt \xD7 prompt)
783
814
 
784
- Two AI agents conversing (e.g., debate, iterative refinement):
815
+ Two models converse; each side sees itself as \`assistant\` and the other as \`user\`. \`maxSessionTurns\` is optional but strongly recommended here \u2014 nothing external stops two models:
785
816
 
786
817
  \`\`\`typescript
787
818
  export default defineAgent({
788
- name: 'code_review_debate',
789
- type: 'dual_ai',
819
+ name: 'research_agent',
790
820
  maxSessionTurns: 20,
821
+ exposeAsTool: true,
822
+ toolDescription: 'Researches and validates findings autonomously',
791
823
  sideA: {
792
- label: 'Reviewer',
793
- prompt: 'code_reviewer',
824
+ label: 'Worker',
825
+ prompt: 'research_worker',
794
826
  stopOnResponse: true,
827
+ sessionStatus: { name: 'set_research_status', messageProperty: 'status' },
828
+ sessionFail: { name: 'fail_research', messageProperty: 'reason' },
795
829
  },
796
830
  sideB: {
797
- label: 'Developer',
798
- prompt: 'code_defender',
831
+ label: 'Reviewer',
832
+ prompt: 'research_reviewer',
799
833
  stopOnResponse: true,
834
+ sessionStop: { name: 'approve_research', messageProperty: 'summary' },
800
835
  },
801
836
  });
802
837
  \`\`\`
803
838
 
804
- ## Dual-AI Subagent Pattern
839
+ ## Example: Relay (handler \xD7 human)
805
840
 
806
- When a \`dual_ai\` agent is used as a subagent tool, a common split is:
807
- - **sideA**: performs work (tools, data gathering, execution)
808
- - **sideB**: reflection, verification, and quality checks
841
+ 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:
809
842
 
810
843
  \`\`\`typescript
811
844
  export default defineAgent({
812
- name: 'research_subagent',
813
- type: 'dual_ai',
814
- exposeAsTool: true,
815
- toolDescription: 'Researches and validates findings autonomously',
816
- maxSessionTurns: 16,
845
+ name: 'relay_agent',
817
846
  sideA: {
818
- label: 'Worker',
819
- prompt: 'research_worker',
820
- sessionStatus: 'set_subagent_status',
821
- sessionFail: 'fail_research_subagent',
822
- sessionStop: 'finish_subagent',
823
- },
824
- sideB: {
825
- label: 'Reviewer',
826
- prompt: 'research_reviewer',
827
- stopOnResponse: true,
847
+ handler: 'relay_pipe',
848
+ subagents: [{ agent: 'assistant_agent', maxInstances: 1 }],
828
849
  },
850
+ sideB: { human: true },
829
851
  });
830
852
  \`\`\`
831
853
 
854
+ The \`relay_pipe\` handler lives in \`agents/handlers/relay_pipe.ts\` \u2014 the full worked example is in \`agents/handlers/AGENTS.md\`.
855
+
856
+ ## Subagent Relationships
857
+
858
+ Composition into a child thread is declared on the side that owns it \u2014 never inside a prompt's \`tools\` array. The side's declaration is the grant:
859
+
860
+ \`\`\`typescript
861
+ sideA: {
862
+ prompt: 'coordinator',
863
+ subagents: [
864
+ { agent: 'calendar_agent', as: 'work_calendar', scopedEnv: ['WORK_CAL_ID'] },
865
+ { agent: 'calendar_agent', as: 'personal_calendar', scopedEnv: ['PERSONAL_CAL_ID'] },
866
+ { agent: 'research_agent', blocking: true, maxInstances: 1 },
867
+ ],
868
+ },
869
+ \`\`\`
870
+
871
+ - The owning side is where child traffic lands: completion, failure, and status messages arrive on the owning side's perspective. Parent\u2192child traffic enters in the child's side B perspective (\`role: 'user'\`), so the child's side A acts on it.
872
+ - A relationship name (\`as\`, defaulting to \`agent\`) may appear on at most one side, must not collide with another tool name, and must not collide with the reserved lifecycle tool names \`subagents\`, \`subagent_create\`, \`subagent_message\`.
873
+ - For a prompt-backed owning side, the runtime injects those lifecycle tools automatically. Handler-backed owning sides need no tool grant \u2014 handlers reach children through \`ThreadState\` directly.
874
+ - **Persistence is universal**: every child instance is a persistent thread. Session end never terminates a thread; only explicit \`state.terminate()\` destroys an instance. \`blocking\` is purely a waiting style.
875
+
876
+ See \`agents/prompts/AGENTS.md\` for the lifecycle tools from the prompt's point of view, and the relationship option list (\`immediate\`, \`optional\`, \`hidden\`, \`parentCommunication\`, init mappings).
877
+
832
878
  ## Agent Handoffs
833
879
 
834
- Expose an agent for handoffs from other prompts:
880
+ 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.
835
881
 
836
882
  \`\`\`typescript
837
883
  export default defineAgent({
838
884
  name: 'escalation_agent',
839
- type: 'ai_human',
840
885
  exposeAsTool: true,
841
886
  toolDescription: 'Escalate to senior support for complex issues',
842
- sideA: {
843
- prompt: 'senior_support',
844
- },
887
+ sideA: { prompt: 'senior_support' },
888
+ sideB: { human: true },
845
889
  });
846
890
  \`\`\`
847
891
 
848
- Other prompts can then include the agent name in their \`tools\` array to enable handoffs.
892
+ ## Stop Semantics
893
+
894
+ Runtime stop evaluation order for prompt-backed sides:
895
+
896
+ 1. **sessionStop / sessionFail** \u2014 end the session (success / terminal failure)
897
+ 2. **stopTool** \u2014 ends the current side's turn
898
+ 3. **stopOnResponse** \u2014 ends the turn when the LLM returns text without tool calls
899
+ 4. **Safety limits** \u2014 \`maxSteps\` (per side), \`maxSessionTurns\` (per session, hard limit: 250)
849
900
 
850
- ## Stop Conditions (Priority Order)
901
+ A handler-backed side's turn ends when its handler invocation returns. A human-backed side's turn ends when the human's inbound message is persisted.
851
902
 
852
- 1. **sessionFail** - Ends entire conversation with a terminal failure
853
- 2. **sessionStop** - Ends entire conversation successfully
854
- 3. **stopTool** - Ends current side's turn
855
- 4. **maxSteps** - Ends side execution when the safety budget is reached
856
- 5. **stopOnResponse** - Ends turn when LLM returns text
857
- 6. **maxSessionTurns** - Ends conversation (hard limit: 250)
903
+ ## Definition-Time Errors
904
+
905
+ These shapes are rejected when definitions load:
906
+
907
+ 1. Both sides human (reserved \u2014 may lift in a later version)
908
+ 2. \`subagents\` declared on a human-backed side
909
+ 3. A side with zero backings, or more than one of \`human\` / \`prompt\` / \`handler\`
910
+ 4. A handler side referencing an undefined handler; a prompt side referencing an undefined prompt
911
+ 5. A relationship name (\`as\`) appearing on both sides, colliding with another tool name, or colliding with a reserved lifecycle tool name
912
+ 6. Session bindings, \`maxSteps\`, or stop bindings on a non-prompt side
913
+ 7. A session binding that is not in object form
858
914
 
859
915
  ## Hooks
860
916
 
@@ -863,17 +919,15 @@ Attach lifecycle hooks at the agent level:
863
919
  \`\`\`typescript
864
920
  export default defineAgent({
865
921
  name: 'support_agent',
866
- type: 'ai_human',
867
922
  hooks: ['log_all_messages', 'sanitize_results'],
868
- sideA: {
869
- prompt: 'customer_support',
870
- },
923
+ sideA: { prompt: 'customer_support' },
924
+ sideB: { human: true },
871
925
  });
872
926
  \`\`\`
873
927
 
874
928
  Agent hooks run as a fallback when the active prompt has no hooks defined. Prompt hooks always take precedence. See \`agents/hooks/AGENTS.md\` for details.
875
929
 
876
- An exposed \`dual_ai\` child can declare required parent integration hooks:
930
+ Any exposed agent can declare required parent integration hooks:
877
931
 
878
932
  \`\`\`typescript
879
933
  parentHooks: [{
@@ -883,12 +937,13 @@ parentHooks: [{
883
937
  }],
884
938
  \`\`\`
885
939
 
886
- Installing the package is inert. Adding this child to a prompt activates the
887
- hook automatically on that direct parent and composition UIs can show the
888
- \`reason\` first. Disabled optional relationships contribute nothing; enabled
889
- hidden relationships still contribute hooks. Required hooks run with the
890
- parent's \`ThreadState\`, load from the child's package namespace, and fail
891
- composition closed if missing. User-scope hooks cannot be listed.
940
+ Installing the package is inert. Declaring a relationship targeting this agent
941
+ (or adding it to a prompt as a handoff) activates the hook automatically on
942
+ that direct parent, and composition UIs disclose the \`reason\` first. Disabled
943
+ optional relationships contribute nothing; enabled hidden relationships still
944
+ contribute hooks. Required hooks run with the parent's \`ThreadState\`, load
945
+ from the child's package namespace, and fail composition closed if missing.
946
+ User-scope hooks cannot be listed.
892
947
 
893
948
  ## Naming Convention
894
949
 
@@ -908,7 +963,8 @@ and the dashboard warns when a site-relative icon path has no matching file.
908
963
 
909
964
  - **Use descriptive names** (\`customer_support_agent\` not \`agent1\`)
910
965
  - **Always use the _agent suffix** - names like \`support_agent\`, \`research_agent\`
911
- - **Always set maxSteps or maxSessionTurns** as safety limits
966
+ - **Write sideB explicitly** - there is no implicit default; a user-facing agent ends with \`sideB: { human: true }\`
967
+ - **Cap prompt \xD7 prompt agents** - set \`maxSessionTurns\` when both sides are prompt-backed; nothing external stops two models
912
968
  - **Match stop conditions to use case** - chat apps use stopOnResponse, workflows use stopTool
913
969
  - **Use labels** for clarity in logs and UI
914
970
  - **Organize with tags** for filtering
@@ -918,6 +974,150 @@ and the dashboard warns when a site-relative icon path has no matching file.
918
974
  Full reference: https://docs.standardagentbuilder.com/api-reference/define/agent
919
975
  `;
920
976
 
977
+ // src/docs/handlers.ts
978
+ var HANDLERS_AGENTS_MD = `# Handler Definitions
979
+
980
+ A handler is a function that takes one side's turns in a conversation. When a side of an agent is backed by a handler, the runtime does not call a model to answer an inbound message \u2014 it calls the handler function once, with the thread state and the message, and whatever that function does is the side's turn.
981
+
982
+ A handler is **not** a hook (hooks decorate turns owned by something else, and 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). Its only role is backing a side of an agent. Handlers may not be referenced by prompt \`tools\` arrays, hook lists, or effect schedules \u2014 such references are definition-time errors.
983
+
984
+ ## Properties
985
+
986
+ | Property | Type | Required | Description |
987
+ |----------|------|----------|-------------|
988
+ | \`description\` | \`string\` | No | Human-facing summary |
989
+ | \`execute\` | \`(state: ThreadState, message: Message) => Promise<void>\` | Yes | The turn implementation |
990
+
991
+ The handler's name comes from its filename: \`agents/handlers/relay_pipe.ts\` defines the handler \`relay_pipe\`. The \`message\` parameter is the full persisted message \u2014 id, role, content, silent flag, metadata, timestamps. Handlers are trusted definition code, like tools.
992
+
993
+ ## Basic Example
994
+
995
+ \`\`\`typescript
996
+ // agents/handlers/relay_pipe.ts
997
+ import { defineHandler } from '@standardagents/builder';
998
+
999
+ export default defineHandler({
1000
+ description: 'Forwards inbound user traffic to the assistant child and relays answers back.',
1001
+ execute: async (state, message) => {
1002
+ // full ThreadState \u2014 forward, respond, spawn, stop, read/write KV
1003
+ },
1004
+ });
1005
+ \`\`\`
1006
+
1007
+ Reference it from exactly one place: the \`handler\` property of an agent side:
1008
+
1009
+ \`\`\`typescript
1010
+ export default defineAgent({
1011
+ name: 'relay_agent',
1012
+ sideA: { handler: 'relay_pipe', subagents: [{ agent: 'assistant_agent', maxInstances: 1 }] },
1013
+ sideB: { human: true },
1014
+ });
1015
+ \`\`\`
1016
+
1017
+ ## Execution Model
1018
+
1019
+ A handler-backed side's turn is:
1020
+
1021
+ 1. An inbound message arrives on the side's perspective.
1022
+ 2. The runtime invokes the handler once with the thread's \`ThreadState\` and that message.
1023
+ 3. The invocation returns (or throws). The turn is complete.
1024
+
1025
+ There are no steps, no tool-choice loop, and no model requests for this side. \`maxSteps\`, \`stopOnResponse\`, \`stopTool\`, and session bindings are not legal on handler sides \u2014 a handler turn has no steps to cap and ends when \`execute\` returns. Handlers end sessions explicitly via \`state.stopSession()\`. When the agent sets \`maxSessionTurns\`, a handler turn counts toward the exchange cap exactly like a prompt turn.
1026
+
1027
+ ## Delivery Semantics
1028
+
1029
+ Delivery to a handler is **exactly-once per message**:
1030
+
1031
+ - The handler is invoked exactly once for each inbound message on the side's perspective, in message persistence order.
1032
+ - The handler is never invoked concurrently for the same thread. Messages that arrive while a handler turn is in flight wait in the queue and are delivered one at a time, in order.
1033
+ - A message whose handler invocation failed is not automatically redelivered. Retry is a userland decision (re-send the message).
1034
+
1035
+ **Warning:** Because queued messages wait behind the in-flight turn, a long-running handler stalls its thread. A handler that awaits a \`blocking\` child session holds the delivery slot for that session's entire duration \u2014 and if that child can only finish after *another* inbound message on this thread, the thread deadlocks. Relay-style handlers should forward without blocking (\`queueMessage\` toward the child) and treat the child's replies as ordinary later turns.
1036
+
1037
+ ## Error Semantics
1038
+
1039
+ A handler that throws is **fatal to the turn, and only the turn**:
1040
+
1041
+ - The turn is marked failed and the error is surfaced in the runtime's request/execution log as a runtime-origin failure \u2014 a throwing handler never silently swallows a message.
1042
+ - The inbound message stays persisted. Nothing is rolled back.
1043
+ - The **session continues**: the failure does not end the session, terminate the thread, or poison the queue. The next inbound message gets a fresh handler invocation.
1044
+
1045
+ If an error should end the session, the handler must do so explicitly with \`state.stopSession()\`.
1046
+
1047
+ ## Capabilities
1048
+
1049
+ A handler receives the full \`ThreadState\` and may do anything tools can do:
1050
+
1051
+ - **Forward** \u2014 \`(await state.getChildThread(ref)).queueMessage(...)\` toward a child, or \`state.notifyParent(...)\` toward a parent.
1052
+ - **Respond** \u2014 \`state.injectMessage({ role, content })\` in its own side's role (\`assistant\` for side A, \`user\` for side B).
1053
+ - **Spawn** \u2014 create child instances of the side's declared \`subagents\` relationships via lifecycle APIs.
1054
+ - **Control** \u2014 \`state.stopSession()\`, \`state.setStatus()\`, effects, environment, key-value store, filesystem.
1055
+
1056
+ Handlers need no tool grants for children: the side's \`subagents\` declarations establish existence and lifecycle, and the handler reaches children through \`ThreadState\` directly. (The injected lifecycle *tools* exist for prompts \u2014 a handler calls the same operations as code.)
1057
+
1058
+ ## The Outbound Boundary
1059
+
1060
+ Handlers get no auto-injected reply channel. If a handler (or the side it feeds) should speak to a user or an external surface, that is userland code \u2014 a tool or handler calling the messaging surface it chooses (the \`message_user\` pattern). **The runtime routes inbound; userland decides outbound.**
1061
+
1062
+ ## Livelock Discipline
1063
+
1064
+ Handler sides do not run away on their own \u2014 a handler runs only when a message arrives. But a handler that *replies to every inbound message* paired with a prompt-backed side creates a message ping-pong with no natural stop. Handler authors own their termination discipline:
1065
+
1066
+ - reply conditionally, not unconditionally;
1067
+ - or set \`maxSessionTurns\` on the agent (available on every shape) as a hard safety valve;
1068
+ - or end the session explicitly with \`state.stopSession()\` when the work is done.
1069
+
1070
+ ## Worked Example: the Relay
1071
+
1072
+ A user-facing thread that does **zero model work**: every inbound user message is forwarded verbatim to a composed assistant child; every child answer is relayed back out through userland messaging. One handler expresses the entire surface:
1073
+
1074
+ \`\`\`typescript
1075
+ // agents/handlers/relay_pipe.ts
1076
+ import { defineHandler } from '@standardagents/builder';
1077
+
1078
+ export default defineHandler({
1079
+ description: 'Pipe user traffic to the assistant child; relay answers outward.',
1080
+ execute: async (state, message) => {
1081
+ const child = state.children.find(c => c.relationship === 'assistant_agent');
1082
+ if (child) {
1083
+ const thread = await state.getChildThread(child.reference);
1084
+ await thread?.queueMessage({ role: 'user', content: message.content ?? '' });
1085
+ }
1086
+ },
1087
+ });
1088
+
1089
+ // agents/agents/relay_agent.ts
1090
+ import { defineAgent } from '@standardagents/builder';
1091
+
1092
+ export default defineAgent({
1093
+ name: 'relay_agent',
1094
+ sideA: {
1095
+ handler: 'relay_pipe',
1096
+ subagents: [{
1097
+ agent: 'assistant_agent',
1098
+ maxInstances: 1,
1099
+ immediate: true, // spawned at thread creation
1100
+ parentCommunication: 'explicit',
1101
+ }],
1102
+ },
1103
+ sideB: { human: true },
1104
+ });
1105
+ \`\`\`
1106
+
1107
+ The child's answers arrive as inbound messages on side A (the owning side) and get their own handler turns, where the handler calls the project's outbound messaging tool. No model participates at any point.
1108
+
1109
+ ## Best Practices
1110
+
1111
+ - **Forward without blocking** \u2014 queue toward children and treat their replies as later turns; never hold the delivery slot on a child session
1112
+ - **Reply conditionally** \u2014 an unconditional reply opposite a prompt side is a ping-pong
1113
+ - **End sessions explicitly** \u2014 \`state.stopSession()\` when the work is done; there are no declarative session bindings on handler sides
1114
+ - **Keep handlers deterministic** \u2014 code that wants a model writes a prompt; code that wants a capability writes a tool; code that decorates writes a hook
1115
+
1116
+ ## Documentation
1117
+
1118
+ Full reference: https://docs.standardagentbuilder.com/api-reference/define/handler
1119
+ `;
1120
+
921
1121
  // src/docs/tools.ts
922
1122
  var TOOLS_AGENTS_MD = `# Custom Tools
923
1123
 
@@ -1845,7 +2045,7 @@ var DEFAULT_TEMPLATE_VERSIONS = {
1845
2045
  typescript: "latest",
1846
2046
  zod: "latest"
1847
2047
  };
1848
- var AGENTBUILDER_TEMPLATE_VERSION = "0.40.12";
2048
+ var AGENTBUILDER_TEMPLATE_VERSION = "0.41.0";
1849
2049
  function ensureTrailingNewline(value) {
1850
2050
  return value.endsWith("\n") ? value : `${value}
1851
2051
  `;
@@ -2028,6 +2228,8 @@ function renderAgentBuilderProject(options) {
2028
2228
  symlink("agents/agents/CLAUDE.md", "AGENTS.md", "Add Claude agent docs symlink"),
2029
2229
  file("agents/prompts/AGENTS.md", PROMPTS_AGENTS_MD, "Add prompt docs"),
2030
2230
  symlink("agents/prompts/CLAUDE.md", "AGENTS.md", "Add Claude prompt docs symlink"),
2231
+ file("agents/handlers/AGENTS.md", HANDLERS_AGENTS_MD, "Add handler docs"),
2232
+ symlink("agents/handlers/CLAUDE.md", "AGENTS.md", "Add Claude handler docs symlink"),
2031
2233
  file("agents/models/AGENTS.md", MODELS_AGENTS_MD, "Add model docs"),
2032
2234
  symlink("agents/models/CLAUDE.md", "AGENTS.md", "Add Claude model docs symlink"),
2033
2235
  file("agents/providers/AGENTS.md", PROVIDERS_AGENTS_MD, "Add provider docs"),
@@ -2055,6 +2257,8 @@ export {
2055
2257
  APPROVED_DEPENDENCY_BUILD_SCRIPTS,
2056
2258
  DEFAULT_TEMPLATE_VERSIONS,
2057
2259
  FIRST_PARTY_PROVIDER_PACKAGES,
2260
+ HANDLERS_AGENTS_MD,
2261
+ HANDLERS_AGENTS_MD as HANDLERS_CLAUDE_MD,
2058
2262
  HOOKS_AGENTS_MD,
2059
2263
  HOOKS_AGENTS_MD as HOOKS_CLAUDE_MD,
2060
2264
  MODELS_AGENTS_MD,