@standardagents/agentbuilder-template 0.45.2 → 0.45.3

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
@@ -1,521 +1,370 @@
1
1
  // src/docs/root.ts
2
- var ROOT_AGENTS_MD = `# Standard Agents
2
+ var ROOT_AGENTS_MD = `# Building agents in this project
3
3
 
4
- This project uses Standard Agents - a framework for building AI agents on Cloudflare Workers.
4
+ This project builds AI agents with Standard Agents. You define agents as TypeScript files under \`agents/\`. The framework discovers them, runs them, and serves a builder UI for testing them. There is no registration step and no Cloudflare configuration to write.
5
5
 
6
- ## Architecture Overview
6
+ Each directory under \`agents/\` has its own \`AGENTS.md\` with the full reference for that kind of file. This file covers what the pieces are, the agent the project starts with, and how to build your own.
7
7
 
8
- Standard Agents uses a **composition model** where four core components work together:
8
+ ## The pieces
9
9
 
10
+ \`\`\`text
11
+ Model -> Prompt -> Agent -> Thread
10
12
  \`\`\`
11
- Model \u2192 Prompt \u2192 Agent \u2192 Thread
12
- \u2502 \u2502 \u2502 \u2502
13
- \u2502 \u2502 \u2502 \u2514\u2500 Conversation instance (Durable Object)
14
- \u2502 \u2502 \u2514\u2500 Orchestrates conversation flow
15
- \u2502 \u2514\u2500 System instructions + tools
16
- \u2514\u2500 LLM provider configuration
17
- \`\`\`
18
13
 
19
- **Models** define which LLM to use (provider, model ID, pricing, fallbacks).
20
- **Prompts** define what the LLM should do (system instructions, available tools).
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
- **Threads** are individual conversation instances with persistent storage.
23
-
24
- ## Packability and Runtime Portability
25
-
26
- Treat an agent project as an installable Standard Agent package, not as a
27
- privileged extension of one runtime. Everything required for the declared
28
- agents to execute must use capabilities defined by the Standard Agent
29
- Specification and exported with the package.
30
-
31
- Do not make required behavior depend on private Durable Object methods,
32
- implementation-specific storage schemas or bindings, undocumented HTTP or
33
- WebSocket events, product sidecars that replace runtime behavior, or
34
- ThreadState._notPackableRuntimeContext. Client-side state may be an
35
- optimistic cache, but it must not become the only source of truth for agent
36
- semantics.
37
-
38
- If the specification lacks a required primitive, define the runtime-neutral
39
- capability in @standardagents/spec, document it in the public specification,
40
- implement it generically in the runtime, and add conformance coverage before
41
- the agent relies on it. Authentication, billing, deployment, and branded UI
42
- services may remain external as long as they are not required for the packed
43
- agent's core execution semantics.
44
-
45
- ## Packaging and Definition Visibility
46
-
47
- The packer follows side \`subagents\` relationships and includes child agents
48
- and their dependencies. This includes hidden children and relationships with
49
- \`as\`. The package includes referenced handlers and exports a \`handlers\`
50
- registry. The pack and unpack dialogs list handlers. Unpack analysis reports an agent\u2019s handlers and side \`subagents\` relationships.
51
- In the pack dialog, a handler defaults to extract. Shared handlers default to copy. Extract removes
52
- the local file; copy keeps it. Dependencies reached by another local agent,
53
- including through its children, default to copy.
54
-
55
- The runtime discovers packages in \`node_modules\` and \`agents/packed/\`.
56
- It loads definitions from lazy registries and \`__meta\` exported by each
57
- package entry module. A malformed \`package.json\` or missing or invalid
58
- \`__meta\` produces a warning and the runtime skips that package.
59
-
60
- \`ThreadState.loadModel\`, \`loadPrompt\`, and \`loadAgent\` enforce package
61
- visibility. Global code can load unpacked definitions and packed entry agents.
62
- Packed code can load its own definitions and other packages' entry agents.
63
- A packed model cannot fall back to a host model with the same short name.
64
- A refused load returns the same not-found error as a missing definition.
65
-
66
- ## How Components Compose
67
-
68
- ### Basic Flow
69
- 1. User creates a thread with an agent
70
- 2. Agent uses its configured prompt
71
- 3. Prompt specifies the model and tools
72
- 4. LLM responds, potentially calling tools
73
- 5. Tools execute and return results
74
- 6. Loop continues until stop condition
75
-
76
- ### Sub-Prompts (Prompts as Tools)
77
- Prompts can call other prompts as tools. This enables:
78
- - **Specialized reasoning**: A main prompt delegates to expert sub-prompts
79
- - **Structured outputs**: Sub-prompt validates and formats data
80
- - **Context isolation**: Sub-prompt gets fresh context without full history
14
+ - A **model** names an LLM and the provider that serves it.
15
+ - A **prompt** holds system instructions, picks a model, and lists the tools the model may call.
16
+ - An **agent** is a conversation between two sides. Each side is a person (\`{ human: true }\`), a prompt (\`{ prompt: 'name' }\`), or a handler (\`{ handler: 'name' }\`, plain code).
17
+ - A **thread** is one running conversation with an agent. It has its own message history, files, and key-value store.
18
+
19
+ | Function | What it defines | Directory |
20
+ |----------|-----------------|-----------|
21
+ | \`defineModel()\` | An LLM and its provider | \`agents/models/\` |
22
+ | \`defineProvider()\` | A custom provider endpoint | \`agents/providers/\` |
23
+ | \`definePrompt()\` | Instructions, model, and tools | \`agents/prompts/\` |
24
+ | \`defineAgent()\` | The two sides of a conversation | \`agents/agents/\` |
25
+ | \`defineTool()\` | A function the model can call | \`agents/tools/\` |
26
+ | \`defineHook()\` | Code that runs at lifecycle points | \`agents/hooks/\` |
27
+ | \`defineHandler()\` | Code that takes a side's turns | \`agents/handlers/\` |
28
+ | \`defineThreadEndpoint()\` | An HTTP route on a thread | \`agents/api/\` |
29
+ | \`defineUserEndpoint()\` | An HTTP route on a user | \`agents/users/\` |
30
+
31
+ Import \`defineThreadEndpoint\` and \`defineUserEndpoint\` from \`@standardagents/spec\`. Import the rest from \`@standardagents/builder\`.
32
+
33
+ ## The starter agent
34
+
35
+ A project created through the Standard Agents platform starts with one working agent, **Guide**. It answers questions about building agents and about the Standard Agent Specification. Run the project, choose **Guide**, and send a message; it needs no file edits and no provider key.
36
+
37
+ | File | What it is |
38
+ |------|------------|
39
+ | \`agents/agents/guide_agent.ts\` | The agent \`guide_agent\`: the prompt \`guide\` on one side, a person on the other |
40
+ | \`agents/prompts/guide.ts\` | The prompt \`guide\`: its instructions, the model \`low_cost\`, and the tool \`read_docs\` |
41
+ | \`agents/models/low_cost.ts\` | The model \`low_cost\`: a low-cost open model served by Novita AI, paid for by the platform account key |
42
+ | \`agents/tools/read_docs.ts\` | The tool \`read_docs\`: reads a page from the builder documentation or the specification site, and no other site |
43
+
44
+ Guide reads the published documentation each time it answers, so it stays current without a code change. It cannot read or change the files in this project.
45
+
46
+ These four are ordinary definition files. Read them as a working example, change them, or delete them once the project has its own agents. Before deleting \`agents/models/low_cost.ts\` or \`agents/tools/read_docs.ts\`, check that no other prompt uses them. If a project does not have these files, nothing else depends on them.
47
+
48
+ ## Build your first agent
49
+
50
+ A working agent needs three files: a model, a prompt that uses it, and an agent that uses the prompt. Create them as written.
81
51
 
82
52
  \`\`\`typescript
83
- // Main prompt can call 'data_extractor' as a tool
84
- definePrompt({
85
- name: 'main_assistant',
86
- tools: ['data_extractor'], // Another prompt exposed as tool
87
- // ...
53
+ // agents/models/default.ts
54
+ import { defineModel } from '@standardagents/builder';
55
+ import { openai } from '@standardagents/openai';
56
+
57
+ export default defineModel({
58
+ name: 'default',
59
+ provider: openai,
60
+ model: 'gpt-5.4-mini',
88
61
  });
89
62
  \`\`\`
90
63
 
91
- ### Agent Handoffs (Agents as Tools)
92
- Agents can delegate to other agents entirely:
93
- - **Specialization**: Route to domain-specific agents
94
- - **Escalation**: Hand off to more capable agents
95
- - **Workflows**: Chain agents for multi-step processes
96
-
97
64
  \`\`\`typescript
98
- defineAgent({
99
- name: 'triage_agent',
100
- exposeAsTool: true,
101
- toolDescription: 'Hand off to triage for initial assessment',
102
- // ...
65
+ // agents/prompts/assistant.ts
66
+ import { definePrompt } from '@standardagents/builder';
67
+
68
+ export default definePrompt({
69
+ name: 'assistant',
70
+ toolDescription: 'A general-purpose assistant',
71
+ model: 'default',
72
+ prompt: 'You are a helpful assistant. Answer clearly and briefly.',
73
+ includeChat: true,
103
74
  });
104
75
  \`\`\`
105
76
 
106
- ## File Structure
77
+ \`\`\`typescript
78
+ // agents/agents/assistant_agent.ts
79
+ import { defineAgent } from '@standardagents/builder';
107
80
 
108
- \`\`\`
109
- AGENTS.md # Project guidance for AI coding tools
110
- CLAUDE.md -> AGENTS.md # Compatibility symlink
111
- vite.config.ts # Uses builder(); Cloudflare config is generated
112
- tsconfig.json # Includes agents and generated .agents types
113
- pnpm-workspace.yaml # Dependency script approvals for pnpm projects
114
- agents/
115
- \u251C\u2500\u2500 agents/ # Agent definitions (defineAgent)
116
- \u2502 \u251C\u2500\u2500 AGENTS.md
117
- \u2502 \u2514\u2500\u2500 CLAUDE.md -> AGENTS.md
118
- \u251C\u2500\u2500 prompts/ # Prompt definitions (definePrompt)
119
- \u2502 \u251C\u2500\u2500 AGENTS.md
120
- \u2502 \u2514\u2500\u2500 CLAUDE.md -> AGENTS.md
121
- \u251C\u2500\u2500 handlers/ # Handler definitions (defineHandler)
122
- \u2502 \u251C\u2500\u2500 AGENTS.md
123
- \u2502 \u2514\u2500\u2500 CLAUDE.md -> AGENTS.md
124
- \u251C\u2500\u2500 models/ # Model configurations (defineModel)
125
- \u2502 \u251C\u2500\u2500 AGENTS.md
126
- \u2502 \u2514\u2500\u2500 CLAUDE.md -> AGENTS.md
127
- \u251C\u2500\u2500 providers/ # Provider definitions (defineProvider)
128
- \u2502 \u251C\u2500\u2500 AGENTS.md
129
- \u2502 \u2514\u2500\u2500 CLAUDE.md -> AGENTS.md
130
- \u251C\u2500\u2500 tools/ # Custom tools (defineTool)
131
- \u2502 \u251C\u2500\u2500 AGENTS.md
132
- \u2502 \u2514\u2500\u2500 CLAUDE.md -> AGENTS.md
133
- \u251C\u2500\u2500 hooks/ # Lifecycle hooks (defineHook)
134
- \u2502 \u251C\u2500\u2500 AGENTS.md
135
- \u2502 \u2514\u2500\u2500 CLAUDE.md -> AGENTS.md
136
- \u2514\u2500\u2500 api/ # Thread API endpoints (defineThreadEndpoint)
137
- \u251C\u2500\u2500 AGENTS.md
138
- \u2514\u2500\u2500 CLAUDE.md -> AGENTS.md
81
+ export default defineAgent({
82
+ name: 'assistant_agent',
83
+ title: 'Assistant',
84
+ description: 'Answers questions in a chat.',
85
+ sideA: { label: 'Assistant', prompt: 'assistant' },
86
+ sideB: { label: 'You', human: true },
87
+ });
139
88
  \`\`\`
140
89
 
141
- The \`builder()\` Vite plugin generates the Worker entry, Durable Object bindings, Cloudflare Vite plugin configuration, and local \`.agents/\` support files at build time. Agent files are **auto-discovered** at runtime. No manual registration needed.
142
-
143
- Standalone local pnpm init approves \`esbuild\` and \`workerd\` before installing dependencies.
144
- The project policy lives in \`pnpm-workspace.yaml\`.
145
- The manifest also declares \`pnpm.onlyBuiltDependencies\` for pnpm 10.
146
- The YAML declares the project root for pnpm 9 compatibility.
147
- With pnpm 9 or pnpm 10 before 10.6, \`pnpm add --workspace-root <package>\` targets that project root.
148
- pnpm 11 may warn that the legacy manifest field is ignored.
149
- The YAML supplies its build approvals.
150
- An existing project policy is preserved.
151
- Local OpenRouter setup explicitly denies the SDK's optional model-type check.
152
- That checker writes no runtime artifacts.
153
- The CLI detects unsupported legacy layouts with pnpm's parser.
154
- Those layouts use \`--ignore-workspace\` during installation and foreground startup.
155
- Supported layouts retain their YAML build approvals.
156
- An enclosing pnpm workspace retains its policy and installation boundary.
157
- Contributor \`--workspace\` projects use their own policy boundary.
158
- The generated policy exempts \`@standardagents/*\` from \`minimumReleaseAge\`.
159
- That exemption permits installation on the release day.
160
-
161
- For remote local development, opt exact hostnames into the authoring boundary:
90
+ Then run the project and talk to the agent:
162
91
 
163
- \`\`\`typescript
164
- builder({ trustedDevHosts: ['macbookserver.tail13bd39.ts.net'] })
92
+ \`\`\`bash
93
+ pnpm dev
165
94
  \`\`\`
166
95
 
167
- This never changes hosted behavior. Adding a hostname grants anyone who can
168
- reach the dev server through that name the ability to attempt authoring access;
169
- same-origin and normal authoring credential checks still apply. Wildcards,
170
- schemes, ports, and paths are rejected.
96
+ Open \`http://localhost:5173\`, sign in with your Standard Agents account, choose **Assistant**, and send a message. A project with no account link signs in with the \`SUPER_ADMIN_PASSWORD\` set in \`.dev.vars\` instead. Use the package manager whose lockfile is in the project (\`npm run dev\`, \`yarn dev\`, or \`bun run dev\` work the same way). If port 5173 is in use, the dev server takes the next free port and prints its address.
171
97
 
172
- ## ThreadState
98
+ New and changed files under \`agents/\` load without a restart. Restart the dev server after you install a package or change \`.dev.vars\`.
173
99
 
174
- \`ThreadState\` is the state object available in tools and hooks:
100
+ ## Model access and keys
175
101
 
176
- \`\`\`typescript
177
- interface ThreadState {
178
- // Identity (readonly)
179
- threadId: string; // Thread identifier
180
- agentId: string; // Agent identifier
181
- userId: string | null; // User identifier (if set)
182
- createdAt: number; // Creation timestamp
183
- children: SubagentRegistryEntry[]; // Child instance registry
184
- terminated: number | null; // Soft-termination timestamp
185
- arguments: Record<string, unknown>; // Invocation or subagent creation args
186
- context: Record<string, unknown>; // Current-execution scratch data
187
- runtimeStreams?: RuntimeStreams; // Optional live execution-host byte streams
188
-
189
- // Methods
190
- getMessages(options?): Promise<MessagesResult>; // Get conversation history, newest first by default
191
- getMessage(id): Promise<Message | null>; // Find a persisted message by ID
192
- updateMessage(id, updates): Promise<Message>; // Update a persisted message
193
- injectMessage(input): Promise<Message>; // Add message to thread
194
- queueMessage(input): Promise<void>; // Queue message for next step
195
- emit(event, data): void; // Broadcast a custom event to connected thread clients
196
- loadModel(name): Promise<ModelConfig>; // Load model definition
197
- loadPrompt(name): Promise<PromptConfig>; // Load prompt definition
198
- loadAgent(name): Promise<AgentConfig>; // Load agent definition
199
- env(propertyName): Promise<string>; // Resolve env value for this thread
200
- setEnv(propertyName, value): Promise<void>; // Set thread env + propagate to active descendants
201
- getValue<T = unknown>(key: string): Promise<T | null>; // Read durable per-thread JSON value
202
- setValue(key: string, value: unknown): Promise<void>; // Write durable value; null/undefined deletes
203
- status(): Promise<ThreadStatus>; // Run status { state: 'idle'|'running'|'failed', since, reason }
204
- log(entry): Promise<void>; // App log row { name, message?, level?, status?, trigger?, request?, response?, error?, duration_ms?, cost?, data? }; no message, no execution
205
- notifyParent(content): Promise<void>; // Queue a silent message to the parent when one exists
206
- setStatus(status): Promise<void>; // Update this child in the parent registry; no-op without a parent
207
- stopSession(options?): Promise<void>; // Terminally stop the current agent session
208
- getChildThread(referenceId): Promise<ThreadState | null>; // Resolve child thread
209
- getParentThread(): Promise<ThreadState | null>; // Resolve parent thread
210
- terminate(): Promise<void>; // Soft-terminate thread
211
- runCode(source, options?): CodeExecution; // Run JS/TS in a fresh Dynamic Worker sandbox
212
- }
102
+ This project is linked to a Standard Agents platform account. The CLI wrote that account's key to \`.dev.vars\` as \`STANDARD_AGENTS_API_KEY\`, and deployed projects receive it automatically. The key covers every provider package this project installs, so the example above runs without any provider account:
103
+
104
+ | Provider | Package | Import |
105
+ |----------|---------|--------|
106
+ | OpenAI | \`@standardagents/openai\` | \`openai\` |
107
+ | Anthropic | \`@standardagents/anthropic\` | \`anthropic\` |
108
+ | Google | \`@standardagents/google\` | \`google\` |
109
+ | OpenRouter | \`@standardagents/openrouter\` | \`openrouter\` |
110
+ | xAI | \`@standardagents/xai\` | \`xai\` |
111
+ | Groq | \`@standardagents/groq\` | \`groq\` |
112
+ | Cerebras | \`@standardagents/cerebras\` | \`cerebras\` |
113
+ | DeepInfra | \`@standardagents/deepinfra\` | \`deepinfra\` |
114
+ | Novita AI | \`@standardagents/novita\` | \`novita\` |
115
+ | Cloudflare Workers AI | \`@standardagents/cloudflare\` | \`cloudflare\` |
116
+
117
+ Usage through the platform key is billed to the platform account. To use your own provider account, set that provider's key in \`.dev.vars\`; see \`agents/models/AGENTS.md\`. A project without \`STANDARD_AGENTS_API_KEY\` needs a provider key for each provider it uses.
118
+
119
+ Never guess a model ID. Take it from the provider's published model list, or from the current model guide:
120
+
121
+ \`\`\`bash
122
+ npx @standardagents/cli@latest current-models
213
123
  \`\`\`
214
124
 
215
- \`state.arguments\` is read-only thread invocation data. Resumable \`subagent_create.arguments\` persists with the child thread, is visible to both child sides, and is also available as prompt variables. Use \`state.context\` only for mutable scratch data during the current execution; it is not durable storage.
216
-
217
- \`state.runtimeStreams\` is optional. Feature-detect it before opening a live
218
- byte stream. Capability names and metadata are runtime-defined; streams never
219
- park or survive an execution-host disconnect. See the tools guide for a full
220
- feature-detection, write, read, and cleanup example.
221
-
222
- \`state.emit(event, data)\` works from tools, hooks, and thread endpoints. In
223
- an endpoint it broadcasts immediately even though there is no active
224
- execution, which is useful for invalidating client snapshots after a durable
225
- \`setValue()\`. Events are not durable; clients must re-read state after
226
- reconnecting.
227
-
228
- \`state.status()\` returns the authoritative run status. \`reason\` is the raw
229
- stop code of the last run (\`stop_on_response\`, \`awaiting_human\`,
230
- \`hook_blocked\`, \`step_error\`, \u2026); a run that handed the turn to a human
231
- side is \`idle\`. Clients get the same value from \`GET /api/threads/:id\`
232
- (\`status\`) and from \`thread_status\` stream frames (snapshot on connect, then
233
- each change), so never poll for it. It is unrelated to \`setStatus()\`.
234
-
235
- \`state.log({ name, message, level, status, trigger, request, response, error, duration_ms, cost, data })\`
236
- records an application log row (\`kind: 'app'\`) shown with the thread's logs;
237
- only \`name\` is required. For an operation, record its outcome (\`status\`:
238
- success | error | skipped), what caused it (\`trigger: { kind, id?, summary? }\`),
239
- what was sent (\`request\`), what came back (\`response\` or \`error\`), and its
240
- \`duration_ms\` and \`cost\` (USD; counted in the thread's cost totals). It never creates a message or starts
241
- execution; oversized fields are truncated (name 120 chars, message and error
242
- 4 KiB, data JSON 16 KiB, request and response JSON 64 KiB each).
243
-
244
- \`runCode\` has no implicit access to thread state, filesystem, network, timers, or host globals. Bridge only the exact capabilities needed through \`imports\` or \`globals\`; use \`modules\` for local relative ES modules; use \`execute\` to select the export and args to run. \`modules\` keys are graph-root relative paths; entry-source imports resolve from \`filename\` when supplied, including \`../\` parent paths that stay inside the supplied graph. TypeScript input is accepted by default and erased before evaluation. In thread endpoints, \`runCode\` forwards functions in imports, globals, and execution arguments as callable proxies. Call \`run.terminate(reason)\` to stop the sandbox. A caller disconnect also stops the run. When \`report\` is supplied, the host receives reports during the run and \`run.reports\` updates as reports arrive.
125
+ \`.dev.vars\` holds secrets and is ignored by Git. Do not commit it or copy its values into source files.
245
126
 
246
- ## Subagents
127
+ ## Check your work
247
128
 
248
- 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):
129
+ - \`pnpm typecheck\` reports type errors in definition files.
130
+ - The dev server terminal prints \`Failed to import <kind> <name>\` when a definition file cannot load.
131
+ - The builder UI shows the same failure in a banner at the top of the page. A model, prompt, or agent that fails to load is missing from its list until the file is fixed.
132
+ - A \`Cannot find module\` failure names a package that is not installed. Add it with the project's package manager, then restart the dev server.
133
+ - Each thread in the builder UI has a Logs tab with every model request, tool call, and error.
249
134
 
250
- - Each subagent runs in its own thread (\`DurableThread\`)
251
- - Parent/child linkage is tracked in thread metadata; instances are visible in \`state.children\`
252
- - 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
253
- - 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
254
- - \`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
255
- - Child completion results are queued back to the parent when \`parentCommunication\` is \`implicit\`
256
- - 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
257
- - 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.
135
+ ## Project layout
258
136
 
259
- Access in tools:
260
- \`\`\`typescript
261
- export default defineTool('...', schema, async (state, args) => {
262
- const threadId = state.threadId;
263
- const creationArgs = state.arguments;
264
- const { messages } = await state.getMessages();
265
- const count = (await state.getValue<number>('invocation_count')) ?? 0;
266
- await state.setValue('invocation_count', count + 1);
267
- // ...
268
- });
137
+ \`\`\`text
138
+ AGENTS.md This guide
139
+ CLAUDE.md -> AGENTS.md The same file, for tools that read CLAUDE.md
140
+ package.json Dependencies; the project owns its versions
141
+ vite.config.ts Loads the builder() plugin
142
+ tsconfig.json
143
+ pnpm-workspace.yaml pnpm settings for this project
144
+ .dev.vars Local secrets (not committed)
145
+ agents/
146
+ \u251C\u2500\u2500 agents/ Agent definitions; starts with guide_agent.ts
147
+ \u251C\u2500\u2500 prompts/ Prompt definitions; starts with guide.ts
148
+ \u251C\u2500\u2500 models/ Model definitions; starts with low_cost.ts
149
+ \u251C\u2500\u2500 providers/ Custom provider definitions
150
+ \u251C\u2500\u2500 tools/ Tools the model can call; starts with read_docs.ts
151
+ \u251C\u2500\u2500 hooks/ Lifecycle hooks
152
+ \u251C\u2500\u2500 handlers/ Code-backed sides
153
+ \u251C\u2500\u2500 api/ Thread HTTP endpoints
154
+ \u2514\u2500\u2500 users/ User HTTP endpoints
269
155
  \`\`\`
270
156
 
271
- Access in hooks:
272
- \`\`\`typescript
273
- export default defineHook({
274
- hook: 'before_create_message',
275
- id: 'my_hook',
276
- execute: async (state, message) => {
277
- console.log(\`Thread: \${state.threadId}, Agent: \${state.agentId}\`);
278
- return message;
279
- }
280
- });
281
- \`\`\`
157
+ Every directory under \`agents/\` contains an \`AGENTS.md\` and a \`CLAUDE.md\` link to it. The \`builder()\` plugin generates the Worker entry, the Cloudflare configuration, and the \`.agents/\` type files. Do not add a Wrangler config, a Worker entry file, or the Cloudflare Vite plugin.
282
158
 
283
- ## Quick Reference
159
+ ## Naming
284
160
 
285
- | Function | Purpose | Directory |
286
- |----------|---------|-----------|
287
- | \`defineModel()\` | Configure LLM provider | \`agents/models/\` |
288
- | \`defineProvider()\` | Custom provider endpoints and overrides | \`agents/providers/\` |
289
- | \`definePrompt()\` | System instructions + tools | \`agents/prompts/\` |
290
- | \`defineHandler()\` | Deterministic side turns | \`agents/handlers/\` |
291
- | \`defineAgent()\` | Conversation orchestration | \`agents/agents/\` |
292
- | \`defineTool()\` | Custom tool functions | \`agents/tools/\` |
293
- | \`defineHook()\` | Lifecycle interception | \`agents/hooks/\` |
294
- | \`defineThreadEndpoint()\` | Custom API routes | \`agents/api/\` |
161
+ - A model, prompt, or agent is named by its \`name\` field. Keep the file name the same.
162
+ - A tool or handler is named by its file name: \`agents/tools/search_docs.ts\` is the tool \`search_docs\`.
163
+ - A hook is named by its \`id\` field.
164
+ - Use \`snake_case\` for all of these. End agent names with \`_agent\`.
165
+ - A tool, a prompt, and an agent exposed as a tool share one namespace. Give them different names.
295
166
 
296
- ## Runtime cost safety
167
+ ## Composing agents
297
168
 
298
- AgentBuilder bounds each Durable Object alarm invocation to 1,000,000 SQLite
299
- rows read and 10,000 rows written. A breach persistently quarantines that one
300
- object, clears its alarm, preserves all storage, and requires an explicit
301
- resume. Routine alarm rows are deleted when completed; effect history remains
302
- available with bounded retention. Deployments integrated with Brolly configure
303
- \`BROLLY_CONTROL_PUBLIC_KEY_JWK\` and \`BROLLY_ACCOUNT_ID\`; the built-in signed
304
- \`POST /api/auth/brolly-control\` route can then quarantine an exact object ID.
169
+ A prompt's \`tools\` array can hold three kinds of names:
305
170
 
306
- Do not expose the Brolly private signing key to the AgentBuilder Worker. Do not
307
- disable the retention sweeper except as a temporary rollback while diagnosing
308
- a deployment.
171
+ - A tool from \`agents/tools/\`.
172
+ - Another prompt. The model calls it as a tool and gets its answer back. Use this for one focused model step, such as extraction or rewriting.
173
+ - An agent with \`exposeAsTool: true\`. Calling it hands the same thread to that agent.
309
174
 
310
- ## Runtime Utilities
175
+ An agent side can also declare \`subagents\`. Each subagent runs in its own thread, and the parent starts and messages it with built-in tools. Use a subagent for work that takes several steps or needs its own review loop. See \`agents/agents/AGENTS.md\`.
311
176
 
312
- Import from \`@standardagents/builder\`:
177
+ Prefer several small agents with a few tools each over one agent with many tools. Models choose tools more reliably from a short list.
178
+
179
+ ## ThreadState
180
+
181
+ Tools, hooks, handlers, and thread endpoints receive a \`ThreadState\` for the thread they run in. The members used most:
182
+
183
+ \`\`\`text
184
+ state.threadId, state.agentId, state.userId Identity
185
+ state.arguments Arguments the thread was created with
186
+ state.getMessages({ limit, offset, order }) Message history: { messages, total, hasMore }
187
+ state.injectMessage({ role, content }) Add a message to the thread
188
+ state.queueMessage({ role, content }) Send a message that starts a turn
189
+ state.getValue(key) / state.setValue(key, v) Durable JSON values for this thread
190
+ state.env(name) / state.setEnv(name, value) Thread variables
191
+ state.readFile / writeFile / findFiles ... The thread's file system
192
+ state.queueTool(name, args) Run another tool after this one
193
+ state.invokeTool(name, args) Run another tool and wait for its result
194
+ state.emit(event, data) Send a live event to connected clients
195
+ state.user() The thread's user, or null
196
+ state.children / state.getChildThread(ref) Subagent threads
197
+ state.stopSession({ result }) End the current session
198
+ state.runCode(source, options) Run JavaScript or TypeScript in a sandbox
199
+ state.execution The running turn, or null when the thread is at rest
200
+ \`\`\`
201
+
202
+ \`agents/tools/AGENTS.md\` shows these in use. The full type is \`ThreadState\` in \`@standardagents/spec\`.
203
+
204
+ ## Development from another machine
205
+
206
+ The \`dev\` script binds the dev server to \`127.0.0.1\`, so other machines cannot reach it. Keep it that way: \`.dev.vars\` holds a live account key.
207
+
208
+ To work from another machine, forward a hostname you trust to the local port, for example through an SSH tunnel or a private reverse proxy. Then list that exact hostname for both Vite and the builder:
313
209
 
314
210
  \`\`\`typescript
315
- import {
316
- queueTool, // Queue another tool for execution
317
- injectMessage, // Add message without triggering execution
318
- getMessages, // Retrieve message history
319
- emitThreadEvent, // Send custom WebSocket events
320
- } from '@standardagents/builder';
211
+ // vite.config.ts
212
+ import { defineConfig } from 'vite';
213
+ import { builder } from '@standardagents/builder';
214
+
215
+ const devHost = 'dev-machine.example.com';
216
+
217
+ export default defineConfig({
218
+ plugins: [builder({ trustedDevHosts: [devHost] })],
219
+ server: { allowedHosts: [devHost] },
220
+ });
321
221
  \`\`\`
322
222
 
323
- ## Documentation
223
+ Anyone who can reach the dev server through a listed hostname can attempt to sign in to it. \`trustedDevHosts\` rejects wildcards, schemes, ports, and paths, and has no effect on a deployed project.
224
+
225
+ ## Deploy
226
+
227
+ On a plan that includes deployment, the platform builds and deploys this project each time you push a commit:
228
+
229
+ \`\`\`bash
230
+ git push
231
+ \`\`\`
232
+
233
+ \`npx @standardagents/cli@latest deploy\` does the same push. The local \`build\` script stops with a message on purpose: builds run on the platform, from the committed \`package.json\` and lockfile. Commit both.
324
234
 
325
- Full documentation: https://docs.standardagentbuilder.com
235
+ ## More documentation
326
236
 
327
- - [Architecture Overview](https://docs.standardagentbuilder.com/introduction/architecture)
328
- - [Models](https://docs.standardagentbuilder.com/core-concepts/models)
329
- - [Prompts](https://docs.standardagentbuilder.com/core-concepts/prompts)
330
- - [Agents](https://docs.standardagentbuilder.com/core-concepts/agents)
331
- - [Tools](https://docs.standardagentbuilder.com/core-concepts/tools)
332
- - [Hooks](https://docs.standardagentbuilder.com/core-concepts/hooks)
333
- - [ThreadState](https://docs.standardagentbuilder.com/core-concepts/flowstate)
334
- - [Thread API](https://docs.standardagentbuilder.com/core-concepts/api)
237
+ - Reference for each definition type: the \`AGENTS.md\` in its directory.
238
+ - Guides and API reference: https://docs.standardagentbuilder.com
239
+ - For coding agents, a longer design guide: \`npx @standardagents/cli@latest skill\`
335
240
  `;
336
241
 
337
242
  // src/docs/models.ts
338
- var MODELS_AGENTS_MD = `# Model Definitions
243
+ var MODELS_AGENTS_MD = `# Model definitions
339
244
 
340
- Models define which LLM provider and model to use for prompts.
245
+ A model definition names an LLM and the provider that serves it. Prompts refer to a model by its \`name\`.
341
246
 
342
247
  ## Properties
343
248
 
344
249
  | Property | Type | Required | Description |
345
250
  |----------|------|----------|-------------|
346
- | \`name\` | \`string\` | Yes | Unique identifier for this model configuration |
347
- | \`provider\` | \`ProviderFactory\` | Yes | Imported provider factory such as \`openrouter\`, \`openai\`, or a local provider from \`agents/providers\` |
348
- | \`model\` | \`string\` | Yes | Model ID sent to provider API |
349
- | \`fallbacks\` | \`string[]\` | No | Fallback model names to try if primary fails |
350
- | \`inputPrice\` | \`number\` | No | Cost per 1M input tokens (USD) |
351
- | \`outputPrice\` | \`number\` | No | Cost per 1M output tokens (USD) |
352
- | \`cachedPrice\` | \`number\` | No | Cost per 1M cached input tokens |
353
- | \`includedProviders\` | \`string[]\` | No | Provider prefixes for OpenRouter routing |
354
- | \`providerTools\` | \`string[]\` | No | Provider-built-in tool names to make available for prompts using this model |
355
-
356
- ## Example
251
+ | \`name\` | \`string\` | Yes | The name prompts use to select this model |
252
+ | \`provider\` | provider import | Yes | A provider imported from its package, or a provider from \`agents/providers/\` |
253
+ | \`model\` | \`string\` | Yes | The model ID the provider expects |
254
+ | \`fallbacks\` | \`string[]\` | No | Names of other models in this project to try when this one fails |
255
+ | \`inputPrice\` | \`number\` | No | USD per 1M input tokens, for cost tracking |
256
+ | \`outputPrice\` | \`number\` | No | USD per 1M output tokens |
257
+ | \`cachedPrice\` | \`number\` | No | USD per 1M cached input tokens |
258
+ | \`includedProviders\` | \`string[]\` | No | OpenRouter only: upstream providers to prefer |
259
+ | \`providerTools\` | \`string[]\` | No | Provider-run tools this model offers to prompts |
260
+ | \`providerOptions\` | \`object\` | No | Default request options for this provider |
261
+ | \`capabilities\` | \`object\` | No | Overrides for what the model supports |
262
+
263
+ ## A first model
357
264
 
358
265
  \`\`\`typescript
266
+ // agents/models/fast.ts
359
267
  import { defineModel } from '@standardagents/builder';
360
- import { openrouter } from '@standardagents/openrouter';
268
+ import { openai } from '@standardagents/openai';
361
269
 
362
270
  export default defineModel({
363
- name: 'default',
364
- provider: openrouter,
365
- model: 'google/gemini-3-flash-preview',
366
- fallbacks: ['fast', 'cheap-heavy'],
271
+ name: 'fast',
272
+ provider: openai,
273
+ model: 'gpt-5.4-nano',
367
274
  });
368
275
  \`\`\`
369
276
 
370
- ## Provider Tools
371
-
372
- Providers can expose built-in tools such as search, file search, code execution,
373
- or provider-specific tools through their \`getTools()\` implementation. Enable the
374
- provider tool names on the model with \`providerTools\`; prompts can then include
375
- those tools like normal tool names. Provider tools execute on the upstream
376
- provider, not in AgentBuilder, and successful calls are logged as provider tool
377
- entries.
378
-
379
- OpenRouter exposes \`web_search\`, \`web_fetch\`, \`datetime\`, and \`image_generation\`
380
- as provider-executed server tools. Configure optional OpenRouter parameters with
381
- \`providerOptions.serverTools\` on the model.
382
-
383
- ## Recommended Models (OpenRouter)
384
-
385
- | Use Case | Model ID | Description |
386
- |----------|----------|-------------|
387
- | Fast/Cheap | \`z-ai/glm-4.5-air\` | Quick responses, low cost |
388
- | Mid-tier | \`google/gemini-3-flash-preview\` | Good balance of speed and quality |
389
- | Cheap Heavy | \`nvidia/nemotron-3-ultra-550b-a55b\` | Frontier open reasoning and orchestration, still affordable |
390
- | Heavy | \`google/gemini-3-pro-preview\` | Most capable, for complex tasks |
391
-
392
- **\u26A0\uFE0F Google Models**: When using Google models (\`google/gemini-*\`), you must enable \`reasoning\` in your prompt configuration or tool calls will fail. See the prompts AGENTS.md for details.
393
-
394
- ## Provider API Keys
395
-
396
- Set these environment variables in \`.dev.vars\` (local) or Cloudflare secrets (production):
397
-
398
- | Provider | Environment Variable |
399
- |----------|---------------------|
400
- | Cloudflare Workers AI | \`CLOUDFLARE_API_TOKEN\` + \`CLOUDFLARE_ACCOUNT_ID\` |
401
- | Cerebras | \`CEREBRAS_API_KEY\` |
402
- | DeepInfra | \`DEEPINFRA_API_KEY\` |
403
- | Groq | \`GROQ_API_KEY\` |
404
- | Novita AI | \`NOVITA_API_KEY\` |
405
- | OpenAI | \`OPENAI_API_KEY\` |
406
- | Anthropic | \`ANTHROPIC_API_KEY\` |
407
- | OpenRouter | \`OPENROUTER_API_KEY\` |
408
- | Baseten Model APIs | \`BASETEN_API_KEY\` |
409
- | Google | \`GOOGLE_API_KEY\` |
410
- | xAI | \`XAI_API_KEY\` |
411
- | TypeSafe AI (decisions) | \`TYPESAFE_AI_API_KEY\` |
412
-
413
- If \`STANDARD_AGENTS_API_KEY\` is present and a provider-specific key is not set,
414
- first-party Standard Agents providers route through the hosted Standard Agents
415
- router. Setting a provider-specific key always uses that provider directly.
416
- Hosted instances reach the router through the \`STANDARD_AGENTS_ROUTER\` service
417
- binding when the platform provides it, including from thread endpoints. If the
418
- router cannot be reached, request logs record \`error_origin: platform-router\`
419
- instead of blaming the model provider.
420
-
421
- ## Decision providers
422
-
423
- Automatic decision logging is an AgentBuilder feature, outside the spec contract.
424
-
425
- TypeSafe AI (\`@standardagents/typesafe\`) evaluates typed questions through
426
- \`state.decide({ provider: typesafe, model: 'jev-latest', context, questions })\`.
427
- It is a decision provider, so use a conversational provider for \`defineModel\`
428
- and prompt-backed sides. Configure \`TYPESAFE_AI_API_KEY\` for BYOK.
429
- Credentials may be scoped to a user/thread; destination URLs must come from
430
- trusted instance-admin or deployment configuration.
431
-
432
- Supply explicit string/object/array context and named \`choice\` questions with
433
- \`options\`, \`score\` questions with ordered \`levels\`, or \`boolean\` questions with
434
- optional true/false criteria. Write complete instructions and meaningful option
435
- or level descriptions. The provider maps context to native state, options/levels
436
- to criteria, and boolean to noul. Answers preserve distributions, legends, and
437
- confidence; boolean answers contain a 0\u20131 \`probability\` whose threshold you choose.
438
- Thread calls persist operation and cost logs even before the first message;
439
- use \`state.injectMessage()\` explicitly when a result belongs in history. See
440
- \`@standardagents/typesafe/README.md\` for the complete conversion contract.
441
-
442
- ## User-Defined Providers
443
-
444
- Create \`agents/providers/<name>.ts\` when a provider is compatible with an existing
445
- provider package but needs its own API key, endpoint, icon, model list, pricing, or
446
- other provider behavior.
277
+ This works in a platform-linked project as written. The project already installs \`@standardagents/openai\`, and the platform key in \`.dev.vars\` pays for the request.
447
278
 
448
- \`\`\`typescript
449
- // agents/providers/darkbloom.ts
450
- import { defineProvider, providerEnv } from '@standardagents/spec';
451
- import { openai } from '@standardagents/openai';
279
+ ## Providers this project has
452
280
 
453
- export default defineProvider({
454
- name: 'darkbloom',
455
- label: 'Darkbloom',
456
- baseProvider: openai,
457
- config: {
458
- apiKey: providerEnv('DARKBLOOM_API_KEY', { valueType: 'secret' }),
459
- baseUrl: providerEnv('DARKBLOOM_BASE_URL', {
460
- valueType: 'url',
461
- default: 'https://api.darkbloom.dev/v1',
462
- }),
463
- },
464
- });
281
+ \`package.json\` lists every provider package below as a dependency, so each import resolves without an install step.
282
+
283
+ | Provider | Package | Import | Your own key |
284
+ |----------|---------|--------|--------------|
285
+ | OpenAI | \`@standardagents/openai\` | \`openai\` | \`OPENAI_API_KEY\` |
286
+ | Anthropic | \`@standardagents/anthropic\` | \`anthropic\` | \`ANTHROPIC_API_KEY\` |
287
+ | Google | \`@standardagents/google\` | \`google\` | \`GOOGLE_API_KEY\` |
288
+ | OpenRouter | \`@standardagents/openrouter\` | \`openrouter\` | \`OPENROUTER_API_KEY\` |
289
+ | xAI | \`@standardagents/xai\` | \`xai\` | \`XAI_API_KEY\` |
290
+ | Groq | \`@standardagents/groq\` | \`groq\` | \`GROQ_API_KEY\` |
291
+ | Cerebras | \`@standardagents/cerebras\` | \`cerebras\` | \`CEREBRAS_API_KEY\` |
292
+ | DeepInfra | \`@standardagents/deepinfra\` | \`deepinfra\` | \`DEEPINFRA_API_KEY\` |
293
+ | Novita AI | \`@standardagents/novita\` | \`novita\` | \`NOVITA_API_KEY\` |
294
+ | Cloudflare Workers AI | \`@standardagents/cloudflare\` | \`cloudflare\` | \`CLOUDFLARE_API_TOKEN\` and \`CLOUDFLARE_ACCOUNT_ID\` |
295
+
296
+ If \`package.json\` does not list a provider package, add it with the project's package manager (for example \`pnpm add @standardagents/openai\`) and restart the dev server. A model whose provider package is missing fails with \`Cannot find module\`, and the builder UI shows that failure in a banner.
297
+
298
+ ## Keys
299
+
300
+ The project needs no provider account. When no key is set for a provider, requests to it use \`STANDARD_AGENTS_API_KEY\`, the platform account key that the CLI wrote to \`.dev.vars\`. The platform bills that usage to the account.
301
+
302
+ To use your own provider account, set the variable from the last column of the table in \`.dev.vars\`. A provider key always takes precedence over the platform key for that provider. For a deployed project, set the same variable in the project's settings on the platform.
303
+
304
+ A provider you define in \`agents/providers/\` always uses its own key. The platform key does not cover it.
305
+
306
+ A model whose provider has no key fails its run. The thread transcript names the variable to set and where to set it, and the Providers page in the builder UI marks the provider "Not configured". A missing key is not retried on the same model; fallback models on other providers still run. For other failures, the transcript shows the kind of failure and the thread's Logs tab holds the full error.
307
+
308
+ ## Choosing a model ID
309
+
310
+ Model IDs change often. Take them from the provider's published model list instead of writing them from memory. Two commands help:
311
+
312
+ \`\`\`bash
313
+ npx @standardagents/cli@latest current-models
314
+ npx @standardagents/cli@latest available-models --provider=openai
465
315
  \`\`\`
466
316
 
317
+ The first prints a current guide to which models suit which work. The second lists every ID a provider serves, and needs that provider's own key in \`.dev.vars\`; the platform key alone is not enough for it.
318
+
319
+ OpenRouter IDs include the lab, such as \`google/gemini-2.5-flash\`. Other providers use the lab's own ID, such as \`gpt-5.4-mini\`.
320
+
321
+ ## Fallbacks
322
+
323
+ \`fallbacks\` names other models defined in this project. Each must exist in \`agents/models/\`.
324
+
467
325
  \`\`\`typescript
468
- // agents/models/darkbloom_chat.ts
326
+ // agents/models/reliable.ts
469
327
  import { defineModel } from '@standardagents/builder';
470
- import darkbloom from '../providers/darkbloom';
328
+ import { openrouter } from '@standardagents/openrouter';
471
329
 
472
330
  export default defineModel({
473
- name: 'darkbloom_chat',
474
- provider: darkbloom,
475
- model: 'darkbloom-chat',
331
+ name: 'reliable',
332
+ provider: openrouter,
333
+ model: 'google/gemini-2.5-flash',
334
+ fallbacks: ['fast'],
476
335
  });
477
336
  \`\`\`
478
337
 
479
- Provider \`overrides\` can replace behavior such as \`getModels\`, \`getIcon\`,
480
- \`getTools\`, pricing/capability lookup, \`generate\`, or \`stream\`. Call \`next()\`
481
- inside an override to compose with the base provider; return your own value to
482
- replace it.
338
+ A failed request is retried once on the same model, then each fallback is tried in order with the same two attempts. Network errors, rate limits (429), server errors (5xx), and authentication errors (401) trigger a retry.
483
339
 
484
- ## Fallback Strategy
340
+ ## Provider tools
485
341
 
486
- When a model fails, fallbacks are tried in order:
487
- 1. Primary model (2 attempts)
488
- 2. First fallback (2 attempts)
489
- 3. Second fallback (2 attempts)
490
- 4. ...and so on
491
-
492
- Retries occur on: network errors, rate limits (429), server errors (5xx), auth errors (401).
493
-
494
- ## OpenRouter Configuration
495
-
496
- For OpenRouter, use the full model path:
342
+ Some providers run tools on their own servers, such as web search. List the ones a model should offer in \`providerTools\`. A prompt that uses the model can then put those names in its \`tools\`.
497
343
 
498
344
  \`\`\`typescript
345
+ // agents/models/researcher.ts
499
346
  import { defineModel } from '@standardagents/builder';
500
347
  import { openrouter } from '@standardagents/openrouter';
501
348
 
502
349
  export default defineModel({
503
- name: 'heavy',
350
+ name: 'researcher',
504
351
  provider: openrouter,
505
- model: 'google/gemini-3-pro-preview',
506
- includedProviders: ['google'], // Prefer Google's servers
352
+ model: 'google/gemini-2.5-flash',
353
+ providerTools: ['web_search', 'web_fetch'],
507
354
  });
508
355
  \`\`\`
509
356
 
510
- ## Best Practices
357
+ OpenRouter offers \`web_search\`, \`web_fetch\`, \`datetime\`, and \`image_generation\`. Provider tools run at the provider, and their calls appear in the thread's logs.
358
+
359
+ ## Custom providers
511
360
 
512
- - **Name by use case**, not model ID (e.g., \`fast\`, \`default\`, \`heavy\`)
513
- - **Configure fallbacks** for production reliability
514
- - **Set pricing** for cost tracking in logs when your provider or runtime does not supply it automatically
515
- - **Cerebras note:** documented Cerebras models attach provider-owned \`usage.cost\` values for request logs, but custom/private Cerebras models should still set \`inputPrice\` and \`outputPrice\`
516
- - **DeepInfra note:** the provider reads live pricing from \`/v1/models\` and preserves response-level \`usage.estimated_cost\`; custom catalog endpoints can be configured with the provider's \`modelsUrl\` slot
517
- - **Novita note:** the Novita provider reads live catalog pricing from \`/openai/v1/models\`, including cache-read prices when available, and prices tiered catalog entries (such as MiniMax M3, which doubles from 524,288 prompt tokens) at the tier that matches each request's prompt size; custom/private Novita models should still set \`inputPrice\`, \`outputPrice\`, and \`cachedPrice\`
518
- - **Use environment variables** for API keys, never hardcode
361
+ To reach an API that is compatible with an installed provider package but has its own endpoint and key, define a provider in \`agents/providers/\` and import it here. See \`agents/providers/AGENTS.md\`.
362
+
363
+ ## Practices
364
+
365
+ - Name a model for its job (\`fast\`, \`default\`, \`researcher\`), so a prompt does not change when the model ID does.
366
+ - Set \`inputPrice\` and \`outputPrice\` for a custom or private model. First-party providers report cost for the models they list.
367
+ - Keep keys in \`.dev.vars\` and in platform settings. Never write a key in a source file.
519
368
 
520
369
  ## Documentation
521
370
 
@@ -523,301 +372,266 @@ Full reference: https://docs.standardagentbuilder.com/api-reference/define/model
523
372
  `;
524
373
 
525
374
  // src/docs/providers.ts
526
- var PROVIDERS_AGENTS_MD = `# Provider Definitions
375
+ var PROVIDERS_AGENTS_MD = `# Provider definitions
527
376
 
528
- Provider definitions create named provider factories that wrap a base provider
529
- package with connection config and optional method overrides.
377
+ Most projects never need this directory. The provider packages listed in \`agents/models/AGENTS.md\` already cover the major model providers.
530
378
 
531
- Use \`defineProvider()\` and export the result as the default export:
379
+ Define a provider when an API is compatible with one of those packages but has its own endpoint, key, or behavior. A provider definition wraps a base provider package with different connection settings.
380
+
381
+ ## Example
532
382
 
533
383
  \`\`\`typescript
534
- import { defineProvider, providerEnv, providerValue } from '@standardagents/spec';
384
+ // agents/providers/acme.ts
385
+ import { defineProvider, providerEnv, providerValue } from '@standardagents/builder';
535
386
  import { openai } from '@standardagents/openai';
536
387
 
537
388
  export default defineProvider({
538
- name: 'my_openai_compatible_provider',
539
- label: 'My OpenAI-compatible Provider',
389
+ name: 'acme',
390
+ label: 'Acme AI',
540
391
  baseProvider: openai,
541
392
  config: {
542
- apiKey: providerEnv('MY_PROVIDER_API_KEY', {
393
+ apiKey: providerEnv('ACME_API_KEY', {
543
394
  valueType: 'secret',
544
395
  required: true,
545
- description: 'Provider API key.',
396
+ description: 'Acme AI API key.',
546
397
  }),
547
- baseUrl: providerValue('https://api.example.com/v1'),
398
+ baseUrl: providerValue('https://api.acme.example/v1'),
548
399
  },
549
400
  });
550
401
  \`\`\`
551
402
 
552
- Models can then import the provider definition and use it like any other provider
553
- factory:
403
+ A model imports the provider as a default import:
554
404
 
555
405
  \`\`\`typescript
406
+ // agents/models/acme_chat.ts
556
407
  import { defineModel } from '@standardagents/builder';
557
- import myProvider from '../providers/my_openai_compatible_provider';
408
+ import acme from '../providers/acme';
558
409
 
559
410
  export default defineModel({
560
- name: 'custom_fast',
561
- provider: myProvider,
562
- model: 'gpt-4o-mini',
411
+ name: 'acme_chat',
412
+ provider: acme,
413
+ model: 'acme-chat-1',
563
414
  });
564
415
  \`\`\`
565
416
 
566
- Provider \`overrides\` can replace behavior such as \`getModels\`, \`getIcon\`,
567
- \`getTools\`, pricing/capability lookup, \`generate\`, or \`stream\`. Call \`next()\`
568
- inside an override to compose with the base provider; return your own value to
569
- replace it.
417
+ Replace the endpoint, the model ID, and the variable name with the real ones for the API you are connecting. Set \`ACME_API_KEY\` in \`.dev.vars\` for local work and in the project's platform settings for a deployed project.
570
418
 
571
- Keep credentials in secret env values. Do not hardcode API keys in provider
572
- definition files.
419
+ ## Configuration values
573
420
 
574
- ## Error attribution
421
+ - \`providerEnv(name, options)\` reads a variable at run time. Use it for keys and for values that differ between environments. \`options\` accepts \`valueType\` (\`'secret'\`, \`'url'\`, or \`'text'\`), \`required\`, \`default\`, and \`description\`.
422
+ - \`providerValue(value)\` is a constant written in the file. Never use it for a key.
575
423
 
576
- When an override catches and rethrows an upstream failure, preserve structured
577
- attribution with \`ProviderError\` instead of putting the provider name only in
578
- the message:
424
+ A custom provider always uses the key it declares. The platform account key does not cover it.
579
425
 
580
- \`\`\`typescript
581
- import { ProviderError } from '@standardagents/builder';
426
+ ## Overrides
427
+
428
+ \`overrides\` can replace or wrap methods of the base provider, such as \`getModels\`, \`getIcon\`, \`getTools\`, \`generate\`, and \`stream\`. Inside an override, call \`next()\` to run the base provider's version and return your own value to replace it.
582
429
 
430
+ When an override catches an upstream failure and rethrows it, throw a \`ProviderError\` from \`@standardagents/builder\` so the thread's logs name the failing service:
431
+
432
+ \`\`\`text
583
433
  throw new ProviderError(message, 'server_error', status, retryAfter, {
584
434
  cause: error,
585
- origin: 'provider:my_openai_compatible_provider',
586
- upstream: 'example-backend',
435
+ origin: 'provider:acme',
436
+ upstream: 'acme-backend',
587
437
  });
588
438
  \`\`\`
589
439
 
590
- \`origin\` is machine-readable (\`provider:<id>\` for provider failures), while
591
- \`upstream\` names the actual backend when it differs from the adapter. Builder
592
- request logs retain these fields and render them separately from the human
593
- message. Do not make operators infer the failing layer from prose.
440
+ \`origin\` identifies the provider as \`provider:<name>\`. \`upstream\` names the backend when it differs from the provider.
441
+
442
+ ## Documentation
443
+
444
+ Full reference: https://docs.standardagentbuilder.com/api-reference/define/model
594
445
  `;
595
446
 
596
447
  // src/docs/prompts.ts
597
- var PROMPTS_AGENTS_MD = `# Prompt Definitions
448
+ var PROMPTS_AGENTS_MD = `# Prompt definitions
598
449
 
599
- Prompts define system instructions, model selection, and available tools for LLM interactions.
450
+ A prompt holds the system instructions for a model, selects the model, and lists the tools the model may call. An agent side that is backed by a prompt runs it on every turn.
600
451
 
601
452
  ## Properties
602
453
 
603
454
  | Property | Type | Required | Description |
604
455
  |----------|------|----------|-------------|
605
- | \`name\` | \`string\` | Yes | Unique identifier for this prompt |
606
- | \`toolDescription\` | \`string\` | Yes | Description shown when used as a tool |
607
- | \`prompt\` | \`string \\| StructuredPrompt\` | Yes | System instructions |
608
- | \`model\` | \`string\` | Yes | Model name to use (references \`agents/models/\`) |
609
- | \`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 |
610
- | \`includeChat\` | \`boolean\` | No | Include full conversation history |
611
- | \`includePastTools\` | \`boolean\` | No | Include previous tool results |
612
- | \`historyLimit\` | \`number\` | No | With \`includeChat\`, load only the newest top-level messages: at least N and under 2N, the cut moving N at a time (prompt-cache stable). Execution history and \`filter_messages\` see only that window; stored history is untouched |
613
- | \`parallelToolCalls\` | \`boolean\` | No | Allow multiple concurrent tool calls |
614
- | \`toolChoice\` | \`'auto' \\| 'none' \\| 'required'\` | No | Tool calling strategy |
615
- | \`requiredSchema\` | \`z.ZodObject\` | No | Input validation when called as tool |
616
- | \`variables\` | \`VariableDefinition[]\` | No | Variable declarations (\`name\`, \`type\`, \`required\`, \`scoped\`, \`description\`) |
617
- | \`env\` | \`Record<string, string>\` | No | Prompt-level default values for variables |
618
- | \`hooks\` | \`string[]\` | No | Hook IDs to run for this prompt (overrides agent hooks) |
619
- | \`reasoning\` | \`ReasoningConfig\` | No | Extended thinking configuration |
620
- | \`providerOptions\` | \`Record<string, unknown>\` | No | Provider-specific request options |
621
-
622
- ## Basic Example
456
+ | \`name\` | \`string\` | Yes | The name agents and other prompts use to refer to this prompt |
457
+ | \`toolDescription\` | \`string\` | Yes | What this prompt does. Shown to a model that can call it as a tool |
458
+ | \`prompt\` | \`string\` or parts array | Yes | The system instructions |
459
+ | \`model\` | \`string\` | Yes | The \`name\` of a model in \`agents/models/\` |
460
+ | \`tools\` | array | No | Names of tools, other prompts, provider tools, and agents exposed as tools |
461
+ | \`includeChat\` | \`boolean\` | No | Send the conversation history with each request. Default \`false\` |
462
+ | \`includePastTools\` | \`boolean\` | No | Send earlier tool calls and their results with each request. Default \`false\` |
463
+ | \`historyLimit\` | \`number\` | No | With \`includeChat\`, send only the newest messages: at least this many and fewer than twice as many |
464
+ | \`parallelToolCalls\` | \`boolean\` | No | Let the model call several tools in one step. Default \`false\` |
465
+ | \`toolChoice\` | \`'auto'\`, \`'none'\`, or \`'required'\` | No | Whether the model may, must not, or must call a tool |
466
+ | \`requiredSchema\` | Zod object | No | Arguments this prompt takes when it is called as a tool or started as a subagent |
467
+ | \`variables\` | array | No | Variables this prompt needs: \`name\`, \`type\`, \`required\`, \`scoped\`, \`description\` |
468
+ | \`env\` | \`Record<string, string>\` | No | Default values for variables |
469
+ | \`hooks\` | \`string[]\` | No | Hook IDs to run for this prompt. Replaces the agent's hooks |
470
+ | \`reasoning\` | \`object\` | No | Reasoning settings: \`level\`, \`maxTokens\`, \`exclude\` |
471
+ | \`providerOptions\` | \`object\` | No | Request options for the model's provider |
472
+
473
+ ## A prompt with tools
623
474
 
624
475
  \`\`\`typescript
476
+ // agents/prompts/customer_support.ts
625
477
  import { definePrompt } from '@standardagents/builder';
626
478
 
627
479
  export default definePrompt({
628
480
  name: 'customer_support',
629
- toolDescription: 'Handle customer support inquiries',
481
+ toolDescription: 'Answer customer support questions',
630
482
  model: 'default',
631
- prompt: \`You are a helpful customer support agent.
632
- Always be polite and professional.
633
- If you cannot help, escalate to a human.\`,
634
- tools: ['search_knowledge_base', 'create_ticket'],
635
- includeChat: true,
636
- });
637
- \`\`\`
638
-
639
- ## Structured Prompts
483
+ prompt: \`You are a customer support agent for an online store.
640
484
 
641
- Include other prompts for reusable instruction blocks:
642
-
643
- \`\`\`typescript
644
- export default definePrompt({
645
- name: 'main_assistant',
646
- toolDescription: 'Primary assistant',
647
- model: 'default',
648
- prompt: [
649
- { type: 'text', content: 'You are a helpful assistant.\\n\\n' },
650
- { type: 'include', prompt: 'common_rules' }, // Includes another prompt
651
- { type: 'env', property: 'ASSISTANT_TONE' }, // Inject variable value
652
- { type: 'text', content: '\\n\\nBe concise.' },
653
- ],
485
+ Search the knowledge base before you answer a product question.
486
+ If the knowledge base does not cover the question, create a ticket and tell the customer a person will follow up.
487
+ When the customer confirms the issue is resolved, close the conversation with a one-sentence summary.\`,
488
+ tools: ['search_knowledge_base', 'create_ticket', 'close_conversation'],
489
+ includeChat: true,
490
+ includePastTools: true,
654
491
  });
655
492
  \`\`\`
656
493
 
657
- A circular prompt include raises an error that names the include chain. A prompt can include the same partial through different paths.
658
-
659
- \`type: 'env'\` inserts a resolved thread variable value. Use this only for non-secret values that are safe to expose to the LLM.
660
-
661
- The sandboxed coding prompt declares \`HTTP_HOST\` as a text variable and injects its resolved value with a structured \`{ type: 'env', property: 'HTTP_HOST' }\` prompt part. Its URL helper uses that value as the origin for files exposed from the thread \`public/\` directory and endpoints exposed from the thread \`api/\` directory. Public URLs should usually omit extensions; extensionless public paths resolve \`.html\`, then \`.js\`, then \`.ts\`, then matching \`index\` files in the same order. Browser pages should reference only public files, \`api/\` endpoints, or absolute external URLs, using relative asset URLs like \`./app\` and \`./api/todos\`; private \`src/\` files are not served directly. Public \`.ts\` files are transformed to browser JavaScript and are not server endpoints. Server-side endpoints live under the thread filesystem \`api/\` directory, and the \`api/\` directory name stays in the public URL: \`api/todo.ts\` is \`/api/threads/{threadId}/api/todo\`, while \`api/todo.ts\` is not \`/api/threads/{threadId}/todo\` or \`/api/threads/{threadId}/todos\`. Endpoint files default-export a function that accepts \`Request\` and returns \`Response\` or \`Promise<Response>\`. Endpoint code can import \`fetch\`, \`fs\`, and \`storage\`, and can import private helpers with relative paths such as \`../src/todo.ts\`. When the coding agent creates a site, page, or endpoint, it should call \`get_public_url\` with the created file path and use the returned URL.
494
+ The model \`default\` is defined in the root \`AGENTS.md\`. The three tools are defined in \`agents/tools/AGENTS.md\`.
662
495
 
663
- The sandboxed coding prompt includes \`set_goal\` (write the whole goal: a short \`summary\`, a \`description\` that is the user's objective in their own words captured verbatim, plus ordered steps) and \`update_goal_step\` (flip one step's status as you start/finish it), and should check work incrementally, checking steps off as it goes and finishing with a summary once every step is done. \`coding_validate_typescript\` is only an early signal; for real verification, call helper exports with \`coding_run\`, get public URLs for pages or endpoints, then probe those URLs from \`coding_run\` using \`import { fetch } from "fetch"\`. Side-effecting endpoint checks should use disposable data and clean up through the endpoint, storage, or file cleanup path when practical. The prompt also includes \`fetch_url\` for reading known external public http(s) pages; use the \`coding_run\` fetch bridge for custom methods, headers, request bodies, or thread endpoint checks.
496
+ ## History settings
664
497
 
665
- Sandboxed code and \`api/\` endpoints can persist small JSON-serializable runtime data through \`import { storage } from "storage"\`. Use \`storage.get\`, \`storage.set\`, \`storage.delete\`, and \`storage.has\` with stable logical keys. Do not expose, mention, or depend on internal storage key prefixes.
498
+ A prompt sees only its system instructions and the newest message unless you turn history on.
666
499
 
667
- The sandboxed coding prompt includes \`delete_file\` for removing files or empty directories from the thread filesystem. Use it to clean up obsolete implementation files, stale public assets, and abandoned endpoint routes after the replacement is working.
500
+ - Set \`includeChat: true\` for any prompt that holds a conversation.
501
+ - Set \`includePastTools: true\` when the model must read a tool's result to continue, which is the usual case for a prompt with tools. Without it, each request omits earlier tool calls and results, and the model may repeat a call until it reaches its step limit.
502
+ - Leave \`includePastTools\` off when the tools only cause side effects or when their output is large or sensitive.
668
503
 
669
- ## Tool Configuration
504
+ ## Reusing instructions
670
505
 
671
- Tools can be simple names or detailed configs:
506
+ \`prompt\` can be an array of parts. An \`include\` part inserts another prompt's instructions. An \`env\` part inserts the value of a variable.
672
507
 
673
508
  \`\`\`typescript
674
- tools: [
675
- 'simple_tool', // Just the tool name
676
- {
677
- name: 'complex_tool',
678
- includeTextResponse: true, // Include sub-prompt text in result
679
- includeToolCalls: false, // Exclude sub-prompt tool calls
680
- includeErrors: true, // Include error details
681
- initUserMessageProperty: 'query', // Use this arg as initial message
682
- },
683
- ],
684
- \`\`\`
685
-
686
- Provider-built-in tools use the same \`tools\` list once the selected model enables
687
- them with \`providerTools\`. AgentBuilder passes them to the provider and records
688
- provider-reported calls in request logs; it does not execute those tools locally.
689
- Generated prompt files may store these selections as \`provider:<toolName>\` to
690
- distinguish provider tools from local tools with the same name.
691
-
692
- ### Subagents Come From the Side, Not the Tools Array
509
+ // agents/prompts/house_rules.ts
510
+ import { definePrompt } from '@standardagents/builder';
693
511
 
694
- 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:
512
+ export default definePrompt({
513
+ name: 'house_rules',
514
+ toolDescription: 'Shared rules for every customer-facing prompt',
515
+ model: 'default',
516
+ prompt: 'Never share one customer\\'s information with another. Never promise a refund; create a ticket instead.',
517
+ });
518
+ \`\`\`
695
519
 
696
520
  \`\`\`typescript
697
- // agents/agents/assistant_agent.ts
698
- sideA: {
699
- prompt: 'coordinator',
700
- subagents: [
701
- { agent: 'calendar_agent', as: 'work_calendar', scopedEnv: ['WORK_CAL_ID'] },
702
- { agent: 'calendar_agent', as: 'personal_calendar', scopedEnv: ['PERSONAL_CAL_ID'] },
703
- { agent: 'research_agent', blocking: true, maxInstances: 1 },
704
- { agent: 'compaction_agent', hidden: true },
705
- { agent: 'browser_agent', optional: 'ENABLE_BROWSER_AGENT' },
521
+ // agents/prompts/billing_support.ts
522
+ import { definePrompt } from '@standardagents/builder';
523
+
524
+ export default definePrompt({
525
+ name: 'billing_support',
526
+ toolDescription: 'Answer billing questions',
527
+ model: 'default',
528
+ prompt: [
529
+ { type: 'text', content: 'You answer billing questions for an online store.\\n\\n' },
530
+ { type: 'include', prompt: 'house_rules' },
531
+ { type: 'text', content: '\\n\\nReply in this tone: ' },
532
+ { type: 'env', property: 'SUPPORT_TONE' },
706
533
  ],
707
- },
534
+ variables: [
535
+ { name: 'SUPPORT_TONE', type: 'text', required: false, scoped: false, description: 'Tone of voice for replies' },
536
+ ],
537
+ env: { SUPPORT_TONE: 'friendly and brief' },
538
+ tools: ['create_ticket'],
539
+ includeChat: true,
540
+ includePastTools: true,
541
+ });
708
542
  \`\`\`
709
543
 
710
- For a prompt-backed side with non-hidden relationships, the runtime automatically injects three lifecycle tools scoped to that side's declarations:
711
-
712
- - \`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.
713
- - \`subagent_create\` \u2014 creates (or continues) an instance. Its \`agent\` argument enumerates the side's enabled relationship *names* (\`as\`, defaulting to the unqualified part of \`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.
714
- - \`subagent_message\` \u2014 queues a message to an existing instance by reference.
715
-
716
- These three names are reserved; a relationship \`as\` that collides with them (or with any other tool name) is a definition-time error.
544
+ An \`env\` part puts the value in front of the model. Use it only for values that are safe for the model and the user to see. A prompt that includes itself, directly or through another prompt, fails to load with an error that names the chain.
717
545
 
718
- \`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.
546
+ ## Prompts as tools
719
547
 
720
- \`subagent_create\` accepts \`agent\`, \`name\`, and \`arguments\`.
721
- The relationship maps its initial message and attachments from \`arguments\`.
722
- Create-or-continue applies the same mappings to the existing child.
723
- Without a mapped message value, Builder uses the optional top-level \`message\` before falling back to serialized arguments.
724
- Empty arguments produce the JSON activation payload {}.
725
- The relationship contributes missing message and attachment fields to the model-facing argument schema.
726
- The model-facing schema and runtime accept optional top-level \`message\` and \`attachments\`.
727
- These fields are AgentBuilder extensions beyond Spec 0.2.0 Subagents \xA75 for existing callers such as Standard Code Mux.
728
- Relationships should migrate to \`initUserMessageProperty\` and mapped \`arguments\`.
729
- Strict providers may require these keys on the wire with null values to omit them.
730
- Mapped arguments take precedence over these compatibility fields.
731
- Relationship-only fields initialize messages without becoming persistent child arguments.
732
-
733
- 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.
734
-
735
- Blocking creation and continuation return the session result through the tool result.
736
- That session has no implicit completion message.
737
- Create-or-continue acquires child execution before persisting its input.
738
- Admission failure leaves the continuation input unsent.
739
- A separate \`subagent_message\` activation retains implicit delivery under \`parentCommunication: 'implicit'\`.
740
-
741
- 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\`.
742
-
743
- See \`agents/agents/AGENTS.md\` for the full \`SubagentRelationship\` option list (\`immediate\`, \`optional\`, \`hidden\`, \`parentCommunication\`, init mappings) and the ownership rules.
744
-
745
- ## Input Validation
746
-
747
- Validate inputs when prompt is called as a tool:
548
+ Any prompt can be listed in another prompt's \`tools\`. The calling model sees \`toolDescription\` and the arguments in \`requiredSchema\`, and gets the prompt's answer as the tool result. The called prompt runs once, with its own instructions and model.
748
549
 
749
550
  \`\`\`typescript
551
+ // agents/prompts/order_extractor.ts
552
+ import { definePrompt } from '@standardagents/builder';
750
553
  import { z } from 'zod';
751
554
 
752
555
  export default definePrompt({
753
- name: 'data_extractor',
754
- toolDescription: 'Extract structured data from text',
556
+ name: 'order_extractor',
557
+ toolDescription: 'Extract the order number and the problem from a customer message',
755
558
  model: 'default',
756
- prompt: 'Extract the requested data from the input.',
559
+ prompt: 'Extract the order number and a one-sentence description of the problem. Reply with JSON: {"orderNumber": string | null, "problem": string}.',
757
560
  requiredSchema: z.object({
758
- text: z.string().describe('Text to extract from'),
759
- fields: z.array(z.string()).describe('Fields to extract'),
561
+ message: z.string().describe('The customer message to read'),
760
562
  }),
761
563
  });
762
564
  \`\`\`
763
565
 
764
- Provider options merge from model defaults to prompt overrides. Later values win.
566
+ \`\`\`typescript
567
+ // agents/prompts/returns_support.ts
568
+ import { definePrompt } from '@standardagents/builder';
569
+
570
+ export default definePrompt({
571
+ name: 'returns_support',
572
+ toolDescription: 'Handle return requests',
573
+ model: 'default',
574
+ prompt: 'You handle return requests. Use order_extractor on the first message, then create a ticket for the return.',
575
+ tools: [
576
+ { name: 'order_extractor', includeTextResponse: true, initUserMessageProperty: 'message' },
577
+ 'create_ticket',
578
+ ],
579
+ includeChat: true,
580
+ includePastTools: true,
581
+ });
582
+ \`\`\`
583
+
584
+ The object form configures a prompt used as a tool:
765
585
 
766
- ## Extended Thinking (Reasoning)
586
+ | Option | Meaning |
587
+ |--------|---------|
588
+ | \`includeTextResponse\` | Return the prompt's text answer in the tool result |
589
+ | \`includeToolCalls\` | Return the tool calls the prompt made |
590
+ | \`includeErrors\` | Return errors the prompt hit |
591
+ | \`initUserMessageProperty\` | The argument to send as the prompt's user message |
592
+ | \`initAttachmentsProperty\` | The argument that holds file paths to attach |
767
593
 
768
- A prompt can set \`reasoning.level\` from 0 to 100. An unset level uses the model's default. Level 0 disables reasoning when the model supports a disabling control. Otherwise, the provider selects the lowest reasoning setting. A model with no reasoning control receives no control. Each provider maps the level to the nearest option the model declares.
594
+ ## Other entries in \`tools\`
769
595
 
770
- A prompt's \`reasoning.level\` replaces a native effort value in \`providerOptions\`. Other \`providerOptions\` keys remain. Without a level, \`providerOptions\` apply as provided. Native effort values in a model's \`providerOptions\` remain valid.
596
+ - **Provider tools.** When the prompt's model lists \`providerTools\`, put those names in \`tools\`. See \`agents/models/AGENTS.md\`.
597
+ - **Agents.** An agent with \`exposeAsTool: true\` can be listed by name. Calling it hands the thread to that agent.
598
+ - **Subagents are not listed here.** They are declared on the agent side, in \`subagents\`. For a prompt on such a side, the runtime adds three tools itself: \`subagents\`, \`subagent_create\`, and \`subagent_message\`. See \`agents/agents/AGENTS.md\`.
771
599
 
772
- Saved prompt input with \`reasoning.effort\` fails validation. A published package prompt with \`reasoning.effort\` loads with a warning. The runtime ignores that field.
600
+ ## Reasoning
773
601
 
774
- Enable for complex reasoning tasks:
602
+ \`reasoning.level\` runs from 0 to 100. Each provider maps it to the nearest setting the model has. Leave it unset to use the model's default. Level 0 turns reasoning off when the model allows that.
775
603
 
776
604
  \`\`\`typescript
605
+ // agents/prompts/escalation_review.ts
606
+ import { definePrompt } from '@standardagents/builder';
607
+
777
608
  export default definePrompt({
778
- name: 'code_reviewer',
779
- toolDescription: 'Review code for issues',
780
- model: 'heavy',
781
- prompt: 'Review the code thoroughly...',
609
+ name: 'escalation_review',
610
+ toolDescription: 'Decide whether a support conversation needs a manager',
611
+ model: 'default',
612
+ prompt: 'Read the conversation and decide whether a manager must step in. Answer "escalate" or "continue" with one sentence of reasoning.',
613
+ includeChat: true,
782
614
  reasoning: {
783
- level: 100, // 0 to 100
784
- maxTokens: 16000, // Max thinking tokens
785
- exclude: true, // Don't include thinking in response
615
+ level: 66,
616
+ exclude: true,
786
617
  },
787
618
  });
788
619
  \`\`\`
789
620
 
790
- **\u26A0\uFE0F Google Models Require Reasoning**: When using Google models (\`google/gemini-*\`) via OpenRouter, you **must** include \`reasoning\` configuration or tool calls will fail. At minimum:
621
+ \`exclude: true\` keeps the reasoning text out of the reply. \`maxTokens\` caps reasoning tokens for models that take a budget. Do not set \`reasoning.effort\`; it is rejected.
791
622
 
792
- \`\`\`typescript
793
- reasoning: {
794
- level: 33, // 0 to 100
795
- },
796
- \`\`\`
623
+ Google models reached through OpenRouter (\`google/gemini-*\`) need a reasoning level before they call tools reliably. Set \`reasoning: { level: 33 }\` or higher on a prompt that gives one of those models tools.
797
624
 
798
625
  ## Hooks
799
626
 
800
- Attach lifecycle hooks to a prompt:
801
-
802
- \`\`\`typescript
803
- export default definePrompt({
804
- name: 'customer_support',
805
- toolDescription: 'Handle customer support',
806
- model: 'default',
807
- prompt: 'You are a support agent...',
808
- hooks: ['limit_to_20', 'redact_pii'], // Hook IDs from agents/hooks/
809
- });
810
- \`\`\`
627
+ \`hooks\` lists hook IDs from \`agents/hooks/\` that run for this prompt. When a prompt lists hooks, the agent's hooks do not run for it. See \`agents/hooks/AGENTS.md\`.
811
628
 
812
- Prompt hooks take precedence over agent hooks. See \`agents/hooks/AGENTS.md\` for details.
629
+ ## Practices
813
630
 
814
- ## Best Practices
815
-
816
- - **Write clear instructions** - Structure with headers and bullet points
817
- - **Describe tools thoroughly** - LLMs decide based on descriptions
818
- - **Validate inputs with Zod** - Catch errors early
819
- - **Use \`includeChat\`** for conversational context
820
- - **Name by purpose**, not model (e.g., \`support_agent\` not \`gpt4_prompt\`)
631
+ - Write instructions for the realistic cases, including what to do when a tool fails or the answer is unknown.
632
+ - Tell the model when to use each tool. The model decides from the tool's description and your instructions.
633
+ - Name a prompt for its job (\`customer_support\`), not its model.
634
+ - Keep one job per prompt. Give a second job to a second prompt and call it as a tool.
821
635
 
822
636
  ## Documentation
823
637
 
@@ -825,273 +639,315 @@ Full reference: https://docs.standardagentbuilder.com/api-reference/define/promp
825
639
  `;
826
640
 
827
641
  // src/docs/agents.ts
828
- var AGENT_DEFINITIONS_AGENTS_MD = `# Agent Definitions
829
-
830
- 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.
642
+ var AGENT_DEFINITIONS_AGENTS_MD = `# Agent definitions
831
643
 
832
- | Backing | Who takes this side's turns | Declared as |
833
- |---------|-----------------------------|-------------|
834
- | **Human** | A person, via whatever interface the runtime provides | \`{ human: true }\` |
835
- | **Prompt** | A model, driven by a prompt from \`agents/prompts/\` | \`{ prompt: 'name', ... }\` |
836
- | **Handler** | Deterministic code from \`agents/handlers/\` | \`{ handler: 'name', ... }\` |
644
+ An agent is a conversation between two sides, \`sideA\` and \`sideB\`. Each side is backed by exactly one of three things:
837
645
 
838
- 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 }\`.
646
+ | Backing | Who takes the side's turns | Written as |
647
+ |---------|----------------------------|------------|
648
+ | Human | A person, through the builder UI or your own client | \`{ human: true }\` |
649
+ | Prompt | A model, running a prompt from \`agents/prompts/\` | \`{ prompt: 'name' }\` |
650
+ | Handler | Code from \`agents/handlers/\` | \`{ handler: 'name' }\` |
839
651
 
840
- ## Agent Properties
652
+ Write both sides. There is no default for a missing side. Side A is the \`assistant\` role in the message history and side B is the \`user\` role.
841
653
 
842
- | Property | Type | Required | Description |
843
- |----------|------|----------|-------------|
844
- | \`name\` | \`string\` | Yes | Unique identifier for this agent |
845
- | \`sideA\` | \`SideConfig\` | Yes | Side A configuration (backing-discriminated) |
846
- | \`sideB\` | \`SideConfig\` | Yes | Side B configuration (backing-discriminated) |
847
- | \`maxSessionTurns\` | \`number\` | No | Session exchange safety cap \u2014 optional on every agent shape |
848
- | \`title\` | \`string\` | No | Human-readable label |
849
- | \`description\` | \`string\` | No | Agent summary |
850
- | \`icon\` | \`string\` | No | Display icon (see Icons below) |
851
- | \`exposeAsTool\` | \`boolean\` | No | Expose a callable entrypoint for handoffs and composition |
852
- | \`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 |
853
- | \`toolDescription\` | \`string\` | No | Default model-facing description when this agent is composed or handed off to (required if exposeAsTool) |
854
- | \`env\` | \`Record<string, string>\` | No | Agent-level default variable values |
855
- | \`hooks\` | \`string[]\` | No | Hook IDs to run for this agent (fallback if prompt has no hooks) |
856
- | \`parentHooks\` | \`ParentHookRequirement[]\` | No | Required hooks automatically contributed to a direct parent composition (legal on any exposed agent) |
857
- | \`tags\` | \`string[]\` | No | Tags for categorization and filtering |
654
+ ## A chat assistant
858
655
 
859
- ## Side Configuration
656
+ A person on side B talks to a model on side A.
860
657
 
861
- A side's configuration is discriminated by its backing: exactly one of \`human\`, \`prompt\`, or \`handler\` must be present.
658
+ \`\`\`typescript
659
+ // agents/agents/support_agent.ts
660
+ import { defineAgent } from '@standardagents/builder';
862
661
 
863
- **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.
662
+ export default defineAgent({
663
+ name: 'support_agent',
664
+ title: 'Customer Support',
665
+ description: 'Answers customer questions from the knowledge base and opens tickets.',
666
+ sideA: {
667
+ label: 'Support',
668
+ prompt: 'customer_support',
669
+ sessionStop: { name: 'close_conversation', messageProperty: 'summary' },
670
+ maxSteps: 30,
671
+ },
672
+ sideB: { label: 'Customer', human: true },
673
+ hooks: ['log_tool_use'],
674
+ });
675
+ \`\`\`
864
676
 
865
- **Prompt side**
677
+ The prompt \`customer_support\` is in \`agents/prompts/AGENTS.md\`, the tool \`close_conversation\` is in \`agents/tools/AGENTS.md\`, and the hook \`log_tool_use\` is in \`agents/hooks/AGENTS.md\`.
866
678
 
867
- | Property | Type | Required | Description |
868
- |----------|------|----------|-------------|
869
- | \`prompt\` | \`string\` | Yes | Prompt name to use (references \`agents/prompts/\`) |
870
- | \`label\` | \`string\` | No | Display name for this side |
871
- | \`subagents\` | \`SubagentRelationship[]\` | No | Side-owned child relationships (see Subagent Relationships below) |
872
- | \`stopOnResponse\` | \`boolean\` | No | Stop side turn on text response without tool calls (default: true) |
873
- | \`stopTool\` | \`string\` | No | Tool that stops this side's turn when called, with or without \`stopToolResponseProperty\` |
874
- | \`stopToolResponseProperty\` | \`string\` | No | Property to extract from the stop tool result. This option requires \`stopTool\`. |
875
- | \`maxSteps\` | \`number\` | No | Maximum step budget for this side |
876
- | \`sessionStop\` | \`SessionToolBinding\` | No | Terminal success lifecycle binding (object form only) |
877
- | \`sessionFail\` | \`SessionToolBinding\` | No | Terminal failure lifecycle binding (object form only) |
878
- | \`sessionStatus\` | \`SessionToolBinding\` | No | Non-terminal status lifecycle binding (object form only) |
879
-
880
- **Handler side**
679
+ ## Agent properties
881
680
 
882
681
  | Property | Type | Required | Description |
883
682
  |----------|------|----------|-------------|
884
- | \`handler\` | \`string\` | Yes | Handler name for this side (references \`agents/handlers/\`) |
885
- | \`label\` | \`string\` | No | Display name for this side |
886
- | \`subagents\` | \`SubagentRelationship[]\` | No | Side-owned child relationships |
887
-
888
- 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\`.
889
-
890
- Session lifecycle bindings use the object form only:
683
+ | \`name\` | \`string\` | Yes | The agent's name. Use \`snake_case\` and end it with \`_agent\` |
684
+ | \`sideA\` | side | Yes | Side A |
685
+ | \`sideB\` | side | Yes | Side B |
686
+ | \`title\` | \`string\` | No | Display name |
687
+ | \`description\` | \`string\` | No | What the agent does |
688
+ | \`icon\` | \`string\` | No | A URL, a data URI, or a path under the project's \`public/\` directory such as \`/icons/support.svg\` |
689
+ | \`maxSessionTurns\` | \`number\` | No | The most exchanges one session may take |
690
+ | \`exposeAsTool\` | \`boolean\` | No | Let prompts and other agents call this agent |
691
+ | \`toolDescription\` | \`string\` | With \`exposeAsTool\` | What a calling model is told about this agent |
692
+ | \`primary\` | \`boolean\` | No | Set \`false\` to hide the agent from agent lists. Default \`true\` |
693
+ | \`env\` | \`Record<string, string>\` | No | Default variable values |
694
+ | \`hooks\` | \`string[]\` | No | Hook IDs to run. A prompt's own \`hooks\` replace these for that prompt |
695
+ | \`parentHooks\` | array | No | Hooks this agent adds to an agent that composes it |
696
+
697
+ ## Side properties
698
+
699
+ A human side takes \`human: true\` and an optional \`label\`. Nothing else is allowed on it.
700
+
701
+ A prompt side:
891
702
 
892
- \`\`\`typescript
703
+ | Property | Type | Description |
704
+ |----------|------|-------------|
705
+ | \`prompt\` | \`string\` | Required. The \`name\` of a prompt |
706
+ | \`label\` | \`string\` | Display name for the side |
707
+ | \`stopOnResponse\` | \`boolean\` | End the side's turn when the model replies without calling a tool. Default \`true\` |
708
+ | \`stopTool\` | \`string\` | A tool that ends the side's turn when called |
709
+ | \`stopToolResponseProperty\` | \`string\` | The property of the stop tool's result to use as the side's reply |
710
+ | \`maxSteps\` | \`number\` | The most model requests this side makes in one turn |
711
+ | \`sessionStop\` | binding | The tool that ends the session successfully |
712
+ | \`sessionFail\` | binding | The tool that ends the session as failed |
713
+ | \`sessionStatus\` | binding | The tool that reports progress without ending the session |
714
+ | \`subagents\` | array | Child agents this side can start |
715
+
716
+ A handler side takes \`handler\` (required), \`label\`, and \`subagents\`.
717
+
718
+ A binding names a tool and the arguments to read from its call:
719
+
720
+ \`\`\`text
893
721
  sessionStop: {
894
- name: 'complete_work', // tool that controls the lifecycle action
895
- messageProperty: 'summary', // arg field that becomes lifecycle message text
896
- attachmentsProperty: 'files', // arg field containing attachment path(s)
722
+ name: 'close_conversation', // the tool
723
+ messageProperty: 'summary', // the argument that becomes the result text
724
+ attachmentsProperty: 'files', // the argument that holds file paths
897
725
  }
898
726
  \`\`\`
899
727
 
900
- ## Derivations
728
+ The bound tool must also be listed in the prompt's \`tools\`, and must exist in \`agents/tools/\`.
901
729
 
902
- These follow from the backings and are never declared separately:
730
+ ## Turns and sessions
903
731
 
904
- - **Chat surface** \u2014 the primary conversational interface renders wherever a \`human\` backing is.
905
- - **Perspective** \u2014 \`role: 'user'\` is the side B perspective, \`role: 'assistant'\` is the side A perspective, for queueing, injection, and rendering alike.
906
- - **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).
907
- - **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.
732
+ A **turn** is one side acting until it stops. A **session** is the exchange of turns until the work is done.
908
733
 
909
- ## Example: Assistant (human \xD7 prompt)
734
+ A prompt side's turn ends at the first of these:
910
735
 
911
- The everyday user-facing assistant \u2014 a person on side B, a model on side A:
736
+ 1. It calls its \`sessionStop\` or \`sessionFail\` tool. The session ends too.
737
+ 2. It calls its \`stopTool\`.
738
+ 3. The model replies with text and no tool call, when \`stopOnResponse\` is \`true\`.
739
+ 4. It reaches \`maxSteps\`.
912
740
 
913
- \`\`\`typescript
914
- import { defineAgent } from '@standardagents/builder';
741
+ A handler side's turn ends when its function returns. A human side's turn ends when the person sends a message.
915
742
 
916
- export default defineAgent({
917
- name: 'support_agent',
918
- sideA: {
919
- label: 'Support',
920
- prompt: 'customer_support',
921
- stopOnResponse: true,
922
- sessionStop: { name: 'close_ticket' },
923
- maxSteps: 50,
924
- },
925
- sideB: { human: true },
926
- tags: ['support', 'tier-1'],
927
- });
928
- \`\`\`
743
+ A session also ends when it reaches \`maxSessionTurns\`. There is no default. Without it, a run stops only at a fallback ceiling of 250 model steps.
929
744
 
930
- ## Example: Worker / Reviewer (prompt \xD7 prompt)
745
+ ## Two models working together
931
746
 
932
- 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:
747
+ Both sides can be prompts. Each side sees its own messages as \`assistant\` and the other side's as \`user\`. Use this shape for work that benefits from review. Give the sides different jobs, name the tool that ends the session, and set \`maxSessionTurns\`, because nothing else stops two models.
933
748
 
934
749
  \`\`\`typescript
750
+ // agents/agents/draft_agent.ts
751
+ import { defineAgent } from '@standardagents/builder';
752
+
935
753
  export default defineAgent({
936
- name: 'research_agent',
937
- maxSessionTurns: 20,
754
+ name: 'draft_agent',
755
+ title: 'Draft and Review',
756
+ description: 'Writes a reply to a customer and revises it until a reviewer approves.',
938
757
  exposeAsTool: true,
939
- toolDescription: 'Researches and validates findings autonomously',
758
+ toolDescription: 'Write a reviewed reply to a customer from a short brief',
759
+ maxSessionTurns: 8,
940
760
  sideA: {
941
- label: 'Worker',
942
- prompt: 'research_worker',
943
- stopOnResponse: true,
944
- sessionStatus: { name: 'set_research_status', messageProperty: 'status' },
945
- sessionFail: { name: 'fail_research', messageProperty: 'reason' },
761
+ label: 'Writer',
762
+ prompt: 'draft_writer',
946
763
  },
947
764
  sideB: {
948
765
  label: 'Reviewer',
949
- prompt: 'research_reviewer',
950
- stopOnResponse: true,
951
- sessionStop: { name: 'approve_research', messageProperty: 'summary' },
766
+ prompt: 'draft_reviewer',
767
+ sessionStop: { name: 'approve_draft', messageProperty: 'final_text' },
952
768
  },
953
769
  });
954
770
  \`\`\`
955
771
 
956
- ## Example: Relay (handler \xD7 human)
957
-
958
- 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:
959
-
960
772
  \`\`\`typescript
961
- export default defineAgent({
962
- name: 'relay_agent',
963
- sideA: {
964
- handler: 'relay_pipe',
965
- subagents: [{ agent: 'assistant_agent', maxInstances: 1 }],
966
- },
967
- sideB: { human: true },
773
+ // agents/prompts/draft_writer.ts
774
+ import { definePrompt } from '@standardagents/builder';
775
+ import { z } from 'zod';
776
+
777
+ export default definePrompt({
778
+ name: 'draft_writer',
779
+ toolDescription: 'Write a reply to a customer from a brief',
780
+ model: 'default',
781
+ prompt: 'Write a reply to the customer described in the brief. When the reviewer asks for changes, send a complete revised reply.',
782
+ requiredSchema: z.object({
783
+ brief: z.string().describe('Who the customer is, what they asked, and what the reply must say'),
784
+ }),
785
+ includeChat: true,
968
786
  });
969
787
  \`\`\`
970
788
 
971
- The \`relay_pipe\` handler lives in \`agents/handlers/relay_pipe.ts\` \u2014 the full worked example is in \`agents/handlers/AGENTS.md\`.
972
-
973
- ## Subagent Relationships
974
-
975
- 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:
976
-
977
789
  \`\`\`typescript
978
- sideA: {
979
- prompt: 'coordinator',
980
- subagents: [
981
- { agent: 'calendar_agent', as: 'work_calendar', scopedEnv: ['WORK_CAL_ID'] },
982
- { agent: 'calendar_agent', as: 'personal_calendar', scopedEnv: ['PERSONAL_CAL_ID'] },
983
- { agent: 'research_agent', blocking: true, maxInstances: 1 },
984
- ],
985
- },
986
- \`\`\`
987
-
988
- - 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.
989
- - A relationship name (\`as\`, defaulting to the unqualified part of \`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\`.
990
- - 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.
991
- - **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.
790
+ // agents/prompts/draft_reviewer.ts
791
+ import { definePrompt } from '@standardagents/builder';
992
792
 
993
- Blocking \`subagent_create\` returns the session result through its tool result.
994
- That session has no implicit completion message.
995
- With \`maxInstances: 1\`, a repeated call continues the existing child.
996
- Continuation acquires execution before persisting its input.
997
- Admission failure leaves the continuation input unsent.
998
- A separate \`subagent_message\` activation retains implicit delivery under \`parentCommunication: 'implicit'\`.
793
+ export default definePrompt({
794
+ name: 'draft_reviewer',
795
+ toolDescription: 'Review a draft reply to a customer',
796
+ model: 'default',
797
+ prompt: \`Review the draft for accuracy, tone, and length.
798
+ If it is ready to send, call approve_draft with the final text.
799
+ Otherwise reply with the specific changes you need.\`,
800
+ tools: ['approve_draft'],
801
+ includeChat: true,
802
+ includePastTools: true,
803
+ });
804
+ \`\`\`
999
805
 
1000
- 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).
806
+ The tool \`approve_draft\` is in \`agents/tools/AGENTS.md\`.
1001
807
 
1002
- ## Agent Handoffs
808
+ ## Subagents
1003
809
 
1004
- 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.
810
+ A side can start other agents as children. Each child runs in its own thread with its own history. Declare the children on the side that uses them:
1005
811
 
1006
812
  \`\`\`typescript
813
+ // agents/agents/coordinator_agent.ts
814
+ import { defineAgent } from '@standardagents/builder';
815
+
1007
816
  export default defineAgent({
1008
- name: 'escalation_agent',
1009
- exposeAsTool: true,
1010
- toolDescription: 'Escalate to senior support for complex issues',
1011
- sideA: { prompt: 'senior_support' },
1012
- sideB: { human: true },
817
+ name: 'coordinator_agent',
818
+ title: 'Support Coordinator',
819
+ description: 'Talks to the customer and delegates reply writing to a reviewed drafting agent.',
820
+ sideA: {
821
+ label: 'Coordinator',
822
+ prompt: 'coordinator',
823
+ subagents: [
824
+ {
825
+ agent: 'draft_agent',
826
+ blocking: true,
827
+ maxInstances: 1,
828
+ initUserMessageProperty: 'brief',
829
+ },
830
+ ],
831
+ },
832
+ sideB: { label: 'Customer', human: true },
1013
833
  });
1014
834
  \`\`\`
1015
835
 
1016
- ## Stop Semantics
836
+ \`\`\`typescript
837
+ // agents/prompts/coordinator.ts
838
+ import { definePrompt } from '@standardagents/builder';
1017
839
 
1018
- Runtime stop evaluation order for prompt-backed sides:
840
+ export default definePrompt({
841
+ name: 'coordinator',
842
+ toolDescription: 'Coordinate customer support and delegate reply writing',
843
+ model: 'default',
844
+ prompt: \`You coordinate customer support.
845
+ For a reply that needs care, call subagent_create with agent "draft_agent", a short name, and a brief in arguments.
846
+ Send the customer the text it returns.\`,
847
+ includeChat: true,
848
+ includePastTools: true,
849
+ });
850
+ \`\`\`
1019
851
 
1020
- 1. **sessionStop / sessionFail** \u2014 end the session (success / terminal failure)
1021
- 2. **stopTool** \u2014 ends the current side's turn
1022
- 3. **stopOnResponse** \u2014 ends the turn when the LLM returns text without tool calls
1023
- 4. **Safety limits** \u2014 \`maxSteps\` (per side), \`maxSessionTurns\` (per session, hard limit: 250)
852
+ Any agent can be a child. Give it \`exposeAsTool: true\` and a \`toolDescription\` so the parent's model is told what it does. Do not list the child in the prompt's \`tools\`. For a prompt side with subagents, the runtime adds three tools on its own:
1024
853
 
1025
- 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.
854
+ | Tool | What it does |
855
+ |------|--------------|
856
+ | \`subagents\` | Lists the children that exist, with their status |
857
+ | \`subagent_create\` | Starts a child. Takes \`agent\` (the relationship name), \`name\` (a label for this child), and \`arguments\` |
858
+ | \`subagent_message\` | Sends a message to an existing child |
1026
859
 
1027
- ## Definition-Time Errors
860
+ \`arguments\` follows the \`requiredSchema\` of the child's side A prompt. The child reads them as \`state.arguments\` and as prompt variables.
1028
861
 
1029
- These shapes are rejected when definitions load:
862
+ Relationship options:
1030
863
 
1031
- 1. Both sides human (reserved \u2014 may lift in a later version)
1032
- 2. \`subagents\` declared on a human-backed side
1033
- 3. A side with zero backings, or more than one of \`human\` / \`prompt\` / \`handler\`
1034
- 4. A handler side referencing an undefined handler; a prompt side referencing an undefined prompt
1035
- 5. A relationship name (\`as\`) appearing on both sides, colliding with another tool name, or colliding with a reserved lifecycle tool name
1036
- 6. Session bindings, \`maxSteps\`, or stop bindings on a non-prompt side
1037
- 7. A session binding that is not in object form
864
+ | Option | Meaning |
865
+ |--------|---------|
866
+ | \`agent\` | Required. The child agent's \`name\` |
867
+ | \`as\` | The relationship name the model sees. Defaults to the agent name. Use it to compose one agent twice |
868
+ | \`description\` | Overrides the child's \`toolDescription\` for this relationship |
869
+ | \`blocking\` | \`subagent_create\` waits for the child's session and returns its result |
870
+ | \`maxInstances\` | The most children of this relationship. With \`blocking\` and \`1\`, a repeated call continues the same child |
871
+ | \`immediate\` | Start the child when the thread is created |
872
+ | \`parentCommunication\` | \`'implicit'\` (default) sends the child's result to the parent as a message. \`'explicit'\` sends nothing unless the child calls \`state.notifyParent()\` |
873
+ | \`initUserMessageProperty\` | The argument to send as the child's first message |
874
+ | \`initAttachmentsProperty\` | The argument that holds file paths to attach |
875
+ | \`initAgentNameProperty\` | The argument to use as the child's display name |
876
+ | \`scopedEnv\` | Variables the child must be given and does not inherit |
877
+ | \`optional\` | A variable name. The relationship is enabled only when that variable is \`true\` |
878
+ | \`hidden\` | Keep the relationship out of the model's tools. Code can still start it |
1038
879
 
1039
- ## Hooks
880
+ A child's thread is never deleted when its session ends. The parent can message the same child again later. Only \`state.terminate()\` ends a child for good.
1040
881
 
1041
- Attach lifecycle hooks at the agent level:
882
+ ## Handing a thread to another agent
883
+
884
+ An agent with \`exposeAsTool: true\` can also be listed in a prompt's \`tools\`. Calling it hands the same thread, with its history, to that agent.
1042
885
 
1043
886
  \`\`\`typescript
887
+ // agents/agents/billing_agent.ts
888
+ import { defineAgent } from '@standardagents/builder';
889
+
1044
890
  export default defineAgent({
1045
- name: 'support_agent',
1046
- hooks: ['log_all_messages', 'sanitize_results'],
1047
- sideA: { prompt: 'customer_support' },
1048
- sideB: { human: true },
891
+ name: 'billing_agent',
892
+ title: 'Billing Support',
893
+ description: 'Answers billing questions.',
894
+ exposeAsTool: true,
895
+ toolDescription: 'Hand the conversation to billing support for invoice, charge, and refund questions',
896
+ primary: false,
897
+ sideA: { label: 'Billing', prompt: 'billing_support' },
898
+ sideB: { label: 'Customer', human: true },
1049
899
  });
1050
900
  \`\`\`
1051
901
 
1052
- 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.
902
+ \`\`\`typescript
903
+ // agents/agents/triage_agent.ts
904
+ import { defineAgent } from '@standardagents/builder';
1053
905
 
1054
- Any exposed agent can declare required parent integration hooks:
906
+ export default defineAgent({
907
+ name: 'triage_agent',
908
+ title: 'Support Triage',
909
+ description: 'Greets the customer and routes billing questions to billing support.',
910
+ sideA: { label: 'Triage', prompt: 'triage' },
911
+ sideB: { label: 'Customer', human: true },
912
+ });
913
+ \`\`\`
1055
914
 
1056
915
  \`\`\`typescript
1057
- parentHooks: [{
1058
- hook: 'sync_gmail_context',
1059
- reason: 'Makes synchronized Gmail context available to the coordinator.',
1060
- stage: 'before_parent', // default: after_parent
1061
- }],
916
+ // agents/prompts/triage.ts
917
+ import { definePrompt } from '@standardagents/builder';
918
+
919
+ export default definePrompt({
920
+ name: 'triage',
921
+ toolDescription: 'Greet a customer and route the conversation',
922
+ model: 'default',
923
+ prompt: 'Greet the customer and find out what they need. For a billing question, call billing_agent. Answer anything else yourself.',
924
+ tools: ['billing_agent'],
925
+ includeChat: true,
926
+ });
1062
927
  \`\`\`
1063
928
 
1064
- Installing the package is inert. Declaring a relationship targeting this agent
1065
- (or adding it to a prompt as a handoff) activates the hook automatically on
1066
- that direct parent, and composition UIs disclose the \`reason\` first. Disabled
1067
- optional relationships contribute nothing; enabled hidden relationships still
1068
- contribute hooks. Required hooks run with the parent's \`ThreadState\`, load
1069
- from the child's package namespace, and fail composition closed if missing.
1070
- User-scope hooks cannot be listed.
929
+ The prompt \`billing_support\` is in \`agents/prompts/AGENTS.md\`.
1071
930
 
1072
- ## Naming Convention
931
+ ## Definitions that are rejected
1073
932
 
1074
- Agent names **must** end with the \`_agent\` suffix (e.g., \`support_agent\`, \`research_agent\`).
1075
- This convention is enforced by the builder UI and makes agents easily identifiable in logs and code.
933
+ An agent fails to load when:
1076
934
 
1077
- ## Icons
935
+ - Both sides are human.
936
+ - A side has no backing, or more than one of \`human\`, \`prompt\`, and \`handler\`.
937
+ - A human side has anything other than \`human\` and \`label\`.
938
+ - A handler side has \`maxSteps\`, a stop setting, or a session binding.
939
+ - A side names a prompt or handler that does not exist.
940
+ - \`exposeAsTool\` is \`true\` and \`toolDescription\` is missing.
941
+ - A relationship name is used on both sides, or matches a tool name or one of the three subagent tool names.
1078
942
 
1079
- Give an agent a custom icon with the \`icon\` property: a full URL, a data URI,
1080
- or a site-relative path served from your project's \`public/\` directory \u2014
1081
- \`icon: '/icons/support.svg'\` expects the file at \`public/icons/support.svg\`.
1082
- The file is served in dev and deployed with your project's assets. Agents
1083
- without an icon (or whose icon fails to load) render a generated identicon,
1084
- and the dashboard warns when a site-relative icon path has no matching file.
943
+ A failed agent is missing from the builder UI's agent list, and the UI shows the error in a banner.
1085
944
 
1086
- ## Best Practices
945
+ ## Practices
1087
946
 
1088
- - **Use descriptive names** (\`customer_support_agent\` not \`agent1\`)
1089
- - **Always use the _agent suffix** - names like \`support_agent\`, \`research_agent\`
1090
- - **Write sideB explicitly** - there is no implicit default; a user-facing agent ends with \`sideB: { human: true }\`
1091
- - **Cap prompt \xD7 prompt agents** - set \`maxSessionTurns\` when both sides are prompt-backed; nothing external stops two models
1092
- - **Match stop conditions to use case** - chat apps use stopOnResponse, workflows use stopTool
1093
- - **Use labels** for clarity in logs and UI
1094
- - **Organize with tags** for filtering
947
+ - Give every agent a \`title\`, a \`description\`, and side labels. They appear in the builder UI and in logs.
948
+ - Keep an agent to one domain with a short tool list. Compose agents for wider work.
949
+ - In a prompt-and-prompt agent, set \`maxSessionTurns\` and bind a \`sessionStop\` tool.
950
+ - Use a prompt as a tool for one model step. Use a subagent when the work needs several steps or a review.
1095
951
 
1096
952
  ## Documentation
1097
953
 
@@ -1099,143 +955,92 @@ Full reference: https://docs.standardagentbuilder.com/api-reference/define/agent
1099
955
  `;
1100
956
 
1101
957
  // src/docs/handlers.ts
1102
- var HANDLERS_AGENTS_MD = `# Handler Definitions
958
+ var HANDLERS_AGENTS_MD = `# Handler definitions
1103
959
 
1104
- 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.
960
+ A handler is a function that takes one side's turns in a conversation. When an agent side is \`{ handler: 'name' }\`, the runtime calls that function once for each message that arrives on the side. No model is called for that side.
1105
961
 
1106
- 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.
962
+ Use a handler for a side that should act by rule: forwarding messages, replying from a lookup, or starting other agents.
1107
963
 
1108
- ## Properties
964
+ A handler is not a tool (a model does not call it) and not a hook (it owns the turn instead of observing it). It is referenced from one place only: the \`handler\` property of an agent side.
1109
965
 
1110
- | Property | Type | Required | Description |
1111
- |----------|------|----------|-------------|
1112
- | \`description\` | \`string\` | No | Human-facing summary |
1113
- | \`execute\` | \`(state: ThreadState, message: Message) => Promise<void>\` | Yes | The turn implementation |
966
+ ## Defining a handler
1114
967
 
1115
- 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.
968
+ The file name is the handler's name: \`agents/handlers/office_hours.ts\` defines \`office_hours\`.
1116
969
 
1117
- ## Basic Example
970
+ | Property | Type | Required | Description |
971
+ |----------|------|----------|-------------|
972
+ | \`description\` | \`string\` | No | What the handler does |
973
+ | \`execute\` | \`(state, message) => Promise<void>\` | Yes | The turn |
1118
974
 
1119
975
  \`\`\`typescript
1120
- // agents/handlers/relay_pipe.ts
976
+ // agents/handlers/office_hours.ts
1121
977
  import { defineHandler } from '@standardagents/builder';
1122
978
 
1123
979
  export default defineHandler({
1124
- description: 'Forwards inbound user traffic to the assistant child and relays answers back.',
980
+ description: 'Replies with the support hours. Uses no model.',
1125
981
  execute: async (state, message) => {
1126
- // full ThreadState \u2014 forward, respond, spawn, stop, read/write KV
982
+ const text = typeof message.content === 'string' ? message.content.toLowerCase() : '';
983
+ const reply = text.includes('hours')
984
+ ? 'Support is open Monday to Friday, 9:00 to 17:00 UTC.'
985
+ : 'Ask me about our support hours.';
986
+
987
+ await state.injectMessage({ role: 'assistant', content: reply });
1127
988
  },
1128
989
  });
1129
990
  \`\`\`
1130
991
 
1131
- Reference it from exactly one place: the \`handler\` property of an agent side:
1132
-
1133
992
  \`\`\`typescript
993
+ // agents/agents/office_hours_agent.ts
994
+ import { defineAgent } from '@standardagents/builder';
995
+
1134
996
  export default defineAgent({
1135
- name: 'relay_agent',
1136
- sideA: { handler: 'relay_pipe', subagents: [{ agent: 'assistant_agent', maxInstances: 1 }] },
1137
- sideB: { human: true },
997
+ name: 'office_hours_agent',
998
+ title: 'Office Hours',
999
+ description: 'Answers questions about support hours without calling a model.',
1000
+ sideA: { label: 'Hours', handler: 'office_hours' },
1001
+ sideB: { label: 'Customer', human: true },
1138
1002
  });
1139
1003
  \`\`\`
1140
1004
 
1141
- ## Execution Model
1142
-
1143
- A handler-backed side's turn is:
1144
-
1145
- 1. An inbound message arrives on the side's perspective.
1146
- 2. The runtime invokes the handler once with the thread's \`ThreadState\` and that message.
1147
- 3. The invocation returns (or throws). The turn is complete.
1148
-
1149
- 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.
1150
-
1151
- ## Delivery Semantics
1005
+ \`message\` is the stored message that started the turn, with its \`id\`, \`role\`, \`content\`, and \`metadata\`. \`state\` is the full \`ThreadState\`, the same one tools receive; see \`agents/tools/AGENTS.md\`.
1152
1006
 
1153
- Delivery to a handler is **exactly-once per message**:
1007
+ A handler speaks by calling \`state.injectMessage()\` with its own side's role: \`assistant\` for side A, \`user\` for side B. Nothing is sent on its behalf.
1154
1008
 
1155
- - The handler is invoked exactly once for each inbound message on the side's perspective, in message persistence order.
1156
- - 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.
1157
- - A message whose handler invocation failed is not automatically redelivered. Retry is a userland decision (re-send the message).
1009
+ ## How a handler turn runs
1158
1010
 
1159
- **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.
1011
+ 1. A message arrives on the handler's side.
1012
+ 2. The runtime calls \`execute\` once with the thread state and that message.
1013
+ 3. \`execute\` returns. The turn is over.
1160
1014
 
1161
- ## Error Semantics
1015
+ - Each message is delivered once, in the order the messages were stored.
1016
+ - One handler call runs at a time per thread. Messages that arrive during a call wait their turn.
1017
+ - A handler side has no \`maxSteps\`, \`stopOnResponse\`, \`stopTool\`, or session bindings. To end the session, call \`state.stopSession()\`.
1018
+ - A handler turn counts toward the agent's \`maxSessionTurns\`.
1162
1019
 
1163
- A handler that throws is **fatal to the turn, and only the turn**:
1020
+ ## Errors
1164
1021
 
1165
- - 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.
1166
- - The inbound message stays persisted. Nothing is rolled back.
1167
- - 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.
1022
+ When \`execute\` throws:
1168
1023
 
1169
- If an error should end the session, the handler must do so explicitly with \`state.stopSession()\`.
1024
+ - The turn is marked failed and the error appears in the thread's logs.
1025
+ - The message that started the turn stays stored. It is not delivered again.
1026
+ - The session continues. The next message gets a new call.
1170
1027
 
1171
- ## Capabilities
1172
-
1173
- A handler receives the full \`ThreadState\` and may do anything tools can do:
1174
-
1175
- - **Forward** \u2014 \`(await state.getChildThread(ref)).queueMessage(...)\` toward a child, or \`state.notifyParent(...)\` toward a parent.
1176
- - **Respond** \u2014 \`state.injectMessage({ role, content })\` in its own side's role (\`assistant\` for side A, \`user\` for side B).
1177
- - **Spawn** \u2014 create child instances of the side's declared \`subagents\` relationships via lifecycle APIs.
1178
- - **Control** \u2014 \`state.stopSession()\`, \`state.setStatus()\`, effects, environment, key-value store, filesystem.
1179
-
1180
- 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.)
1181
-
1182
- ## The Outbound Boundary
1183
-
1184
- 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.**
1185
-
1186
- ## Livelock Discipline
1187
-
1188
- 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:
1189
-
1190
- - reply conditionally, not unconditionally;
1191
- - or set \`maxSessionTurns\` on the agent (available on every shape) as a hard safety valve;
1192
- - or end the session explicitly with \`state.stopSession()\` when the work is done.
1193
-
1194
- ## Worked Example: the Relay
1195
-
1196
- 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:
1197
-
1198
- \`\`\`typescript
1199
- // agents/handlers/relay_pipe.ts
1200
- import { defineHandler } from '@standardagents/builder';
1028
+ To end the session on an error, catch it and call \`state.stopSession()\`.
1201
1029
 
1202
- export default defineHandler({
1203
- description: 'Pipe user traffic to the assistant child; relay answers outward.',
1204
- execute: async (state, message) => {
1205
- const child = state.children.find(c => c.relationship === 'assistant_agent');
1206
- if (child) {
1207
- const thread = await state.getChildThread(child.reference);
1208
- await thread?.queueMessage({ role: 'user', content: message.content ?? '' });
1209
- }
1210
- },
1211
- });
1030
+ ## Subagents
1212
1031
 
1213
- // agents/agents/relay_agent.ts
1214
- import { defineAgent } from '@standardagents/builder';
1032
+ A handler side can declare \`subagents\`, with the same options as a prompt side (see \`agents/agents/AGENTS.md\`). A handler reaches its children through \`state\`: \`state.children\` lists them, and \`state.getChildThread(reference)\` returns a child's thread state, whose \`queueMessage()\` sends it a message. A handler needs no tools for this.
1215
1033
 
1216
- export default defineAgent({
1217
- name: 'relay_agent',
1218
- sideA: {
1219
- handler: 'relay_pipe',
1220
- subagents: [{
1221
- agent: 'assistant_agent',
1222
- maxInstances: 1,
1223
- immediate: true, // spawned at thread creation
1224
- parentCommunication: 'explicit',
1225
- }],
1226
- },
1227
- sideB: { human: true },
1228
- });
1229
- \`\`\`
1034
+ ## Avoiding stalls and loops
1230
1035
 
1231
- 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.
1036
+ - Do not wait inside a handler for work that needs another message on the same thread. Messages queue behind the running call, so that wait never ends. Forward with \`queueMessage()\` and handle the answer as a later turn.
1037
+ - A handler that replies to every message, opposite a prompt side, never stops. Reply only when there is something to say, set \`maxSessionTurns\` on the agent, or call \`state.stopSession()\` when the work is done.
1232
1038
 
1233
- ## Best Practices
1039
+ ## Practices
1234
1040
 
1235
- - **Forward without blocking** \u2014 queue toward children and treat their replies as later turns; never hold the delivery slot on a child session
1236
- - **Reply conditionally** \u2014 an unconditional reply opposite a prompt side is a ping-pong
1237
- - **End sessions explicitly** \u2014 \`state.stopSession()\` when the work is done; there are no declarative session bindings on handler sides
1238
- - **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
1041
+ - Keep handlers short and rule-based. Work that needs judgment belongs in a prompt.
1042
+ - End sessions explicitly with \`state.stopSession()\`.
1043
+ - Put a capability that a model should be able to call in a tool, not a handler.
1239
1044
 
1240
1045
  ## Documentation
1241
1046
 
@@ -1243,341 +1048,294 @@ Agent reference: https://docs.standardagentbuilder.com/api-reference/define/agen
1243
1048
  `;
1244
1049
 
1245
1050
  // src/docs/tools.ts
1246
- var TOOLS_AGENTS_MD = `# Custom Tools
1051
+ var TOOLS_AGENTS_MD = `# Tools
1247
1052
 
1248
- Tools extend agent capabilities beyond text generation. They allow LLMs to fetch data, perform calculations, execute side effects, and chain to other prompts or agents.
1053
+ A tool is a function a model can call. Each file in this directory defines one tool, and the file name is the tool's name: \`search_knowledge_base.ts\` defines \`search_knowledge_base\`. Use \`snake_case\` file names. A prompt offers a tool to its model by listing the name in \`tools\`.
1249
1054
 
1250
- ## Function Signature
1055
+ ## Shape
1251
1056
 
1252
- \`\`\`typescript
1057
+ \`\`\`text
1253
1058
  defineTool({
1254
- description: string, // What the tool does (shown to LLM)
1255
- args?: z.ZodObject, // Input validation schema (optional)
1256
- execute: (state, args) => ToolResult, // Implementation
1257
- variables?: VariableDefinition[], // Declared variables (optional)
1258
- progressArgument?: string, // Arg holding a short progress label, e.g. "writing index.html"
1059
+ description, // Required. What the tool does. The model reads this to decide when to call it
1060
+ args, // A z.object(...) describing the arguments. Omit for a tool with none
1061
+ execute, // Required. async (state, args) => ToolResult
1062
+ variables, // Variables the tool needs
1063
+ progressArgument, // The argument that holds a short progress label for the UI
1259
1064
  })
1260
1065
  \`\`\`
1261
1066
 
1262
- ## Basic Example
1067
+ \`execute\` returns a \`ToolResult\`:
1068
+
1069
+ | Property | Type | Description |
1070
+ |----------|------|-------------|
1071
+ | \`status\` | \`'success'\` or \`'error'\` | Required |
1072
+ | \`result\` | \`string\` | The output the model reads. Use JSON for structured data |
1073
+ | \`error\` | \`string\` | The message the model reads when \`status\` is \`'error'\` |
1074
+ | \`error_code\` | \`string\` | A stable code for the failure |
1075
+ | \`error_data\` | \`object\` | Structured details about the failure |
1076
+ | \`attachments\` | array | Files to attach, each \`{ name, mimeType, data }\` with base64 \`data\` |
1077
+
1078
+ ## A tool with arguments
1263
1079
 
1264
1080
  \`\`\`typescript
1081
+ // agents/tools/search_knowledge_base.ts
1265
1082
  import { defineTool } from '@standardagents/builder';
1266
1083
  import { z } from 'zod';
1267
1084
 
1268
1085
  export default defineTool({
1269
- description: 'Search the knowledge base for articles matching a query',
1086
+ description: 'Search the store knowledge base. Returns up to \`limit\` articles with a title, a URL, and an excerpt.',
1270
1087
  args: z.object({
1271
- query: z.string().describe('Search query'),
1272
- limit: z.number().optional().default(10).describe('Max results'),
1088
+ query: z.string().describe('What to search for, in plain words'),
1089
+ limit: z.number().int().min(1).max(10).default(5).describe('How many articles to return'),
1273
1090
  }),
1091
+ variables: [
1092
+ {
1093
+ name: 'KNOWLEDGE_BASE_URL',
1094
+ type: 'text',
1095
+ required: true,
1096
+ scoped: false,
1097
+ description: 'Base URL of the knowledge base search API',
1098
+ },
1099
+ ],
1274
1100
  execute: async (state, args) => {
1275
- const results = await searchKB(args.query, args.limit);
1276
- return {
1277
- status: 'success',
1278
- result: JSON.stringify(results),
1279
- };
1101
+ const baseUrl = await state.env('KNOWLEDGE_BASE_URL');
1102
+ if (!baseUrl) {
1103
+ return { status: 'error', error: 'KNOWLEDGE_BASE_URL is not set.', error_code: 'not_configured' };
1104
+ }
1105
+
1106
+ const url = new URL('/search', baseUrl);
1107
+ url.searchParams.set('q', args.query);
1108
+ url.searchParams.set('limit', String(args.limit));
1109
+
1110
+ const response = await fetch(url);
1111
+ if (!response.ok) {
1112
+ return {
1113
+ status: 'error',
1114
+ error: \`The knowledge base returned HTTP \${response.status}.\`,
1115
+ error_code: 'upstream_error',
1116
+ };
1117
+ }
1118
+
1119
+ return { status: 'success', result: await response.text() };
1280
1120
  },
1281
1121
  });
1282
1122
  \`\`\`
1283
1123
 
1284
- Tool variable definitions preserve \`scoped: true\`. A scoped variable blocks inheritance from the parent thread environment.
1285
-
1286
- \`defineTool()\` preserves \`annotations\` unchanged.
1124
+ Return an error result for a failure the model can react to, as above. The model reads \`error\` and can retry or tell the user. Let unexpected exceptions throw; the runtime records them in the thread's logs.
1287
1125
 
1288
- ## Argument Schemas
1126
+ ## Arguments
1289
1127
 
1290
- Tool args must be a top-level \`z.object(...)\`. Prefer explicit, bounded schemas made from strings, numbers, booleans, nulls, literals, enums, arrays, objects, optionals, nullables, and defaults. Avoid provider-visible recursive or open-ended schemas such as \`z.unknown()\`, \`z.any()\`, and \`z.object({}).catchall(...)\`; strict tool providers can reject the generated JSON Schema. If a tool needs arbitrary JSON input, accept it as a JSON string and parse it inside \`execute\`.
1128
+ \`args\` must be a \`z.object(...)\`. Build it from strings, numbers, booleans, enums, literals, arrays, and nested objects, with \`.optional()\`, \`.nullable()\`, and \`.default()\` as needed. Add \`.describe()\` to every field; the model reads the descriptions.
1291
1129
 
1292
- ## ToolResult Interface
1130
+ Avoid \`z.any()\`, \`z.unknown()\`, and open-ended records. Some providers reject the schemas they produce. For free-form JSON, take a string argument and parse it in \`execute\`.
1293
1131
 
1294
- | Property | Type | Description |
1295
- |----------|------|-------------|
1296
- | \`status\` | \`'success' \\| 'error'\` | Whether the tool succeeded |
1297
- | \`result\` | \`string\` | Tool output (required for success) |
1298
- | \`error\` | \`string\` | Error message (for error status) |
1299
- | \`error_code\` | \`string\` | Machine-readable error code |
1300
- | \`error_data\` | \`Record<string, unknown>\` | Structured error payload |
1301
- | \`attachments\` | \`ToolAttachment[]\` | Files to attach (images, etc.) |
1302
-
1303
- ## ThreadState Access
1132
+ ## Thread state
1304
1133
 
1305
- The \`state\` parameter provides access to:
1134
+ \`state\` is the thread the tool runs in. This tool stores a record in the thread's key-value store and reads the thread's user:
1306
1135
 
1307
1136
  \`\`\`typescript
1308
- execute: async (state, args) => {
1309
- // Thread information
1310
- const threadId = state.thread.id;
1311
- const thread = state.thread.instance;
1312
- const creationArgs = state.arguments; // Invocation or subagent creation args
1313
-
1314
- // Configuration
1315
- const agentName = state.agent.name;
1316
- const modelName = state.model.name;
1317
-
1318
- // Message history
1319
- const messages = state.messages;
1320
-
1321
- // Thread variable resolution
1322
- const apiKey = await state.env('GOOGLE_API_KEY');
1323
- await state.setEnv('LAST_TOOL_RUN_AT', new Date().toISOString());
1324
-
1325
- // Durable per-thread JSON values
1326
- const runCount = (await state.getValue<number>('run_count')) ?? 0;
1327
- await state.setValue('run_count', runCount + 1);
1328
-
1329
- // The thread's user profile (identity only, never credentials) \u2014 for
1330
- // per-user personalization or entitlement checks. Null when user-less.
1331
- const user = await state.user();
1332
-
1333
- // A thread sees its owner's skills and enabled shared skills.
1334
- // Load a skill body when it applies. Mutations require an instance admin.
1335
- const skills = await state.skills.list();
1336
- const skill = await state.skills.get('pdf-processing');
1337
- const script = await state.skills.readFile('pdf-processing', 'scripts/extract.py');
1338
-
1339
- // Sandboxed JS/TS execution with explicit bridge capabilities
1340
- const run = state.runCode('export async function main(path) { return await read(path) }', {
1341
- execute: { fn: 'main', args: ['/notes.txt'] },
1342
- globals: {
1343
- read: async (path: string) => {
1344
- const data = await state.readFile(path);
1345
- return data ? new TextDecoder().decode(data) : null;
1346
- },
1347
- },
1348
- });
1349
- const codeResult = await run;
1350
-
1351
- // Parent state (for sub-prompts)
1352
- const root = state.rootState;
1353
- }
1354
- \`\`\`
1355
-
1356
- Use \`state.runCode()\` instead of \`eval\` or \`new Function\` for model- or user-authored JavaScript/TypeScript. The sandbox starts with no ambient host capabilities; provide imports/globals explicitly, use \`modules\` for local relative ES modules, use \`execute\` to select an export and pass args, use \`report\` for streamed observations, and call \`run.terminate(reason)\` from your own timeout budget when needed. \`modules\` keys are graph-root relative paths; entry-source imports resolve from \`options.filename\` when supplied. Relative module imports may use \`./\` and \`../\` as long as they resolve within the supplied module graph. In thread endpoints, \`runCode\` forwards functions in imports, globals, and execution arguments as callable proxies. Call \`run.terminate(reason)\` to stop the sandbox. A caller disconnect also stops the run. When \`report\` is supplied, the host receives reports during the run and \`run.reports\` updates as reports arrive.
1357
-
1358
- ### Optional Runtime Streams
1137
+ // agents/tools/create_ticket.ts
1138
+ import { defineTool } from '@standardagents/builder';
1139
+ import { z } from 'zod';
1359
1140
 
1360
- Some runtimes expose live, authorized byte streams through
1361
- \`state.runtimeStreams\`. Always feature-detect the optional surface. The
1362
- following neutral example streams media to a workstation-hosted transcoder,
1363
- half-closes the writable side, reads the encoded result, and cleans up:
1141
+ export default defineTool({
1142
+ description: 'Open a support ticket for a person to follow up. Returns the ticket ID.',
1143
+ args: z.object({
1144
+ subject: z.string().describe('One line that names the problem'),
1145
+ details: z.string().describe('Everything a support person needs to act on the ticket'),
1146
+ }),
1147
+ execute: async (state, args) => {
1148
+ const user = await state.user();
1149
+ const ticket = {
1150
+ id: \`T-\${crypto.randomUUID().slice(0, 8)}\`,
1151
+ subject: args.subject,
1152
+ details: args.details,
1153
+ openedBy: user?.email ?? null,
1154
+ };
1364
1155
 
1365
- \`\`\`typescript
1366
- const runtimeStreams = state.runtimeStreams;
1367
- if (!runtimeStreams) {
1368
- return { status: 'error', error: 'This runtime has no workstation streams.' };
1369
- }
1156
+ await state.setValue(\`ticket:\${ticket.id}\`, ticket);
1157
+ state.emit('ticket_created', { id: ticket.id });
1370
1158
 
1371
- const opened = await runtimeStreams.open({
1372
- capability: 'workstation_transcoder',
1373
- metadata: { inputFormat: 'wav', outputFormat: 'opus' },
1374
- timeoutMs: 10_000,
1159
+ return { status: 'success', result: JSON.stringify({ ticketId: ticket.id }) };
1160
+ },
1375
1161
  });
1376
- if (!opened.ok) {
1377
- return { status: 'error', error: opened.error, error_code: opened.code };
1378
- }
1379
-
1380
- try {
1381
- const writer = opened.stream.writable.getWriter();
1382
- try {
1383
- await writer.write(sourceMedia);
1384
- await writer.close();
1385
- } finally {
1386
- writer.releaseLock();
1387
- }
1162
+ \`\`\`
1388
1163
 
1389
- const reader = opened.stream.readable.getReader();
1390
- const encoded: Uint8Array[] = [];
1391
- try {
1392
- for (;;) {
1393
- const { value, done } = await reader.read();
1394
- if (done) break;
1395
- encoded.push(value);
1396
- }
1397
- } finally {
1398
- reader.releaseLock();
1399
- }
1164
+ Each ticket has its own key. \`getValue()\` followed by \`setValue()\` is two separate steps, so code that reads a shared list, changes it, and writes it back can overwrite a record that other code just added. Store each record under its own key.
1400
1165
 
1401
- const closed = await opened.stream.closed;
1402
- if (!closed.ok) throw new Error(closed.error ?? 'Transcoding failed.');
1403
- return { status: 'success', result: \`Received \${encoded.length} chunks.\` };
1404
- } finally {
1405
- opened.stream.close();
1406
- }
1407
- \`\`\`
1166
+ What \`state\` offers:
1408
1167
 
1409
- \`readable\` carries host-to-runtime bytes; \`writable\` carries
1410
- runtime-to-host bytes. Streams are live and non-durable. \`close()\` is
1411
- idempotent, and the \`closed\` promise settles once.
1168
+ | Use | Members |
1169
+ |-----|---------|
1170
+ | Identity | \`threadId\`, \`agentId\`, \`userId\`, \`arguments\` |
1171
+ | Messages | \`getMessages()\`, \`getMessage(id)\`, \`injectMessage()\`, \`queueMessage()\`, \`updateMessage()\` |
1172
+ | Durable values | \`getValue(key)\`, \`setValue(key, value)\`; a \`null\` value deletes the key |
1173
+ | Variables | \`env(name)\`, \`setEnv(name, value)\` |
1174
+ | Files | \`readFile\`, \`writeFile\`, \`readdirFile\`, \`statFile\`, \`moveFile\`, \`renameFile\`, \`unlinkFile\`, \`mkdirFile\`, \`findFiles\`, \`grepFiles\` |
1175
+ | Other tools | \`queueTool(name, args)\`, \`invokeTool(name, args)\` |
1176
+ | Live events | \`emit(event, data)\` |
1177
+ | User | \`user()\` returns the thread's user or \`null\` |
1178
+ | Subagents | \`children\`, \`getChildThread(reference)\`, \`getParentThread()\`, \`notifyParent()\`, \`setStatus()\` |
1179
+ | Session | \`stopSession()\`, \`execution\` |
1180
+ | Sandbox | \`runCode(source, options)\` |
1181
+ | Logs | \`log({ name, message, data })\` adds a row to the thread's logs |
1412
1182
 
1413
- Sandboxed coding code must not use ambient network globals. For HTTP inside \`coding_run\`, import \`{ fetch }\` from \`"fetch"\`; the bridge supports \`fetch(url, init)\` with methods, headers, and bodies, and returns a Response-like object with \`text()\`, \`json()\`, \`arrayBuffer()\`, and \`headers.get()\`. The sandboxed coding prompt includes \`fetch_url\` for reading known external public http(s) pages as Markdown. Use the \`coding_run\` fetch bridge instead for custom methods, headers, request bodies, or checks against thread public file/API endpoint URLs returned by \`get_public_url\`.
1183
+ \`getMessages()\` returns \`{ messages, total, hasMore }\`, newest first. Pass \`{ order: 'asc' }\` for oldest first, and \`limit\` and \`offset\` to page.
1414
1184
 
1415
- Sandboxed coding agents should verify runtime behavior as they work. \`coding_validate_typescript\` is useful but incomplete; call helper exports with \`coding_run\`, probe pages and endpoints with \`coding_run\` plus the fetch bridge, inspect status/body, and clean up disposable side-effecting test data when practical.
1185
+ ## Tools without arguments, and session tools
1416
1186
 
1417
- ## Tool Without Arguments
1187
+ A tool bound as an agent side's \`sessionStop\` ends the session when the model calls it. The binding reads the result text from one of the tool's arguments, so the tool itself can be small:
1418
1188
 
1419
1189
  \`\`\`typescript
1190
+ // agents/tools/close_conversation.ts
1191
+ import { defineTool } from '@standardagents/builder';
1192
+ import { z } from 'zod';
1193
+
1420
1194
  export default defineTool({
1421
- description: 'Get current server time',
1422
- execute: async (state) => {
1423
- return {
1424
- status: 'success',
1425
- result: new Date().toISOString(),
1426
- };
1195
+ description: 'Close the conversation once the customer confirms the issue is resolved.',
1196
+ args: z.object({
1197
+ summary: z.string().describe('One sentence that says how the issue was resolved'),
1198
+ }),
1199
+ execute: async (state, args) => {
1200
+ await state.setValue('resolution', args.summary);
1201
+ return { status: 'success', result: 'Conversation closed.' };
1427
1202
  },
1428
1203
  });
1429
1204
  \`\`\`
1430
1205
 
1431
- ## Error Handling
1432
-
1433
- Return errors gracefully instead of throwing:
1434
-
1435
1206
  \`\`\`typescript
1207
+ // agents/tools/approve_draft.ts
1208
+ import { defineTool } from '@standardagents/builder';
1209
+ import { z } from 'zod';
1210
+
1436
1211
  export default defineTool({
1437
- description: 'Fetch user data',
1438
- args: z.object({ userId: z.string() }),
1439
- execute: async (state, args) => {
1440
- try {
1441
- const user = await fetchUser(args.userId);
1442
- return { status: 'success', result: JSON.stringify(user) };
1443
- } catch (error) {
1444
- return {
1445
- status: 'error',
1446
- error: \`Failed to fetch user: \${error.message}\`,
1447
- };
1448
- }
1212
+ description: 'Approve the draft and finish. Call this only when the draft is ready to send.',
1213
+ args: z.object({
1214
+ final_text: z.string().describe('The approved reply, complete and ready to send'),
1215
+ }),
1216
+ execute: async () => {
1217
+ return { status: 'success', result: 'Draft approved.' };
1449
1218
  },
1450
1219
  });
1451
1220
  \`\`\`
1452
1221
 
1453
- ## Queueing Additional Tools
1454
-
1455
- Queue another tool to run after the current one:
1456
-
1457
1222
  \`\`\`typescript
1458
- import { queueTool } from '@standardagents/builder';
1223
+ // agents/tools/current_time.ts
1224
+ import { defineTool } from '@standardagents/builder';
1459
1225
 
1460
1226
  export default defineTool({
1461
- description: 'Create order and send confirmation',
1462
- args: z.object({ items: z.array(z.string()) }),
1463
- execute: async (state, args) => {
1464
- const order = await createOrder(args.items);
1465
-
1466
- // Queue email tool to run next
1467
- queueTool(state, 'send_confirmation_email', {
1468
- orderId: order.id,
1469
- email: state.thread.metadata.userEmail,
1470
- });
1471
-
1472
- return { status: 'success', result: JSON.stringify(order) };
1227
+ description: 'Get the current date and time in UTC as an ISO 8601 string.',
1228
+ execute: async () => {
1229
+ return { status: 'success', result: new Date().toISOString() };
1473
1230
  },
1474
1231
  });
1475
1232
  \`\`\`
1476
1233
 
1477
- ## Injecting Messages
1234
+ To end a session from code instead of a binding, call \`await state.stopSession({ result: '...' })\` inside \`execute\`.
1235
+
1236
+ ## Running another tool
1478
1237
 
1479
- Add messages without triggering re-execution:
1238
+ \`state.queueTool(name, args)\` runs another tool after the current one finishes. \`state.invokeTool(name, args)\` runs it now and returns its result.
1480
1239
 
1481
1240
  \`\`\`typescript
1482
- import { injectMessage } from '@standardagents/builder';
1241
+ // agents/tools/escalate_to_manager.ts
1242
+ import { defineTool } from '@standardagents/builder';
1243
+ import { z } from 'zod';
1483
1244
 
1484
1245
  export default defineTool({
1485
- description: 'Log audit event',
1486
- args: z.object({ event: z.string() }),
1246
+ description: 'Escalate the conversation to a manager. Opens a ticket and records the escalation.',
1247
+ args: z.object({
1248
+ reason: z.string().describe('Why a manager is needed'),
1249
+ }),
1487
1250
  execute: async (state, args) => {
1488
- await injectMessage(state, {
1251
+ const ticket = await state.invokeTool('create_ticket', {
1252
+ subject: 'Manager escalation',
1253
+ details: args.reason,
1254
+ });
1255
+ if (ticket.status === 'error') {
1256
+ return ticket;
1257
+ }
1258
+
1259
+ await state.injectMessage({
1489
1260
  role: 'system',
1490
- content: \`[AUDIT] \${args.event}\`,
1261
+ content: \`Escalated to a manager: \${args.reason}\`,
1262
+ silent: true,
1491
1263
  });
1492
- return { status: 'success', result: 'Logged' };
1264
+
1265
+ return { status: 'success', result: ticket.result ?? 'Escalated.' };
1493
1266
  },
1494
1267
  });
1495
1268
  \`\`\`
1496
1269
 
1497
- ## Stopping Sessions
1270
+ \`injectMessage\` adds a message to the thread without starting a turn. \`silent: true\` keeps it out of the chat view.
1498
1271
 
1499
- \`state.execution.stop()\` stops the current execution loop. To terminally end the active agent session, including blocking subagent runs that expect a session boundary, use \`state.stopSession()\`:
1272
+ ## Files and attachments
1500
1273
 
1501
- \`\`\`typescript
1502
- await state.stopSession({
1503
- result: 'Waiting for clarification',
1504
- notifyParent: 'Please clarify the request',
1505
- status: 'waiting for clarification',
1506
- });
1507
- \`\`\`
1274
+ Each thread has its own file system. A tool can write a file there and attach files to its result.
1508
1275
 
1509
- \`notifyParent\` queues a silent message to the parent when one exists; \`status\` is ignored without a parent registry entry. Omit \`notifyParent\` and \`status\` when stopping should not send a parent message or update child status.
1276
+ \`\`\`typescript
1277
+ // agents/tools/export_transcript.ts
1278
+ import { defineTool } from '@standardagents/builder';
1510
1279
 
1511
- ## Variables and Env
1280
+ export default defineTool({
1281
+ description: 'Save the conversation so far as a text file in the thread and return its path.',
1282
+ execute: async (state) => {
1283
+ const { messages } = await state.getMessages({ order: 'asc', limit: 200 });
1284
+ const transcript = messages
1285
+ .filter((message) => typeof message.content === 'string' && message.content.length > 0)
1286
+ .map((message) => \`\${message.role}: \${message.content}\`)
1287
+ .join('\\n');
1512
1288
 
1513
- Tools can declare variable requirements with \`variables\`, including \`required\` and \`description\` metadata:
1289
+ const path = '/exports/transcript.txt';
1290
+ await state.writeFile(path, transcript, 'text/plain');
1514
1291
 
1515
- \`\`\`typescript
1516
- export default defineTool({
1517
- description: 'Search uploaded documents',
1518
- args: z.object({ query: z.string() }),
1519
- execute: async (state, args) => {
1520
- const vectorStoreId = await state.env('VECTOR_STORE_ID');
1521
- const results = await searchVectorStore(vectorStoreId, args.query);
1522
- return { status: 'success', result: JSON.stringify(results) };
1292
+ return { status: 'success', result: JSON.stringify({ path }) };
1523
1293
  },
1524
- variables: [
1525
- {
1526
- name: 'VECTOR_STORE_ID',
1527
- type: 'text',
1528
- required: true,
1529
- scoped: false,
1530
- description: 'OpenAI Vector Store ID',
1531
- },
1532
- ],
1533
1294
  });
1534
1295
  \`\`\`
1535
1296
 
1536
- Use \`scoped: true\` for per-instance values that should not inherit from parent thread env (for example per-account OAuth tokens on subagents).
1297
+ ## Variables
1298
+
1299
+ Declare the variables a tool reads in \`variables\`, so the project records what the tool needs. Read a value with \`await state.env('NAME')\`.
1300
+
1301
+ Set \`scoped: true\` for a value that belongs to one thread and must not be inherited from a parent thread, such as a per-account token given to a subagent.
1302
+
1303
+ Values come from, in order: the thread, the agent's \`env\`, the prompt's \`env\`, the user, and the project's variables. Put secrets in \`.dev.vars\` locally and in the project's platform settings when deployed.
1537
1304
 
1538
- ## Returning Attachments
1305
+ ## Sandboxed code
1539
1306
 
1540
- Tools can return file attachments (e.g., generated images):
1307
+ Use \`state.runCode()\` to run JavaScript or TypeScript that a model or a user wrote. Never use \`eval\` or \`new Function\`. The sandbox has no network, file, or thread access. Pass in only the functions the code may call.
1541
1308
 
1542
1309
  \`\`\`typescript
1310
+ // agents/tools/calculate.ts
1311
+ import { defineTool } from '@standardagents/builder';
1312
+ import { z } from 'zod';
1313
+
1543
1314
  export default defineTool({
1544
- description: 'Generate a chart from data',
1545
- args: z.object({ data: z.array(z.number()) }),
1315
+ description: 'Evaluate a JavaScript arithmetic expression such as "(19.99 * 3) * 1.08" and return the number.',
1316
+ args: z.object({
1317
+ expression: z.string().describe('A JavaScript expression that evaluates to a number'),
1318
+ }),
1546
1319
  execute: async (state, args) => {
1547
- const chartImage = await generateChart(args.data);
1548
- return {
1549
- status: 'success',
1550
- result: 'Chart generated successfully',
1551
- attachments: [{
1552
- name: 'chart.png',
1553
- mimeType: 'image/png',
1554
- data: chartImage.base64,
1555
- width: 800,
1556
- height: 600,
1557
- }],
1558
- };
1320
+ const run = await state.runCode(\`export function main() { return (\${args.expression}); }\`, {
1321
+ execute: { fn: 'main', args: [] },
1322
+ });
1323
+
1324
+ if (run.status !== 'success') {
1325
+ return { status: 'error', error: run.error?.message ?? 'The expression could not be evaluated.' };
1326
+ }
1327
+ return { status: 'success', result: String(run.result) };
1559
1328
  },
1560
1329
  });
1561
1330
  \`\`\`
1562
1331
 
1563
- ## Best Practices
1564
-
1565
- - **Write clear descriptions** - LLMs decide tool usage based on descriptions
1566
- - **Describe all parameters** with \`.describe()\` in Zod schemas
1567
- - **Handle errors gracefully** - return error status, don't throw
1568
- - **Use snake_case** for file names (\`search_knowledge_base.ts\`)
1569
- - **Keep tools focused** - one task per tool
1570
- - **Use \`state.rootState\`** when queueing from sub-prompts
1332
+ ## Practices
1571
1333
 
1572
- ## Supported Zod Types
1573
-
1574
- - Primitives: \`string\`, \`number\`, \`boolean\`, \`null\`
1575
- - Enums: \`z.enum(['a', 'b', 'c'])\`
1576
- - Arrays: \`z.array(z.string())\`
1577
- - Objects: \`z.object({ ... })\`
1578
- - Optional: \`z.string().optional()\`
1579
- - Default: \`z.number().default(10)\`
1580
- - Nullable: \`z.string().nullable()\`
1334
+ - Write the description for the model: what the tool does, when to call it, and what it returns.
1335
+ - Keep one job per tool.
1336
+ - Research a third-party API from its current documentation before you write a tool for it.
1337
+ - Check \`state\` before you add a dependency. Files, key-value storage, scheduling, and user data are built in.
1338
+ - Never put a secret in a tool file. Read it with \`state.env()\`.
1581
1339
 
1582
1340
  ## Documentation
1583
1341
 
@@ -1585,522 +1343,404 @@ Full reference: https://docs.standardagentbuilder.com/api-reference/define/tool
1585
1343
  `;
1586
1344
 
1587
1345
  // src/docs/hooks.ts
1588
- var HOOKS_AGENTS_MD = `# Lifecycle Hooks
1589
-
1590
- Hooks intercept and modify agent execution at specific lifecycle points.
1591
-
1592
- ## Available Hooks
1593
-
1594
- | Hook | Signature | Purpose |
1595
- |------|-----------|---------|
1596
- | \`after_thread_created\` | \`(state: ThreadState) => void\` | Initialize a newly created thread before execution |
1597
- | \`after_subagent_created\` | \`(state: ThreadState, childState: ThreadState) => void\` | Run parent-side logic after a child thread is created |
1598
- | \`after_system_message\` | \`(state: ThreadState, systemMessage: string) => string \\| null \\| undefined\` | Modify the rendered system message before the history snapshot |
1599
- | \`filter_messages\` | \`(state: ThreadState, messages: Message[]) => Message[]\` | Build the request context from one stable history snapshot per model request (only the newest window when the prompt sets \`historyLimit\`) |
1600
- | \`prefilter_llm_history\` | \`(state: ThreadState, messages: LLMMessage[]) => LLMMessage[]\` | Modify transformed provider messages before dispatch |
1601
- | \`before_create_message\` | \`(state: ThreadState, message: Message) => Message\` | Modify message before database insert |
1602
- | \`after_create_message\` | \`(state: ThreadState, message: Message) => void\` | Run logic after message created |
1603
- | \`before_update_message\` | \`(state: ThreadState, id: string, updates: Record) => Record\` | Modify updates for a persisted message before its update |
1604
- | \`after_update_message\` | \`(state: ThreadState, message: Message) => void\` | Run logic after message updated |
1605
- | \`before_store_tool_result\` | \`(state: ThreadState, toolCall: HookToolCall, result: HookToolResult) => HookToolResult\` | Modify tool result before storage |
1606
- | \`after_tool_call_success\` | \`(state: ThreadState, toolCall: HookToolCall, result: HookToolResult) => HookToolResult \\| null\` | Process successful tool execution |
1607
- | \`after_tool_call_failure\` | \`(state: ThreadState, toolCall: HookToolCall, result: HookToolResult) => HookToolResult \\| null\` | Process failed tool execution |
1608
- | \`user_client_attached\` | \`(user: UserState, client: { id, kind? }) => void\` | User-scope: a socket attached to the user event stream |
1609
- | \`user_client_detached\` | \`(user: UserState, client: { id, kind? }) => void\` | User-scope: a socket detached |
1610
- | \`user_value_changed\` | \`(user: UserState, key: string) => void\` | User-scope: a durable user value changed (any writer) |
1611
- | \`tool_call_started\` | \`(state: ThreadState, toolCall: HookToolCall, progress?: string) => void\` | Observe a tool call when it starts streaming (before it runs) |
1612
- | \`tool_call_done\` | \`(state: ThreadState, toolCall: HookToolCall, result: HookToolResult) => void\` | Observe a tool call after it finishes (success or failure) |
1613
-
1614
- User-scope hooks receive \`UserState\`. All thread hooks receive \`ThreadState\`
1615
- first and \`HookExecutionContext\` last. The context source is prompt, agent, or
1616
- child provenance; child provenance includes \`relationshipName\`, resolved
1617
- \`agentName\`, and optional package signature.
1618
-
1619
- Packed thread hooks receive a \`ThreadState\` in their owning package namespace.
1620
- Every hook can use \`state.loadPrompt('own')\` to load a package-local prompt.
1621
- This includes \`before_create_message\`.
1622
-
1623
- ## Creating a Hook
1624
-
1625
- Each hook file exports a default \`defineHook\` call with three properties:
1346
+ var HOOKS_AGENTS_MD = `# Lifecycle hooks
1347
+
1348
+ A hook is a function the runtime calls at a fixed point in a thread's life: before a message is stored, before a model request is sent, after a tool runs. Use hooks for behavior that cuts across prompts, such as trimming history, redacting data, and logging.
1349
+
1350
+ ## Defining a hook
1351
+
1352
+ Each file exports one hook. \`hook\` is the point it runs at, \`id\` is its name, and \`execute\` is the function. The file name can be anything; \`id\` is what agents and prompts refer to. \`id\` must be \`snake_case\` and unique in the project.
1626
1353
 
1627
1354
  \`\`\`typescript
1355
+ // agents/hooks/limit_history.ts
1628
1356
  import { defineHook } from '@standardagents/builder';
1629
1357
 
1630
1358
  export default defineHook({
1631
- hook: 'filter_messages', // Hook type
1632
- id: 'limit_recent_messages', // Unique ID (snake_case)
1359
+ hook: 'filter_messages',
1360
+ id: 'limit_history',
1633
1361
  execute: async (state, messages) => {
1634
- return messages.slice(-20);
1635
- }
1362
+ return messages.slice(-40);
1363
+ },
1636
1364
  });
1637
1365
  \`\`\`
1638
1366
 
1639
- ## Hook Scoping
1367
+ A hook does nothing until an agent or a prompt lists its \`id\`:
1368
+
1369
+ \`\`\`text
1370
+ defineAgent({ name: 'support_agent', hooks: ['log_tool_use'], ... })
1371
+ definePrompt({ name: 'customer_support', hooks: ['limit_history'], ... })
1372
+ \`\`\`
1373
+
1374
+ - A prompt's \`hooks\` replace the agent's \`hooks\` while that prompt runs.
1375
+ - A prompt with no \`hooks\` uses the agent's.
1376
+ - Work that happens outside a prompt, such as creating the thread, uses the agent's.
1640
1377
 
1641
- Hooks are scoped to **prompts** or **agents** via their ID:
1378
+ ## Hook points
1642
1379
 
1643
- \`\`\`typescript
1644
- // In a prompt definition
1645
- definePrompt({
1646
- name: 'customer_support',
1647
- hooks: ['limit_recent_messages', 'redact_pii'],
1648
- // ...
1649
- });
1380
+ Hooks that change data return the new value.
1650
1381
 
1651
- // In an agent definition
1652
- defineAgent({
1653
- name: 'support_agent',
1654
- hooks: ['log_all_messages'],
1655
- // ...
1656
- });
1657
- \`\`\`
1382
+ | Hook | Arguments after \`state\` | Returns | Runs |
1383
+ |------|-------------------------|---------|------|
1384
+ | \`after_system_message\` | \`systemMessage: string\` | \`string\`, or nothing to keep it | After the system instructions are built for a request |
1385
+ | \`filter_messages\` | \`messages: Message[]\` | \`Message[]\` | Once per model request, on the stored history |
1386
+ | \`prefilter_llm_history\` | \`messages: LLMMessage[]\` | \`LLMMessage[]\` | Just before the request is sent, on provider-format messages |
1387
+ | \`before_create_message\` | \`message: Message\` | \`Message\` | Before a message is stored |
1388
+ | \`before_update_message\` | \`id: string, updates\` | \`updates\` | Before a stored message is changed |
1389
+ | \`before_store_tool_result\` | \`toolCall, result\` | \`result\` | Before a tool result is stored |
1390
+ | \`after_tool_call_success\` | \`toolCall, result\` | \`result\`, or \`null\` to drop the call | After a tool succeeds |
1391
+ | \`after_tool_call_failure\` | \`toolCall, result\` | \`result\`, or \`null\` to drop the call | After a tool fails |
1658
1392
 
1659
- - **Prompt hooks** take precedence over agent hooks
1660
- - If a prompt defines hooks, only those run (agent hooks are skipped)
1661
- - If no prompt hooks are defined, agent hooks run as fallback
1662
- - Enabled direct children contribute their declared \`parentHooks\`
1663
- - Ordering is child \`before_parent\`, prompt-or-agent base, child \`after_parent\`
1664
- - Child requirements load from the child's package and run on the parent state
1665
- - Disabled optional children contribute nothing; hidden enabled children still do
1666
- - Installing alone is inert; adding the child relationship activates its disclosed hooks
1667
- - Hooks are filtered by type at runtime
1668
- - At-rest operations have no active prompt and use agent-level hooks
1669
-
1670
- ## History and the Request Context
1671
-
1672
- Each provider request runs \`after_system_message\`, takes one stable persisted-history
1673
- snapshot, runs \`filter_messages\` exactly once, transforms the result, and then runs
1674
- \`prefilter_llm_history\`. These request-context hooks do not replace
1675
- \`state.execution.messageHistory\`. Messages injected by \`after_system_message\` enter
1676
- the current snapshot. Messages injected by either request-context hook enter the
1677
- following snapshot. Silent messages remain eligible unless a request-context hook removes
1678
- them.
1679
-
1680
- When the active prompt sets \`historyLimit\` (N), the snapshot and
1681
- \`state.execution.messageHistory\` hold only the newest top-level rows: at least N
1682
- and fewer than 2N, the cut moving N at a time. A hook looking for an older marker
1683
- (a compaction summary, a task header) may not see it; stored history is untouched.
1684
-
1685
- ## Create-Message Lifecycle
1686
-
1687
- \`before_create_message\` and \`after_create_message\` run for runtime insertion
1688
- paths, including \`injectMessage()\` (active and at rest), agent-handoff user
1689
- messages, and generated-asset path messages. A \`before_create_message\` hook
1690
- that throws during \`injectMessage()\` rejects the injection and nothing is
1691
- persisted. Other runtime insertions log the failure and keep the last good
1692
- message.
1693
-
1694
- ## File Naming
1695
-
1696
- Hook files can use **any name** \u2014 the hook type and ID are defined inside the file:
1393
+ Hooks that observe return nothing.
1697
1394
 
1698
- \`\`\`
1699
- agents/hooks/
1700
- \u251C\u2500\u2500 my_filter.ts # Could contain any hook type
1701
- \u251C\u2500\u2500 redact_sensitive_data.ts
1702
- \u251C\u2500\u2500 log_tool_usage.ts
1703
- \u2514\u2500\u2500 add_metadata.ts
1704
- \`\`\`
1395
+ | Hook | Arguments after \`state\` | Runs |
1396
+ |------|-------------------------|------|
1397
+ | \`after_thread_created\` | none | When a thread is created, before its first turn |
1398
+ | \`after_subagent_created\` | \`childState\` | In the parent, after a subagent thread is created |
1399
+ | \`after_create_message\` | \`message: Message\` | After a message is stored |
1400
+ | \`after_update_message\` | \`message: Message\` | After a stored message is changed |
1401
+ | \`tool_call_started\` | \`toolCall, progress?\` | When the model starts a tool call |
1402
+ | \`tool_call_done\` | \`toolCall, result\` | After a tool call finishes |
1403
+
1404
+ Every hook receives a final \`context\` argument that says whether the prompt, the agent, or a composed child agent listed it.
1405
+
1406
+ \`toolCall\` is \`{ id, type: 'function', function: { name, arguments } }\`, where \`arguments\` is a JSON string. \`result\` is \`{ status, result?, error?, attachments? }\`.
1705
1407
 
1706
1408
  ## Examples
1707
1409
 
1708
- ### Filter Messages (Limit Context)
1410
+ Redact data before it reaches the model:
1709
1411
 
1710
1412
  \`\`\`typescript
1413
+ // agents/hooks/redact_card_numbers.ts
1711
1414
  import { defineHook } from '@standardagents/builder';
1712
1415
 
1416
+ const CARD_NUMBER = /\\b(?:\\d[ -]?){13,16}\\b/g;
1417
+
1713
1418
  export default defineHook({
1714
- hook: 'filter_messages',
1715
- id: 'limit_to_20',
1419
+ hook: 'prefilter_llm_history',
1420
+ id: 'redact_card_numbers',
1716
1421
  execute: async (state, messages) => {
1717
- return messages.slice(-20);
1718
- }
1422
+ return messages.map((message) =>
1423
+ typeof message.content === 'string'
1424
+ ? { ...message, content: message.content.replace(CARD_NUMBER, '[card number removed]') }
1425
+ : message,
1426
+ );
1427
+ },
1719
1428
  });
1720
1429
  \`\`\`
1721
1430
 
1722
- ### Prefilter LLM History (Redact Content)
1431
+ Log every tool call. This hook returns \`result\` unchanged:
1723
1432
 
1724
1433
  \`\`\`typescript
1434
+ // agents/hooks/log_tool_use.ts
1725
1435
  import { defineHook } from '@standardagents/builder';
1726
1436
 
1727
1437
  export default defineHook({
1728
- hook: 'prefilter_llm_history',
1729
- id: 'redact_pii',
1730
- execute: async (state, messages) => {
1731
- return messages.map(msg => ({
1732
- ...msg,
1733
- content: typeof msg.content === 'string'
1734
- ? msg.content.replace(/\\b\\d{4}-\\d{4}-\\d{4}-\\d{4}\\b/g, '[REDACTED]')
1735
- : msg.content,
1736
- }));
1737
- }
1438
+ hook: 'after_tool_call_success',
1439
+ id: 'log_tool_use',
1440
+ execute: async (state, toolCall, result) => {
1441
+ console.log(\`[\${state.threadId}] \${toolCall.function.name} succeeded\`);
1442
+ return result;
1443
+ },
1738
1444
  });
1739
1445
  \`\`\`
1740
1446
 
1741
- ### Before Create Message (Add Metadata)
1447
+ In \`after_tool_call_success\` and \`after_tool_call_failure\`, returning \`null\` removes the tool call and its result from the history. A hook that only observes must return the \`result\` it was given.
1448
+
1449
+ Replace a failed result with a fallback the model can use:
1742
1450
 
1743
1451
  \`\`\`typescript
1452
+ // agents/hooks/knowledge_base_fallback.ts
1744
1453
  import { defineHook } from '@standardagents/builder';
1745
1454
 
1746
1455
  export default defineHook({
1747
- hook: 'before_create_message',
1748
- id: 'add_version_metadata',
1749
- execute: async (state, message) => {
1750
- if (message.role === 'user' && message.content) {
1751
- return { ...message, content: \`[v1.0] \${message.content}\` };
1456
+ hook: 'after_tool_call_failure',
1457
+ id: 'knowledge_base_fallback',
1458
+ execute: async (state, toolCall, result) => {
1459
+ if (toolCall.function.name !== 'search_knowledge_base') {
1460
+ return result;
1752
1461
  }
1753
- return message;
1754
- }
1462
+ return {
1463
+ status: 'success',
1464
+ result: JSON.stringify({ articles: [], note: 'The knowledge base is unavailable. Open a ticket instead.' }),
1465
+ };
1466
+ },
1755
1467
  });
1756
1468
  \`\`\`
1757
1469
 
1758
- ### Before Store Tool Result (Sanitize)
1470
+ Shorten large tool results before they are stored:
1759
1471
 
1760
1472
  \`\`\`typescript
1473
+ // agents/hooks/truncate_tool_results.ts
1761
1474
  import { defineHook } from '@standardagents/builder';
1762
1475
 
1476
+ const MAX_LENGTH = 8000;
1477
+
1763
1478
  export default defineHook({
1764
1479
  hook: 'before_store_tool_result',
1765
- id: 'sanitize_results',
1766
- execute: async (state, toolCall, toolResult) => {
1767
- // toolCall: { id, type: 'function', function: { name, arguments } }
1768
- // toolResult: { status, result?, error?, stack?, attachments? }
1769
- if (toolResult.result) {
1770
- return { ...toolResult, result: toolResult.result.replace(/secret/gi, '[REDACTED]') };
1480
+ id: 'truncate_tool_results',
1481
+ execute: async (state, toolCall, result) => {
1482
+ if (!result.result || result.result.length <= MAX_LENGTH) {
1483
+ return result;
1771
1484
  }
1772
- return toolResult;
1773
- }
1485
+ return { ...result, result: \`\${result.result.slice(0, MAX_LENGTH)}\\n[truncated]\` };
1486
+ },
1774
1487
  });
1775
1488
  \`\`\`
1776
1489
 
1777
- ### After Tool Call Success (Logging)
1778
-
1779
- > Warning: In \`after_tool_call_success\` / \`after_tool_call_failure\`,
1780
- > returning \`null\` does NOT mean "keep the original result" \u2014 it tells the
1781
- > runtime to **REMOVE the tool call from history and skip storing the result**
1782
- > (the assistant tool-call message is deleted). A side-effect-only hook MUST
1783
- > return the \`result\` it received.
1490
+ Set up a new thread:
1784
1491
 
1785
1492
  \`\`\`typescript
1493
+ // agents/hooks/record_thread_start.ts
1786
1494
  import { defineHook } from '@standardagents/builder';
1787
1495
 
1788
1496
  export default defineHook({
1789
- hook: 'after_tool_call_success',
1790
- id: 'log_tool_success',
1791
- execute: async (state, toolCall, result) => {
1792
- console.log(\`Tool \${toolCall.function.name} succeeded\`);
1793
- return result; // ALWAYS return the result \u2014 null would DELETE the tool call
1794
- }
1497
+ hook: 'after_thread_created',
1498
+ id: 'record_thread_start',
1499
+ execute: async (state) => {
1500
+ await state.setValue('started_at', new Date().toISOString());
1501
+ },
1795
1502
  });
1796
1503
  \`\`\`
1797
1504
 
1798
- ### After Tool Call Failure (Recovery)
1505
+ ## How a request is built
1506
+
1507
+ For each model request the runtime:
1508
+
1509
+ 1. Builds the system instructions and runs \`after_system_message\`.
1510
+ 2. Takes one snapshot of the stored history.
1511
+ 3. Runs \`filter_messages\` once on that snapshot.
1512
+ 4. Converts the result to the provider's format and runs \`prefilter_llm_history\`.
1513
+
1514
+ These hooks shape what the model sees. They do not change the stored history. With \`historyLimit\` set on the prompt, the snapshot holds only the newest messages, so a hook cannot see older ones.
1515
+
1516
+ ## Errors
1517
+
1518
+ A hook that throws is logged and skipped, and the data it was given is used unchanged. One exception lets a hook stop a turn on purpose: a \`prefilter_llm_history\` hook can throw an error with \`block: true\`. No model request is sent, and the error message is shown as the assistant's reply.
1799
1519
 
1800
1520
  \`\`\`typescript
1521
+ // agents/hooks/enforce_message_limit.ts
1801
1522
  import { defineHook } from '@standardagents/builder';
1802
1523
 
1524
+ const MAX_MESSAGES = 200;
1525
+
1803
1526
  export default defineHook({
1804
- hook: 'after_tool_call_failure',
1805
- id: 'weather_fallback',
1806
- execute: async (state, toolCall, result) => {
1807
- if (toolCall.function.name === 'fetch_weather') {
1808
- return {
1809
- status: 'success',
1810
- result: JSON.stringify({ temp: 'unavailable', fallback: true }),
1811
- };
1527
+ hook: 'prefilter_llm_history',
1528
+ id: 'enforce_message_limit',
1529
+ execute: async (state, messages) => {
1530
+ const { total } = await state.getMessages({ limit: 1 });
1531
+ if (total > MAX_MESSAGES) {
1532
+ throw Object.assign(new Error('This conversation has reached its limit. Start a new one.'), { block: true });
1812
1533
  }
1813
- return result; // keep the original error \u2014 null would DELETE the tool call
1814
- }
1534
+ return messages;
1535
+ },
1815
1536
  });
1816
1537
  \`\`\`
1817
1538
 
1818
- ## Hook Categories
1819
-
1820
- **Transformation hooks** (return modified data):
1821
- - \`after_system_message\` (return replacement string, or null/undefined for no change)
1822
- - \`filter_messages\`
1823
- - \`prefilter_llm_history\`
1824
- - \`before_create_message\`
1825
- - \`before_update_message\`
1826
- - \`before_store_tool_result\`
1827
- - \`after_tool_call_success\` (return the \u2014 possibly modified \u2014 result; \`null\` REMOVES the tool call from history)
1828
- - \`after_tool_call_failure\` (return the \u2014 possibly modified \u2014 result; \`null\` REMOVES the tool call from history)
1829
-
1830
- **Event hooks** (side effects only):
1831
- - \`after_thread_created\`
1832
- - \`after_subagent_created\`
1833
- - \`after_create_message\`
1834
- - \`after_update_message\`
1835
-
1836
- ## Best Practices
1837
-
1838
- - **Keep hooks fast** - target <100ms execution
1839
- - **Wrap in try-catch** - hooks continue on error (original data preserved)
1840
- - **Use unique IDs** - hook IDs must be snake_case and unique across the project
1841
- - **Scope appropriately** - assign hooks to prompts or agents that need them
1842
- - **Document purpose** with clear comments
1843
- - **Use for cross-cutting concerns** - logging, redaction, enrichment
1539
+ \`before_create_message\` is the other exception: when it throws during \`state.injectMessage()\`, the message is not stored.
1844
1540
 
1845
- ## Documentation
1541
+ ## User hooks
1846
1542
 
1847
- Full reference: https://docs.standardagentbuilder.com/api-reference/define/hook
1543
+ Three hooks run for a user instead of a thread. They receive a \`UserState\` (see \`agents/users/AGENTS.md\`), run for every user, and are not listed on an agent or prompt.
1848
1544
 
1849
- ## Blocking a step from a hook
1545
+ | Hook | Arguments | Runs |
1546
+ |------|-----------|------|
1547
+ | \`user_client_attached\` | \`user, client\` | A client connects to the user's event stream |
1548
+ | \`user_client_detached\` | \`user, client\` | A client disconnects |
1549
+ | \`user_value_changed\` | \`user, key\` | One of the user's durable values changes |
1850
1550
 
1851
- Hook failures are normally non-fatal (logged and ignored). A
1852
- \`prefilter_llm_history\` hook may deliberately BLOCK the step \u2014 the session
1853
- stops with a visible assistant message BEFORE any LLM request \u2014 by throwing an
1854
- error that carries \`block: true\`:
1551
+ ## Practices
1855
1552
 
1856
- \`\`\`typescript
1857
- throw Object.assign(new Error('Thread limit reached'), { block: true });
1858
- \`\`\`
1553
+ - Keep hooks fast. Request hooks run before every model request.
1554
+ - Return the value you were given when you have nothing to change.
1555
+ - Use a hook for behavior shared across prompts. Put behavior that belongs to one tool in that tool.
1859
1556
 
1860
- Use this for entitlement/policy gates (e.g. per-user concurrency leases).
1557
+ ## Documentation
1558
+
1559
+ Full reference: https://docs.standardagentbuilder.com/api-reference/define/hook
1861
1560
  `;
1862
1561
 
1863
1562
  // src/docs/api.ts
1864
- var API_AGENTS_MD = `# Thread-Specific API Endpoints
1563
+ var API_AGENTS_MD = `# Thread endpoints
1865
1564
 
1866
- Define custom API endpoints that operate on specific threads with access to the thread's ThreadState.
1565
+ A thread endpoint is an HTTP route that runs against one thread. Each file in this directory becomes a route under \`/api/threads/:threadId/\`, and receives that thread's \`ThreadState\`. Use endpoints to give your own front end or service access to a thread's data.
1867
1566
 
1868
- Unmatched thread endpoint paths return JSON with HTTP 404.
1869
- A method without a matching endpoint route also returns HTTP 404.
1567
+ ## Routing
1870
1568
 
1871
- ## File-Based Routing
1569
+ The file path sets the route. A suffix before \`.ts\` sets the method; a file without one answers \`GET\`.
1872
1570
 
1873
- | File Pattern | HTTP Method | Route |
1874
- |--------------|-------------|-------|
1875
- | \`name.ts\` | GET | \`/api/threads/:threadId/name\` |
1876
- | \`name.get.ts\` | GET | \`/api/threads/:threadId/name\` |
1877
- | \`name.post.ts\` | POST | \`/api/threads/:threadId/name\` |
1878
- | \`name.put.ts\` | PUT | \`/api/threads/:threadId/name\` |
1879
- | \`name.delete.ts\` | DELETE | \`/api/threads/:threadId/name\` |
1880
- | \`index.ts\` | GET | \`/api/threads/:threadId\` |
1881
- | \`nested/route.ts\` | GET | \`/api/threads/:threadId/nested/route\` |
1882
- | \`items/[id].ts\` | GET | \`/api/threads/:threadId/items/:id\` |
1571
+ | File | Method | Route |
1572
+ |------|--------|-------|
1573
+ | \`summary.ts\` | GET | \`/api/threads/:threadId/summary\` |
1574
+ | \`summary.get.ts\` | GET | \`/api/threads/:threadId/summary\` |
1575
+ | \`notes.post.ts\` | POST | \`/api/threads/:threadId/notes\` |
1576
+ | \`notes/[id].put.ts\` | PUT | \`/api/threads/:threadId/notes/:id\` |
1577
+ | \`notes/[id].delete.ts\` | DELETE | \`/api/threads/:threadId/notes/:id\` |
1883
1578
  | \`files/[*].ts\` | GET | \`/api/threads/:threadId/files/*\` |
1884
1579
 
1885
- Files without a method suffix default to GET. Method suffixes are case-insensitive and support \`.get.ts\`, \`.post.ts\`, \`.put.ts\`, \`.patch.ts\`, and \`.delete.ts\`.
1580
+ - Suffixes are \`.get\`, \`.post\`, \`.put\`, \`.patch\`, and \`.delete\`.
1581
+ - \`[name]\` matches one path segment and arrives as \`params.name\`.
1582
+ - \`[*]\` matches the rest of the path, slashes included, and arrives as \`params['*']\`.
1583
+ - A fixed route wins over \`[name]\`, and \`[name]\` wins over \`[*]\`.
1584
+ - A path or method with no matching file returns HTTP 404 as JSON.
1886
1585
 
1887
- Dynamic segments use \`[name]\` and are passed to the handler as \`params.name\`. Catch-all segments use \`[*]\` and are passed as \`params['*']\`; the catch-all value may contain \`/\` for nested paths. Static routes match before dynamic routes, and dynamic routes match before catch-all routes.
1586
+ ## Defining an endpoint
1888
1587
 
1889
- ## Basic Example
1588
+ Import \`defineThreadEndpoint\` from \`@standardagents/spec\`.
1890
1589
 
1891
1590
  \`\`\`typescript
1892
- // agents/api/status.get.ts -> GET /api/threads/:threadId/status
1591
+ // agents/api/summary.get.ts
1893
1592
  import { defineThreadEndpoint } from '@standardagents/spec';
1894
1593
 
1895
- export default defineThreadEndpoint(async (req, state, params) => {
1896
- const { messages, total } = await state.getMessages();
1594
+ export default defineThreadEndpoint(async (req, state) => {
1595
+ const { messages, total } = await state.getMessages({ limit: 5 });
1596
+
1897
1597
  return Response.json({
1898
1598
  threadId: state.threadId,
1599
+ agent: state.agentId,
1899
1600
  messageCount: total,
1900
- status: 'active',
1901
- params,
1601
+ latest: messages.map((message) => ({ role: message.role, content: message.content })),
1902
1602
  });
1903
1603
  });
1904
1604
  \`\`\`
1905
1605
 
1906
- ## Handler Parameters
1606
+ The function receives:
1907
1607
 
1908
1608
  | Parameter | Type | Description |
1909
1609
  |-----------|------|-------------|
1910
- | \`req\` | \`Request\` | The incoming HTTP request |
1911
- | \`state\` | \`ThreadState\` | Thread state with messages, identity, \`arguments\`, file system, \`runCode\`, etc. |
1912
- | \`params\` | \`Record<string, string>\` | Route parameters captured from \`[name]\` and \`[*]\` path segments |
1913
- | \`auth\` | \`ThreadEndpointAuth\` | The request's authentication context: \`caller\` (the authenticated identity, or \`null\` for anonymous requests) and \`threadAccess\` (whether the caller passes the runtime's access policy for this thread) |
1610
+ | \`req\` | \`Request\` | The HTTP request |
1611
+ | \`state\` | \`ThreadState\` | The thread. See \`agents/tools/AGENTS.md\` for its members |
1612
+ | \`params\` | \`Record<string, string>\` | Values from \`[name]\` and \`[*]\` segments |
1613
+ | \`auth\` | \`{ caller, threadAccess }\` | Who is calling, and whether they may access this thread |
1914
1614
 
1915
- ## Access Levels
1615
+ The thread is at rest while an endpoint runs, so \`state.execution\` is \`null\` and \`state.stopSession()\` is not available.
1916
1616
 
1917
- Endpoints are \`authenticated\` by default: the runtime authenticates the
1918
- request and enforces thread access before your handler runs, and \`auth.caller\`
1919
- is always present. Declare \`{ access: 'public' }\` to accept anonymous
1920
- requests \u2014 the runtime still resolves \`auth.caller\` when credentials are
1921
- presented, so one endpoint can serve both audiences:
1617
+ ## Reading a request body
1922
1618
 
1923
1619
  \`\`\`typescript
1924
- // agents/api/shared/[*].ts -> GET /api/threads/:threadId/shared/<path>
1620
+ // agents/api/notes.post.ts
1925
1621
  import { defineThreadEndpoint } from '@standardagents/spec';
1622
+ import { z } from 'zod';
1926
1623
 
1927
- export default defineThreadEndpoint(
1928
- async (req, state, params, auth) => {
1929
- if (!auth.threadAccess && !isShared(params['*'])) {
1930
- return new Response(null, { status: 404 });
1931
- }
1932
- // serve the file...
1933
- },
1934
- { access: 'public' }
1935
- );
1936
- \`\`\`
1937
-
1938
- A public endpoint is reachable by anyone who has the URL \u2014 treat it like an
1939
- internet-facing route: validate the path, reject early, and never echo
1940
- private state to anonymous callers.
1941
-
1942
- Note: \`state.execution\` is always \`null\` in endpoints since the thread is at rest. In thread endpoints, \`runCode\` forwards functions in imports, globals, and execution arguments as callable proxies. Call \`run.terminate(reason)\` to stop the sandbox. A caller disconnect also stops the run. When \`report\` is supplied, the host receives reports during the run and \`run.reports\` updates as reports arrive. \`state.emit(event, data)\` broadcasts a custom WebSocket event from endpoints as well as active executions; use it as an invalidation hint after persisting durable state, not as a replacement for reconnect recovery.
1943
-
1944
- ## ThreadState Methods
1945
-
1946
- \`getMessages()\` returns messages newest first by default. Set \`order: 'asc'\` for oldest first.
1947
- \`getMessage(id)\` finds persisted messages at any depth. It also finds pending messages from the message list. It returns \`null\` for unknown IDs.
1948
- \`updateMessage(id, updates)\` updates a persisted message. It returns the updated \`Message\`.
1949
- Updates to missing or pending messages fail before update hooks run.
1950
- Message results include \`reasoning_content\`, \`reasoning_details\`, \`prompt\`, and available subagent fields: \`subagent_id\`, \`subagent_name\`, \`subagent_title\`, \`subagent_description\`, \`subagent_status\`, \`subagent_relationship\`, \`subagent_blocking\`, \`subagent_thread_name\`, and \`subagent_spawn_group_id\`.
1951
-
1952
- Access thread data through the state object:
1953
-
1954
- \`\`\`typescript
1955
- import { defineThreadEndpoint } from '@standardagents/spec';
1624
+ const NoteInput = z.object({
1625
+ text: z.string().min(1).max(2000),
1626
+ });
1956
1627
 
1957
1628
  export default defineThreadEndpoint(async (req, state) => {
1958
- // Thread identity
1959
- console.log('Thread ID:', state.threadId);
1960
- console.log('Agent ID:', state.agentId);
1961
- console.log('User ID:', state.userId);
1962
- console.log('Arguments:', state.arguments);
1963
-
1964
- // Message operations
1965
- const { messages, total, hasMore } = await state.getMessages({
1966
- limit: 100,
1967
- offset: 0,
1968
- order: 'desc',
1969
- });
1970
-
1971
- // Thread env (thread scope + active descendants)
1972
- const approvalGate = await state.env('TOPDOWN_APPROVAL_GATE');
1973
- await state.setEnv('TOPDOWN_APPROVAL_GATE', approvalGate || 'open');
1974
-
1975
- // Durable per-thread JSON values
1976
- const exports = (await state.getValue<number>('export_count')) ?? 0;
1977
- await state.setValue('export_count', exports + 1);
1978
- state.emit('export_count_changed', { count: exports + 1 });
1629
+ const input = NoteInput.safeParse(await req.json().catch(() => null));
1630
+ if (!input.success) {
1631
+ return Response.json({ error: 'Send a JSON body with a "text" string.' }, { status: 400 });
1632
+ }
1979
1633
 
1980
- // File system operations
1981
- const files = await state.findFiles('**/*.json');
1982
- const data = await state.readFile('/data/config.json');
1634
+ const note = { id: crypto.randomUUID(), text: input.data.text, createdAt: new Date().toISOString() };
1983
1635
 
1984
- // Resource loading
1985
- const agent = await state.loadAgent('my-agent');
1986
- const prompt = await state.loadPrompt('my-prompt');
1636
+ await state.setValue(\`note:\${note.id}\`, note);
1637
+ state.emit('notes_changed', { id: note.id });
1987
1638
 
1988
- return Response.json({ messages, total });
1639
+ return Response.json(note, { status: 201 });
1989
1640
  });
1990
1641
  \`\`\`
1991
1642
 
1992
- \`state.loadModel\`, \`state.loadPrompt\`, and \`state.loadAgent\` enforce package
1993
- visibility. Global code can load unpacked definitions and packed entry agents.
1994
- Packed code can load its own definitions and other packages' entry agents.
1995
- A packed model cannot fall back to a host model with the same short name.
1996
- A refused load returns the same not-found error as a missing definition.
1643
+ Each note has its own key. Requests to a thread endpoint can run at the same time, and \`getValue()\` followed by \`setValue()\` is two separate steps. An endpoint that read one shared list, changed it, and wrote it back could overwrite a note that another request had just added. Give each record its own key, as above, so no request rewrites another's data.
1644
+
1645
+ \`state.emit()\` sends a live event to clients connected to the thread. Events are not stored. A client that reconnects must read the data again.
1997
1646
 
1998
- ## POST with Request Body
1647
+ ## Route parameters
1999
1648
 
2000
1649
  \`\`\`typescript
2001
- // agents/api/export.post.ts -> POST /api/threads/:threadId/export
1650
+ // agents/api/notes/[id].delete.ts
2002
1651
  import { defineThreadEndpoint } from '@standardagents/spec';
2003
1652
 
2004
- export default defineThreadEndpoint(async (req, state) => {
2005
- const body = await req.json();
2006
- const { format = 'json' } = body;
2007
-
2008
- const { messages } = await state.getMessages({ order: 'asc' });
2009
-
2010
- if (format === 'csv') {
2011
- const csv = messages.map(m => \`\${m.role},\${m.content}\`).join('\\n');
2012
- return new Response(csv, {
2013
- headers: { 'Content-Type': 'text/csv' },
2014
- });
1653
+ export default defineThreadEndpoint(async (req, state, params) => {
1654
+ const key = \`note:\${params.id}\`;
1655
+ if ((await state.getValue(key)) === null) {
1656
+ return Response.json({ error: 'No note has that ID.' }, { status: 404 });
2015
1657
  }
2016
1658
 
2017
- return Response.json(messages);
1659
+ await state.setValue(key, null);
1660
+ state.emit('notes_changed', { id: params.id });
1661
+
1662
+ return new Response(null, { status: 204 });
2018
1663
  });
2019
1664
  \`\`\`
2020
1665
 
2021
- ## Nested Routes
1666
+ Setting a value to \`null\` deletes its key.
2022
1667
 
2023
- Create directories for nested routes:
1668
+ ## Access
2024
1669
 
2025
- \`\`\`
2026
- agents/api/
2027
- \u251C\u2500\u2500 status.get.ts -> GET /api/threads/:threadId/status
2028
- \u251C\u2500\u2500 export.post.ts -> POST /api/threads/:threadId/export
2029
- \u2514\u2500\u2500 messages/
2030
- \u251C\u2500\u2500 count.ts -> GET /api/threads/:threadId/messages/count
2031
- \u251C\u2500\u2500 [id].ts -> GET /api/threads/:threadId/messages/:id
2032
- \u2514\u2500\u2500 [*].ts -> GET /api/threads/:threadId/messages/*
2033
- \`\`\`
1670
+ An endpoint requires an authenticated caller who may access the thread. The runtime checks both before your function runs, and \`auth.caller\` is always set.
2034
1671
 
2035
- ## Error Handling
2036
-
2037
- AgentBuilder returns JSON for every failed request under \`/api/\`.
2038
- The response preserves the HTTP status.
2039
- The body uses \`{ "error": { "code": "internal_error", "message": "..." }, "requestId": "..." }\`.
2040
- The \`X-Request-ID\` header matches \`requestId\`.
2041
- Failed responses use \`Cache-Control: no-store\`.
2042
-
2043
- An uncaught exception returns HTTP 500 with code \`internal_error\`.
2044
- The server log contains the exception and request ID.
2045
- The response message omits exception details.
2046
- Authentication infrastructure failures return HTTP 503 with code \`auth_unavailable\`.
2047
- Platform network failures, unexpected HTTP statuses, and invalid payloads return HTTP 502 with code \`platform_unreachable\`.
2048
- Check \`PLATFORM_ENDPOINT\` and platform availability after that error.
2049
- An explicit platform credential or membership denial uses code \`platform_auth_rejected\` during local bootstrap.
2050
- Unknown API paths return HTTP 404 with code \`not_found\`.
2051
-
2052
- Read \`error.message\` to display a failure.
2053
- Include \`requestId\` in a support report.
2054
- Returned legacy JSON errors retain their extra fields and response headers.
2055
- The router converts their \`error\` field to the structured envelope.
2056
- A legacy string error with a separate message retains its code in the envelope.
2057
- Its separate message becomes the envelope message.
2058
- WebSocket upgrades and successful responses retain their existing behavior.
2059
- This error contract is an AgentBuilder extension.
2060
-
2061
- Handlers can return domain errors with an appropriate HTTP status:
1672
+ Pass \`{ access: 'public' }\` to accept requests with no credentials. \`auth.caller\` is then \`null\` for an anonymous request, and \`auth.threadAccess\` says whether a signed-in caller may access the thread.
2062
1673
 
2063
1674
  \`\`\`typescript
2064
- return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
1675
+ // agents/api/files/[*].ts
1676
+ import { defineThreadEndpoint } from '@standardagents/spec';
1677
+
1678
+ export default defineThreadEndpoint(
1679
+ async (req, state, params, auth) => {
1680
+ const path = \`/\${params['*'] ?? ''}\`;
1681
+ const isShared = path.startsWith('/public/') && !path.includes('..');
1682
+ if (!auth.threadAccess && !isShared) {
1683
+ return new Response(null, { status: 404 });
1684
+ }
1685
+
1686
+ const [data, stat] = await Promise.all([state.readFile(path), state.statFile(path)]);
1687
+ if (!data || !stat) {
1688
+ return new Response(null, { status: 404 });
1689
+ }
1690
+
1691
+ return new Response(data, {
1692
+ headers: {
1693
+ 'Content-Type': stat.mimeType,
1694
+ 'Content-Disposition': 'attachment',
1695
+ 'X-Content-Type-Options': 'nosniff',
1696
+ },
1697
+ });
1698
+ },
1699
+ { access: 'public' },
1700
+ );
2065
1701
  \`\`\`
2066
1702
 
2067
- Let unexpected exceptions reach the runtime boundary.
1703
+ Anyone with the URL can call a public endpoint. Validate the path, and never return private thread data to an anonymous caller.
2068
1704
 
2069
- ## File System Operations
1705
+ Serve thread files as downloads, as this example does. Endpoints share an origin with the builder UI and its APIs. An HTML or SVG file shown inline would run its scripts with the access of whoever opened the link. \`Content-Disposition: attachment\` makes the browser save the file, and \`X-Content-Type-Options: nosniff\` stops it from guessing a different type.
2070
1706
 
2071
- \`\`\`typescript
2072
- import { defineThreadEndpoint } from '@standardagents/spec';
1707
+ ## Thread files
2073
1708
 
2074
- export default defineThreadEndpoint(async (req, state) => {
2075
- // Read files
2076
- const content = await state.readFile('/data/file.txt');
1709
+ Each thread has its own file system, reached through \`state\`:
1710
+
1711
+ | Method | Does |
1712
+ |--------|------|
1713
+ | \`readFile(path)\` | Returns the file's bytes as an \`ArrayBuffer\`, or \`null\` |
1714
+ | \`writeFile(path, data, mimeType)\` | Creates or replaces a file |
1715
+ | \`statFile(path)\` | Returns the file's details, or \`null\` |
1716
+ | \`readdirFile(path)\` | Returns \`{ entries }\` for a directory |
1717
+ | \`moveFile(from, to)\` / \`renameFile(path, newName)\` | Moves or renames. Does not overwrite |
1718
+ | \`unlinkFile(path)\` | Deletes a file |
1719
+ | \`findFiles(pattern)\` | Returns \`{ paths }\` matching a glob such as \`**/*.json\` |
1720
+ | \`grepFiles(pattern)\` | Searches file contents |
2077
1721
 
2078
- // Write files
2079
- await state.writeFile('/output/result.json', JSON.stringify(data), 'application/json');
1722
+ ## Errors
2080
1723
 
2081
- // List directory
2082
- const { entries } = await state.readdirFile('/data');
1724
+ Return an error as JSON with the right status:
2083
1725
 
2084
- // Move or rename files and directories (does not overwrite an existing destination)
2085
- await state.moveFile('/data/file.txt', '/archive/file.txt');
2086
- await state.renameFile('/output/result.json', 'final.json');
1726
+ \`\`\`text
1727
+ return Response.json({ error: 'Send a JSON body with a "text" string.' }, { status: 400 });
1728
+ \`\`\`
2087
1729
 
2088
- // Search files
2089
- const matches = await state.grepFiles('pattern');
2090
- const paths = await state.findFiles('**/*.ts');
1730
+ The runtime rewrites every failed \`/api/\` response into one shape, and adds a request ID:
2091
1731
 
2092
- return Response.json({ success: true });
2093
- });
1732
+ \`\`\`text
1733
+ { "error": { "code": "internal_error", "message": "..." }, "requestId": "..." }
2094
1734
  \`\`\`
2095
1735
 
2096
- ## Best Practices
1736
+ Let unexpected exceptions throw. The caller receives HTTP 500 with code \`internal_error\` and no exception details; the dev server terminal and the deployed project's logs hold the exception and the same request ID.
2097
1737
 
2098
- - **Keep handlers fast** - endpoints are in the request path
2099
- - **Paginate large queries** - use limit/offset for messages
2100
- - **Validate input data** - check request body before processing
2101
- - **Use descriptive file names** - \`export.post.ts\` not \`ep1.ts\`
2102
- - **Return proper status codes** - 200, 400, 404, 500
2103
- - **Import from spec** - use \`@standardagents/spec\` for \`defineThreadEndpoint\`
1738
+ ## Practices
1739
+
1740
+ - Validate request bodies before you use them.
1741
+ - Page large reads with \`limit\` and \`offset\`.
1742
+ - Name files for what they do: \`export.post.ts\`, not \`ep1.ts\`.
1743
+ - Keep endpoints quick. Start long work with \`state.queueMessage()\` and let the agent do it.
2104
1744
 
2105
1745
  ## Documentation
2106
1746
 
@@ -2108,133 +1748,250 @@ Full reference: https://docs.standardagentbuilder.com/core-concepts/api
2108
1748
  `;
2109
1749
 
2110
1750
  // src/docs/users.ts
2111
- var USERS_AGENTS_MD = `# User Endpoints
1751
+ var USERS_AGENTS_MD = `# User endpoints
2112
1752
 
2113
- Define user-scoped API endpoints \u2014 HTTP handlers that operate on a USER
2114
- (durable cross-thread state) rather than a single thread. They execute inside
2115
- the per-user hub with a serialization guarantee: at most one user endpoint
2116
- runs at a time per user, and each observes all prior completed writes. That
2117
- makes this directory the home for quotas, leases, counters, registries, and
2118
- any other user-level logic where lost updates matter.
1753
+ A user endpoint is an HTTP route that runs for one user instead of one thread. Each file in this directory becomes a route under \`/api/users/me/\`. Use user endpoints for data that spans a user's threads: preferences, quotas, counters, and registries.
2119
1754
 
2120
- Specification: https://standardagentspec.com/0.2.0/specification/users
1755
+ For one user, only one user endpoint runs at a time, and each run sees every write made by the runs before it. That makes this the safe place to check a limit and then update it.
2121
1756
 
2122
- ## File-Based Routing
1757
+ ## Routing
2123
1758
 
2124
- | File Pattern | HTTP Method | Route |
2125
- |--------------|-------------|-------|
2126
- | \`name.ts\` | GET | \`/api/users/me/name\` |
2127
- | \`name.post.ts\` | POST | \`/api/users/me/name\` |
2128
- | \`pkg/name.post.ts\` | POST | \`/api/users/me/pkg/name\` |
1759
+ Routing follows the same rules as thread endpoints: the path sets the route, a suffix sets the method, and \`[name]\` and \`[*]\` capture segments.
1760
+
1761
+ | File | Method | Route |
1762
+ |------|--------|-------|
1763
+ | \`preferences.ts\` | GET | \`/api/users/me/preferences\` |
1764
+ | \`preferences.put.ts\` | PUT | \`/api/users/me/preferences\` |
1765
+ | \`quota/spend.post.ts\` | POST | \`/api/users/me/quota/spend\` |
2129
1766
  | \`items/[id].ts\` | GET | \`/api/users/me/items/:id\` |
2130
- | \`files/[*].ts\` | GET | \`/api/users/me/files/*\` |
2131
1767
 
2132
- Routing rules match thread endpoints (method suffixes, nested directories,
2133
- \`[param]\`, \`[*]\`). Root your endpoints in a directory named for your
2134
- package so installed packages coexist. Administrators can address any user's
2135
- endpoints at \`/api/users/:id/...\`. The \`:id\` is the instance user id; an
2136
- administrator who knows only an email resolves it with
2137
- \`GET /api/users/lookup?email=<email>\` (\`{ users: [{ id, username, email, source }] }\`).
1768
+ \`me\` is the signed-in caller. An administrator can call the same endpoints for another user at \`/api/users/:id/...\`.
1769
+
1770
+ ## Defining a user endpoint
2138
1771
 
2139
- ## Basic Example
1772
+ Import \`defineUserEndpoint\` from \`@standardagents/spec\`. The function receives the request, a \`UserState\`, and the route parameters.
2140
1773
 
2141
1774
  \`\`\`typescript
2142
- // agents/users/quota.post.ts \u2192 POST /api/users/me/quota
1775
+ // agents/users/quota/spend.post.ts
2143
1776
  import { defineUserEndpoint } from '@standardagents/spec';
1777
+ import { z } from 'zod';
1778
+
1779
+ const SpendInput = z.object({
1780
+ amount: z.number().int().positive(),
1781
+ });
1782
+
1783
+ const DAILY_LIMIT = 100;
2144
1784
 
2145
- export default defineUserEndpoint(async (req, user, params) => {
2146
- const used = (await user.getValue<number>('quota.used')) ?? 0;
2147
- const limit = Number(await user.getValue('quota.limit')) || 0;
2148
- const { amount } = await req.json();
2149
- if (used + amount > limit) {
2150
- return Response.json({ ok: false, used, limit }, { status: 429 });
1785
+ export default defineUserEndpoint(async (req, user) => {
1786
+ const input = SpendInput.safeParse(await req.json().catch(() => null));
1787
+ if (!input.success) {
1788
+ return Response.json({ error: 'Send a JSON body with a positive integer "amount".' }, { status: 400 });
2151
1789
  }
2152
- await user.setValue('quota.used', used + amount);
2153
- user.emit('quota_changed', { used: used + amount, limit });
2154
- return Response.json({ ok: true });
1790
+
1791
+ const key = \`quota.\${new Date().toISOString().slice(0, 10)}\`;
1792
+ const used = (await user.getValue<number>(key)) ?? 0;
1793
+ if (used + input.data.amount > DAILY_LIMIT) {
1794
+ return Response.json({ ok: false, used, limit: DAILY_LIMIT }, { status: 429 });
1795
+ }
1796
+
1797
+ await user.setValue(key, used + input.data.amount);
1798
+ user.emit('quota_changed', { used: used + input.data.amount, limit: DAILY_LIMIT });
1799
+
1800
+ return Response.json({ ok: true, used: used + input.data.amount, limit: DAILY_LIMIT });
2155
1801
  });
2156
1802
  \`\`\`
2157
1803
 
2158
1804
  ## UserState
2159
1805
 
2160
- Handlers receive a \`UserState\`: \`userId\`, \`identity()\`, durable values
2161
- (\`getValue\`/\`setValue\`/\`listValues\`), \`env()\`, \`emit()\` (broadcast on
2162
- the user event stream at \`/api/users/me/stream\`), \`listThreads()\`,
2163
- \`fetch(grant, url, init)\` (privileged fetch through a credential grant), and
2164
- \`thread(threadId)\` (raise one of the user's threads at rest).
2165
-
2166
- \`user.identity()\` returns a \`UserIdentity\`. Its \`username\` and \`role\` fields may be absent or null.
2167
-
2168
- ## User Rooms
2169
-
2170
- A \`users/pkg/room.ts\` file may export \`defineUserRoom\` to handle hub frames
2171
- under the same per-user serialization as user endpoints. It is not HTTP-routed.
2172
- \`user.clients()\` lists admitted connections; \`user.send(clientId, frame)\`
2173
- targets one admitted client. Automatic admission preserves reconnect behavior.
2174
-
2175
- For application proofs beyond the user's transport authentication, declare
2176
- \`admission: 'explicit'\`. Pending sockets cannot displace an admitted client or
2177
- receive its targeted traffic, broadcasts or wake requests. Require both
2178
- \`user.roomAttachments\` and \`connection.attachmentId\`; fail closed if absent.
2179
- Use \`user.roomAttachments.send(connection.attachmentId, frame)\` for challenges
2180
- and replies. After verifying the application's proof, call
2181
- \`user.roomAttachments.admit(connection.attachmentId)\`; false means the socket
2182
- is gone. Reject a failed proof with
2183
- \`user.roomAttachments.close(connection.attachmentId, 4003, "Proof rejected")\`;
2184
- this closes that socket and reports its detach. Never use a client id or timestamp as the physical attachment identity.
2185
-
2186
- In explicit mode, \`onPresence\` reports every physical attach/detach, including
2187
- pending and superseded sockets, without reconnect grace. Discard challenges on
2188
- detach and application restart. Runtime hibernation preserves attachment ids
2189
- and admission; reconnect uses a new id, and restored legacy sockets start pending.
2190
- The runtime supplies admission mechanics; your room owns authentication policy.
2191
-
2192
- ## Application Roles
2193
-
2194
- Read \`(await user.identity()).roles ?? []\` to authorize application operations.
2195
- Hosted roles are assigned in Platform \u2192 project Settings \u2192 Users and pushed
2196
- to the instance. They are separate from the \`admin\`/\`user\`/\`api\` access
2197
- level and built-in permissions. Role strings are case-sensitive; their meaning
2198
- belongs to your application. A \`:*\` suffix does not match anything implicitly.
1806
+ | Member | Does |
1807
+ |--------|------|
1808
+ | \`userId\` | The user's ID |
1809
+ | \`identity()\` | Returns the user's profile: \`id\`, \`username\`, \`email\`, \`role\`, \`roles\` |
1810
+ | \`getValue(key)\` / \`setValue(key, value)\` | Durable JSON values for this user. A \`null\` value deletes the key |
1811
+ | \`listValues({ prefix, limit, offset })\` | Lists the user's values |
1812
+ | \`env(name)\` | Reads a variable |
1813
+ | \`emit(event, data)\` | Sends a live event to the user's connected clients |
1814
+ | \`listThreads({ agentId, limit, offset })\` | Lists the user's threads |
1815
+ | \`thread(threadId)\` | Returns the \`ThreadState\` of one of the user's threads |
1816
+
1817
+ Do not store credentials in user values. They are plain JSON.
1818
+
1819
+ ## Calling a user endpoint from a thread
1820
+
1821
+ Tools, hooks, handlers, and thread endpoints reach the user through \`state.user()\`. \`call()\` runs a user endpoint by its path under \`agents/users/\`, with the same one-at-a-time guarantee.
2199
1822
 
2200
1823
  \`\`\`typescript
2201
- const identity = await user.identity();
2202
- const roles = identity.roles ?? [];
2203
- if (!roles.includes('reports:read')) {
2204
- return Response.json({ error: 'Forbidden' }, { status: 403 });
2205
- }
2206
- \`\`\`
1824
+ // agents/tools/spend_quota.ts
1825
+ import { defineTool } from '@standardagents/builder';
1826
+ import { z } from 'zod';
2207
1827
 
2208
- Check roles in each request or execution that performs a protected operation.
2209
- Identity is a snapshot within that execution; pushed role changes take effect
2210
- on the next request or execution. Missing roles grant no access. Never trust
2211
- roles supplied in a request body or user-writable KV.
1828
+ export default defineTool({
1829
+ description: 'Spend units of the user\\'s daily quota before doing metered work. Fails when the quota is used up.',
1830
+ args: z.object({
1831
+ amount: z.number().int().positive().describe('How many units to spend'),
1832
+ }),
1833
+ execute: async (state, args) => {
1834
+ const user = await state.user();
1835
+ if (!user) {
1836
+ return { status: 'error', error: 'This thread has no user.', error_code: 'no_user' };
1837
+ }
2212
1838
 
2213
- ## Caller Channels
1839
+ const response = await user.call('quota/spend', { amount: args.amount });
1840
+ if (!response.ok) {
1841
+ return { status: 'error', error: 'The daily quota is used up.', error_code: 'quota_exceeded' };
1842
+ }
1843
+ return { status: 'success', result: await response.text() };
1844
+ },
1845
+ });
1846
+ \`\`\`
2214
1847
 
2215
- Handlers can distinguish who invoked them via the
2216
- \`X-Standard-Agents-Caller\` request header, which the runtime stamps and
2217
- un-forgeably overwrites on public routes: \`user\` (the user's own HTTP
2218
- request), \`admin\` (administrative addressing), or \`thread\` (a
2219
- runtime-internal \`state.user().call()\`). Gate enforcement-critical
2220
- mutations (quotas, leases) on the \`thread\` channel.
1848
+ A user endpoint can tell how it was called from the \`X-Standard-Agents-Caller\` request header, which the runtime sets and callers cannot forge: \`user\` for the user's own HTTP request, \`admin\` for an administrator, and \`thread\` for a call from \`state.user().call()\`. Check for \`thread\` in an endpoint that only your agents should be able to change.
2221
1849
 
2222
- ## Reaching the User Scope from Threads
1850
+ ## Roles
2223
1851
 
2224
- Tools, hooks, and thread endpoints call user endpoints through the thread's
2225
- user handle:
1852
+ A user's application roles are strings assigned in the project's user settings on the platform. Their meaning is up to you. Read them from the identity and check them in every request that needs one:
2226
1853
 
2227
1854
  \`\`\`typescript
2228
- const user = await state.user();
2229
- await user?.call('quota', { amount: 1 }); // serialized, atomic
1855
+ // agents/users/reports.ts
1856
+ import { defineUserEndpoint } from '@standardagents/spec';
1857
+
1858
+ export default defineUserEndpoint(async (req, user) => {
1859
+ const identity = await user.identity();
1860
+ const roles = identity.roles ?? [];
1861
+ if (!roles.includes('reports:read')) {
1862
+ return Response.json({ error: 'Forbidden' }, { status: 403 });
1863
+ }
1864
+
1865
+ const threads = await user.listThreads({ limit: 20 });
1866
+ return Response.json({ threads });
1867
+ });
2230
1868
  \`\`\`
2231
1869
 
2232
- ## Live Events
1870
+ Roles are matched exactly; a suffix such as \`:*\` has no special meaning. A user with no roles gets no access. Never read roles from a request body or from user values.
1871
+
1872
+ ## Live events
1873
+
1874
+ A client can open a WebSocket to \`/api/users/me/stream\` and receive every \`user.emit()\` as \`{ "event": string, "data": unknown }\`. Events are not stored. Read the durable values again after connecting or reconnecting.
1875
+
1876
+ ## Documentation
1877
+
1878
+ Specification: https://standardagentspec.com/0.2.0/specification/users
1879
+ `;
1880
+
1881
+ // src/starter.ts
1882
+ var STARTER_SPECIFICATION_VERSION = "0.2.0";
1883
+ var LOW_COST_MODEL = `import { defineModel } from '@standardagents/builder';
1884
+ import { novita } from '@standardagents/novita';
1885
+
1886
+ export default defineModel({
1887
+ name: 'low_cost',
1888
+ provider: novita,
1889
+ model: 'deepseek/deepseek-v4.1-flash-1017',
1890
+ });
1891
+ `;
1892
+ var GUIDE_PROMPT = `import { definePrompt } from '@standardagents/builder';
1893
+
1894
+ export default definePrompt({
1895
+ name: 'guide',
1896
+ toolDescription: 'Answer questions about building agents in this project and about the Standard Agent Specification',
1897
+ model: 'low_cost',
1898
+ prompt: \`You are the guide in a new Standard Agents Builder project. You answer questions about building agents with Standard Agents Builder and about the Standard Agent Specification.
1899
+
1900
+ You have one tool, read_docs. It reads a page from the two documentation sites:
1901
+
1902
+ - Builder documentation. Its index is https://docs.standardagentbuilder.com/llms.txt
1903
+ - The Standard Agent Specification. Its index is https://standardagentspec.org/llms.txt
1904
+
1905
+ Each index lists a Markdown URL for every page.
1906
+
1907
+ How to answer:
1908
+
1909
+ 1. For a question about the builder or the specification, read the index of the site that covers it, unless you already read it in this conversation. Then read the pages that cover the question.
1910
+ 2. Take every URL from an index or from a page you have read. Do not build a URL from memory.
1911
+ 3. Answer from what the pages say. If the pages do not cover the question, say that and name the pages you read. Do not fill the gap from memory.
1912
+ 4. Answer for version ${STARTER_SPECIFICATION_VERSION} of the specification, whose pages have /${STARTER_SPECIFICATION_VERSION}/ in their URL, unless the user names another version.
1913
+ 5. Keep the answer short. Show a complete code example when it helps. End with the URLs of the pages you used.
1914
+
1915
+ You cannot read or change the files in this project. When the user wants something built, say which files to create and what goes in them, and suggest they give that to their coding agent. The AGENTS.md file in the project root explains the project to a coding agent.
1916
+
1917
+ For a greeting or a question about yourself, reply without reading anything. Say what you can answer and offer these example questions: "How do I add a tool to a prompt?", "What is the difference between a prompt and an agent?", and "How do subagents work in the specification?"\`,
1918
+ tools: ['read_docs'],
1919
+ includeChat: true,
1920
+ includePastTools: true,
1921
+ });
1922
+ `;
1923
+ var READ_DOCS_TOOL = `import { defineTool } from '@standardagents/builder';
1924
+ import { z } from 'zod';
1925
+
1926
+ // The only sites this tool reads. A URL on any other host is refused before a request is made.
1927
+ const SITES = ['docs.standardagentbuilder.com', 'standardagentspec.org'];
1928
+ // About 15,000 tokens. Every page listed in the two indexes is shorter than this.
1929
+ const MAX_CHARACTERS = 60_000;
1930
+
1931
+ export default defineTool({
1932
+ description:
1933
+ 'Read one page of the Standard Agents Builder documentation or the Standard Agent Specification. Returns the page as Markdown. Start with an index: https://docs.standardagentbuilder.com/llms.txt or https://standardagentspec.org/llms.txt.',
1934
+ args: z.object({
1935
+ url: z.string().describe('The https URL of an index or of a Markdown page that an index lists'),
1936
+ }),
1937
+ execute: async (_state, args) => {
1938
+ let url: URL;
1939
+ try {
1940
+ url = new URL(args.url);
1941
+ } catch {
1942
+ return { status: 'error', error: 'That is not a URL.', error_code: 'not_allowed' };
1943
+ }
1944
+ if (url.protocol !== 'https:' || !SITES.includes(url.hostname) || url.username || url.password || url.port) {
1945
+ return {
1946
+ status: 'error',
1947
+ error: \`This tool reads only https pages on \${SITES.join(' and ')}.\`,
1948
+ error_code: 'not_allowed',
1949
+ };
1950
+ }
1951
+
1952
+ const response = await fetch(url, { headers: { accept: 'text/markdown, text/plain' } });
1953
+ const type = response.headers.get('content-type') ?? '';
1954
+ // An unknown path can come back as an HTML page with status 200.
1955
+ if (response.status === 404 || (response.ok && !/^text\\/(markdown|plain)\\b/.test(type))) {
1956
+ return {
1957
+ status: 'error',
1958
+ error: \`No documentation page at \${url.href}. Take the URL from an index.\`,
1959
+ error_code: 'not_found',
1960
+ };
1961
+ }
1962
+ if (!response.ok) {
1963
+ return {
1964
+ status: 'error',
1965
+ error: \`The documentation site returned HTTP \${response.status}.\`,
1966
+ error_code: 'upstream_error',
1967
+ };
1968
+ }
1969
+
1970
+ const text = await response.text();
1971
+ if (text.length <= MAX_CHARACTERS) return { status: 'success', result: text };
1972
+ return {
1973
+ status: 'success',
1974
+ result: \`\${text.slice(0, MAX_CHARACTERS)}\\n\\n[Truncated: the page is \${text.length} characters long. Read a more specific page instead.]\`,
1975
+ };
1976
+ },
1977
+ });
1978
+ `;
1979
+ var GUIDE_AGENT = `import { defineAgent } from '@standardagents/builder';
2233
1980
 
2234
- Clients subscribe to \`GET /api/users/me/stream\` (WebSocket) and receive every
2235
- \`emit()\` as \`{ "event": string, "data": unknown }\`. The stream is live-only:
2236
- re-read durable state on connect and reconnect.
1981
+ export default defineAgent({
1982
+ name: 'guide_agent',
1983
+ title: 'Guide',
1984
+ description: 'Answers questions about building agents in this project and about the Standard Agent Specification.',
1985
+ sideA: { label: 'Guide', prompt: 'guide', maxSteps: 12 },
1986
+ sideB: { label: 'You', human: true },
1987
+ });
2237
1988
  `;
1989
+ var STARTER_AGENT_FILES = [
1990
+ { path: "agents/models/low_cost.ts", content: LOW_COST_MODEL, label: "model" },
1991
+ { path: "agents/tools/read_docs.ts", content: READ_DOCS_TOOL, label: "documentation tool" },
1992
+ { path: "agents/prompts/guide.ts", content: GUIDE_PROMPT, label: "prompt" },
1993
+ { path: "agents/agents/guide_agent.ts", content: GUIDE_AGENT, label: "agent" }
1994
+ ];
2238
1995
 
2239
1996
  // src/index.ts
2240
1997
  var FIRST_PARTY_PROVIDER_PACKAGES = [
@@ -2258,7 +2015,7 @@ var APPROVED_DEPENDENCY_BUILD_SCRIPTS = [
2258
2015
  "sharp",
2259
2016
  "workerd"
2260
2017
  ];
2261
- var AGENTBUILDER_TEMPLATE_VERSION = "0.45.2";
2018
+ var AGENTBUILDER_TEMPLATE_VERSION = "0.45.3";
2262
2019
  var DEFAULT_TEMPLATE_VERSIONS = {
2263
2020
  standardAgents: `^${AGENTBUILDER_TEMPLATE_VERSION}`,
2264
2021
  sip: "^1.0.1",
@@ -2314,7 +2071,7 @@ function renderPackageJson(options) {
2314
2071
  private: true,
2315
2072
  type: "module",
2316
2073
  scripts: {
2317
- dev: "vite --host 0.0.0.0 --port 5173",
2074
+ dev: "vite --host 127.0.0.1 --port 5173",
2318
2075
  build: options.includeBuildScript ? "vite build" : "echo 'Standard Agents projects build and deploy on the platform. Push your commit (git push or agents deploy) to trigger a build.' && exit 1",
2319
2076
  typecheck: "tsc --noEmit"
2320
2077
  },
@@ -2400,8 +2157,9 @@ function renderDevVarsExample() {
2400
2157
  # Local data encryption key. Generate with: openssl rand -hex 32
2401
2158
  ENCRYPTION_KEY=
2402
2159
 
2403
- # Optional local admin password for development access.
2404
- SUPER_ADMIN_PASSWORD=password
2160
+ # Local password sign-in for projects without a Standard Agents account link.
2161
+ # Leave it unset for a platform-linked project. Use a long random value.
2162
+ SUPER_ADMIN_PASSWORD=
2405
2163
 
2406
2164
  # Platform connection. The CLI writes real values to .dev.vars for platform-created projects.
2407
2165
  STANDARD_AGENTS_API_KEY=
@@ -2430,9 +2188,33 @@ function renderTemplateMetadata(options) {
2430
2188
  }, null, 2)}
2431
2189
  `;
2432
2190
  }
2191
+ var AGENT_DOCS = [
2192
+ { directory: ".", content: ROOT_AGENTS_MD, label: "project" },
2193
+ { directory: "agents/agents", content: AGENT_DEFINITIONS_AGENTS_MD, label: "agent" },
2194
+ { directory: "agents/prompts", content: PROMPTS_AGENTS_MD, label: "prompt" },
2195
+ { directory: "agents/models", content: MODELS_AGENTS_MD, label: "model" },
2196
+ { directory: "agents/providers", content: PROVIDERS_AGENTS_MD, label: "provider" },
2197
+ { directory: "agents/tools", content: TOOLS_AGENTS_MD, label: "tool" },
2198
+ { directory: "agents/hooks", content: HOOKS_AGENTS_MD, label: "hook" },
2199
+ { directory: "agents/handlers", content: HANDLERS_AGENTS_MD, label: "handler" },
2200
+ { directory: "agents/api", content: API_AGENTS_MD, label: "thread endpoint" },
2201
+ { directory: "agents/users", content: USERS_AGENTS_MD, label: "user endpoint" }
2202
+ ];
2203
+ function renderAgentDocs() {
2204
+ return AGENT_DOCS.flatMap(({ directory, content, label }) => {
2205
+ const prefix = directory === "." ? "" : `${directory}/`;
2206
+ return [
2207
+ file(`${prefix}AGENTS.md`, content, `Add ${label} docs`),
2208
+ symlink(`${prefix}CLAUDE.md`, "AGENTS.md", `Add Claude ${label} docs symlink`)
2209
+ ];
2210
+ });
2211
+ }
2212
+ function renderStarterAgent() {
2213
+ return STARTER_AGENT_FILES.map(({ path, content, label }) => file(path, content, `Add starter ${label}`));
2214
+ }
2433
2215
  function renderAgentBuilderSupportFiles(options) {
2434
2216
  return [
2435
- file("package.json", renderPackageJson(options), "Add local AgentBuilder package manifest")
2217
+ file("package.json", renderPackageJson(options), "Add local Standard Agents Builder package manifest")
2436
2218
  ];
2437
2219
  }
2438
2220
  function renderAgentBuilderProject(options) {
@@ -2454,24 +2236,8 @@ function renderAgentBuilderProject(options) {
2454
2236
  file("pnpm-workspace.yaml", renderPnpmWorkspaceYaml(), "Add pnpm settings"),
2455
2237
  file("tsconfig.json", renderTsConfig(), "Add TypeScript config"),
2456
2238
  file("vite.config.ts", renderViteConfig(), "Add Vite config"),
2457
- file("agents/agents/AGENTS.md", AGENT_DEFINITIONS_AGENTS_MD, "Add agent docs"),
2458
- symlink("agents/agents/CLAUDE.md", "AGENTS.md", "Add Claude agent docs symlink"),
2459
- file("agents/prompts/AGENTS.md", PROMPTS_AGENTS_MD, "Add prompt docs"),
2460
- symlink("agents/prompts/CLAUDE.md", "AGENTS.md", "Add Claude prompt docs symlink"),
2461
- file("agents/handlers/AGENTS.md", HANDLERS_AGENTS_MD, "Add handler docs"),
2462
- symlink("agents/handlers/CLAUDE.md", "AGENTS.md", "Add Claude handler docs symlink"),
2463
- file("agents/models/AGENTS.md", MODELS_AGENTS_MD, "Add model docs"),
2464
- symlink("agents/models/CLAUDE.md", "AGENTS.md", "Add Claude model docs symlink"),
2465
- file("agents/providers/AGENTS.md", PROVIDERS_AGENTS_MD, "Add provider docs"),
2466
- symlink("agents/providers/CLAUDE.md", "AGENTS.md", "Add Claude provider docs symlink"),
2467
- file("agents/tools/AGENTS.md", TOOLS_AGENTS_MD, "Add tool docs"),
2468
- symlink("agents/tools/CLAUDE.md", "AGENTS.md", "Add Claude tool docs symlink"),
2469
- file("agents/hooks/AGENTS.md", HOOKS_AGENTS_MD, "Add hook docs"),
2470
- symlink("agents/hooks/CLAUDE.md", "AGENTS.md", "Add Claude hook docs symlink"),
2471
- file("agents/api/AGENTS.md", API_AGENTS_MD, "Add API docs"),
2472
- symlink("agents/api/CLAUDE.md", "AGENTS.md", "Add Claude API docs symlink"),
2473
- file("agents/users/AGENTS.md", USERS_AGENTS_MD, "Add user endpoint docs"),
2474
- symlink("agents/users/CLAUDE.md", "AGENTS.md", "Add Claude user endpoint docs symlink")
2239
+ ...renderAgentDocs(),
2240
+ ...renderStarterAgent()
2475
2241
  ];
2476
2242
  if (options.includeTemplateMetadata) {
2477
2243
  files.push(file(".standardagents/template.json", renderTemplateMetadata(options), "Add Standard Agents template metadata"));
@@ -2482,6 +2248,7 @@ export {
2482
2248
  AGENTBUILDER_TEMPLATE_VERSION,
2483
2249
  AGENT_DEFINITIONS_AGENTS_MD as AGENTS_CLAUDE_MD,
2484
2250
  AGENT_DEFINITIONS_AGENTS_MD,
2251
+ AGENT_DOCS,
2485
2252
  API_AGENTS_MD,
2486
2253
  API_AGENTS_MD as API_CLAUDE_MD,
2487
2254
  APPROVED_DEPENDENCY_BUILD_SCRIPTS,
@@ -2499,6 +2266,8 @@ export {
2499
2266
  PROVIDERS_AGENTS_MD as PROVIDERS_CLAUDE_MD,
2500
2267
  ROOT_AGENTS_MD,
2501
2268
  ROOT_AGENTS_MD as ROOT_CLAUDE_MD,
2269
+ STARTER_AGENT_FILES,
2270
+ STARTER_SPECIFICATION_VERSION,
2502
2271
  TOOLS_AGENTS_MD,
2503
2272
  TOOLS_AGENTS_MD as TOOLS_CLAUDE_MD,
2504
2273
  USERS_AGENTS_MD,