@strav/brain 1.0.0-alpha.8 → 1.0.1

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.
Files changed (75) hide show
  1. package/package.json +23 -7
  2. package/src/agent.ts +97 -0
  3. package/src/agent_generate_result.ts +32 -0
  4. package/src/agent_result.ts +39 -0
  5. package/src/agent_runner.ts +265 -0
  6. package/src/agent_stream_event.ts +100 -0
  7. package/src/brain_config.ts +218 -1
  8. package/src/brain_driver.ts +247 -0
  9. package/src/brain_error.ts +86 -10
  10. package/src/brain_manager.ts +419 -5
  11. package/src/brain_provider.ts +89 -10
  12. package/src/define_tool.ts +42 -0
  13. package/src/drivers/anthropic/anthropic_brain_driver.ts +641 -0
  14. package/src/drivers/anthropic/anthropic_helpers.ts +65 -0
  15. package/src/drivers/anthropic/anthropic_message_builder.ts +258 -0
  16. package/src/drivers/anthropic/anthropic_response_mapper.ts +123 -0
  17. package/src/drivers/anthropic/anthropic_tool_loop.ts +246 -0
  18. package/src/drivers/anthropic/index.ts +1 -0
  19. package/src/drivers/deepseek/deepseek_brain_driver.ts +117 -0
  20. package/src/drivers/deepseek/index.ts +1 -0
  21. package/src/drivers/gemini/gemini_brain_driver.ts +1064 -0
  22. package/src/drivers/gemini/index.ts +1 -0
  23. package/src/drivers/minimax/index.ts +1 -0
  24. package/src/drivers/minimax/minimax_brain_driver.ts +84 -0
  25. package/src/drivers/ollama/index.ts +1 -0
  26. package/src/drivers/ollama/ollama_brain_driver.ts +86 -0
  27. package/src/drivers/openai/index.ts +1 -0
  28. package/src/drivers/openai/openai_brain_driver.ts +796 -0
  29. package/src/drivers/openai/openai_helpers.ts +58 -0
  30. package/src/drivers/openai/openai_message_builder.ts +187 -0
  31. package/src/drivers/openai/openai_response_mapper.ts +70 -0
  32. package/src/drivers/openai/openai_tool_dispatch.ts +127 -0
  33. package/src/drivers/openai/openai_tool_loop.ts +191 -0
  34. package/src/drivers/openai_compat/index.ts +1 -0
  35. package/src/drivers/openai_compat/openai_compat_brain_driver.ts +616 -0
  36. package/src/drivers/openai_responses/index.ts +1 -0
  37. package/src/drivers/openai_responses/openai_responses_brain_driver.ts +1015 -0
  38. package/src/drivers/openrouter/index.ts +1 -0
  39. package/src/drivers/openrouter/openrouter_brain_driver.ts +137 -0
  40. package/src/drivers/qwen/index.ts +1 -0
  41. package/src/drivers/qwen/qwen_brain_driver.ts +103 -0
  42. package/src/index.ts +86 -8
  43. package/src/mcp/client.ts +243 -0
  44. package/src/mcp/index.ts +23 -0
  45. package/src/mcp/oauth.ts +227 -0
  46. package/src/mcp/pool.ts +106 -0
  47. package/src/mcp/resolve_mcp_tools.ts +108 -0
  48. package/src/mcp_server.ts +63 -0
  49. package/src/output_schema.ts +72 -0
  50. package/src/persistence/brain_message.ts +34 -0
  51. package/src/persistence/brain_message_repository.ts +98 -0
  52. package/src/persistence/brain_store.ts +166 -0
  53. package/src/persistence/brain_suspended_run.ts +30 -0
  54. package/src/persistence/brain_suspended_run_repository.ts +59 -0
  55. package/src/persistence/brain_thread.ts +30 -0
  56. package/src/persistence/brain_thread_repository.ts +56 -0
  57. package/src/persistence/database_brain_store.ts +190 -0
  58. package/src/persistence/index.ts +48 -0
  59. package/src/persistence/schemas/brain_message_schema.ts +61 -0
  60. package/src/persistence/schemas/brain_suspended_run_schema.ts +58 -0
  61. package/src/persistence/schemas/brain_thread_schema.ts +50 -0
  62. package/src/persistence/schemas/index.ts +3 -0
  63. package/src/suspended_run.ts +153 -0
  64. package/src/thread.ts +40 -1
  65. package/src/tool.ts +42 -0
  66. package/src/tool_execution_error.ts +26 -0
  67. package/src/tool_runner.ts +81 -0
  68. package/src/translate/index.ts +19 -0
  69. package/src/translate/translate_cache.ts +78 -0
  70. package/src/translate/translate_provider.ts +46 -0
  71. package/src/translate/translator.ts +271 -0
  72. package/src/types.ts +431 -1
  73. package/src/zod/index.ts +121 -0
  74. package/src/provider.ts +0 -48
  75. package/src/providers/anthropic_provider.ts +0 -227
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@strav/brain",
3
- "version": "1.0.0-alpha.8",
4
- "description": "Strav AI module — unified Provider interface, BrainManager, threads, prompt caching. Anthropic provider in V1; OpenAI / Gemini / DeepSeek follow.",
3
+ "version": "1.0.1",
4
+ "description": "Strav AI module — unified Provider interface, BrainManager, threads, prompt caching, tools / agents / MCP. Anthropic + OpenAI providers; Gemini / DeepSeek follow.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
7
7
  "types": "./src/index.ts",
8
8
  "exports": {
9
- ".": "./src/index.ts"
9
+ ".": "./src/index.ts",
10
+ "./mcp": "./src/mcp/index.ts",
11
+ "./persistence": "./src/persistence/index.ts",
12
+ "./translate": "./src/translate/index.ts",
13
+ "./zod": "./src/zod/index.ts"
10
14
  },
11
15
  "files": [
12
16
  "src",
@@ -19,11 +23,23 @@
19
23
  "access": "public"
20
24
  },
21
25
  "dependencies": {
22
- "@strav/kernel": "1.0.0-alpha.8",
23
- "@anthropic-ai/sdk": "^0.100.0"
26
+ "@anthropic-ai/sdk": "^0.100.0",
27
+ "@google/genai": "^2.7.0",
28
+ "@modelcontextprotocol/sdk": "^1.29.0",
29
+ "@strav/database": "1.0.1",
30
+ "@strav/kernel": "1.0.1",
31
+ "openai": "^6.0.0"
24
32
  },
25
33
  "peerDependencies": {
26
- "@types/bun": ">=1.3.14"
34
+ "@types/bun": ">=1.3.14",
35
+ "zod": "^4.0.0"
27
36
  },
28
- "devDependencies": null
37
+ "peerDependenciesMeta": {
38
+ "zod": {
39
+ "optional": true
40
+ }
41
+ },
42
+ "devDependencies": {
43
+ "zod": "^4.4.3"
44
+ }
29
45
  }
package/src/agent.ts ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * `Agent` — declarative base class for AI agents.
3
+ *
4
+ * Apps subclass and set the static-ish properties: which model to
5
+ * use, what the agent's persona is, which tools it has access to,
6
+ * and an optional iteration ceiling. The `BrainManager.agent(Class)`
7
+ * call resolves an instance via the container, builds an
8
+ * `AgentRunner`, and lets the app stream input + context into it.
9
+ *
10
+ * ```ts
11
+ * @inject()
12
+ * class ResearchAgent extends Agent {
13
+ * override readonly instructions = 'You are a meticulous research assistant.'
14
+ * override readonly tools = [searchTool, summarizeTool]
15
+ * override readonly tier: ModelTier = 'powerful'
16
+ * }
17
+ *
18
+ * const result = await brain.agent(ResearchAgent)
19
+ * .input('What is the current state of bun.sql?')
20
+ * .context({ userId: '01ABC...' })
21
+ * .run()
22
+ * ```
23
+ *
24
+ * Structured output (typed result without `.output(schema)` at the call site):
25
+ *
26
+ * ```ts
27
+ * class CityAgent extends Agent<CityAnswer> {
28
+ * override readonly instructions = 'You only emit verified city data.'
29
+ * override readonly outputSchema = citySchema // OutputSchema<CityAnswer>
30
+ * }
31
+ *
32
+ * const { value } = await brain.agent(CityAgent).input('Capital of France?').run()
33
+ * // ^? CityAnswer — runner is typed from the class generic
34
+ * ```
35
+ *
36
+ * The generic threads `T` through `BrainManager.agent(Class)` →
37
+ * `AgentRunner<T>` → `AgentGenerateResult<T>`. Subclasses that
38
+ * don't declare an output type stay `Agent<never>` and `run()`
39
+ * returns `AgentResult` exactly as before.
40
+ */
41
+
42
+ import type { MCPServer } from './mcp_server.ts'
43
+ import type { OutputSchema } from './output_schema.ts'
44
+ import type { ModelTier } from './types.ts'
45
+ import type { Tool } from './tool.ts'
46
+
47
+ export abstract class Agent<T = never> {
48
+ /** System prompt — the persona / instructions Claude sees on every turn. */
49
+ abstract readonly instructions: string
50
+
51
+ /** Tools the agent can call. Empty array → the model answers without tools. */
52
+ readonly tools: readonly Tool[] = []
53
+
54
+ /**
55
+ * MCP servers exposed to the agent. Anthropic's backend connects
56
+ * to them and surfaces their tools to the model alongside any
57
+ * locally-registered `tools`. Empty array (or omitted) → no MCP
58
+ * servers; the agent runs with just `tools` (or no tools at all).
59
+ */
60
+ readonly mcpServers: readonly MCPServer[] = []
61
+
62
+ /** Override the configured default provider. Default = brain's default provider. */
63
+ readonly provider?: string
64
+
65
+ /** Explicit model ID. Wins over `tier`. */
66
+ readonly model?: string
67
+
68
+ /** Tier sugar. Default `'powerful'` for agentic work. */
69
+ readonly tier: ModelTier = 'powerful'
70
+
71
+ /**
72
+ * Safety ceiling on the agentic loop. Default `10`. Hitting it
73
+ * returns a result with `stopReason: 'max_iterations'`; the loop
74
+ * doesn't throw because partial progress (assistant messages, tool
75
+ * results) is usually still useful to surface.
76
+ */
77
+ readonly maxIterations: number = 10
78
+
79
+ /** Hard cap on per-call response tokens. Default `4096`. */
80
+ readonly maxTokens: number = 4096
81
+
82
+ /**
83
+ * Structured-output schema. Set on subclasses that extend
84
+ * `Agent<SomeType>` to declare the agent always returns that
85
+ * shape; `BrainManager.agent(Class)` then types the runner
86
+ * automatically so `.run()` returns `AgentGenerateResult<T>`
87
+ * without a per-call `.output(schema)`.
88
+ *
89
+ * Leave unset on `Agent<never>` subclasses (no structured
90
+ * output). The runner falls back to the standard tool-loop path
91
+ * and returns `AgentResult`.
92
+ *
93
+ * Apps that want a per-call override still chain
94
+ * `.output(otherSchema)` — that wins over the class-side value.
95
+ */
96
+ readonly outputSchema?: OutputSchema<T>
97
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `AgentGenerateResult<T>` — what an Agent run returns when the
3
+ * runner was switched into structured-output mode via
4
+ * `.output(schema)`.
5
+ *
6
+ * Combines the structured-output payload (`value` + raw `text`) with
7
+ * the agent-loop bookkeeping (`messages`, `iterations`, `stopReason`,
8
+ * `usage`) so apps can still render the trace + report token spend
9
+ * the same way they do for `AgentResult`. `iterations` is always `0`
10
+ * in V1 because the structured-output path doesn't engage the
11
+ * tool-use loop — see the docs for the (deferred) "tools + schema"
12
+ * combined slice.
13
+ */
14
+
15
+ import type { ChatUsage, Message } from './types.ts'
16
+
17
+ export interface AgentGenerateResult<T = unknown> {
18
+ /** Parsed structured value matching the supplied `OutputSchema<T>`. */
19
+ value: T
20
+ /** Raw JSON text the model produced — handy for logging when `parse` rejects. */
21
+ text: string
22
+ /** Full message history of the run (single user → assistant turn in V1). */
23
+ messages: Message[]
24
+ /** Always `0` in V1 — the schema path doesn't engage the tool-use loop. */
25
+ iterations: number
26
+ /** Provider-reported terminal stop reason. */
27
+ stopReason: string
28
+ /** Token usage from the single underlying `generate` call. */
29
+ usage: ChatUsage
30
+ /** See `ChatResult.responseId`. */
31
+ responseId?: string
32
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `AgentResult` — what an agentic loop returns when it ends. Combines
3
+ * the final assistant `text`, the full message history (including
4
+ * tool calls + results so apps can render the trace), the total
5
+ * iteration count (how many tool-use round-trips the loop made),
6
+ * and aggregated token usage across every model call inside the
7
+ * loop.
8
+ *
9
+ * `stopReason` is the provider's terminal stop reason (typically
10
+ * `'end_turn'`). When the loop exits because it hit `maxIterations`,
11
+ * `stopReason` is `'max_iterations'` — distinct from the provider
12
+ * value so apps can detect "the model would have kept going."
13
+ */
14
+
15
+ import type { ChatUsage, Message } from './types.ts'
16
+
17
+ export interface AgentResult {
18
+ /** Concatenated text from the final assistant turn. */
19
+ text: string
20
+ /** Full message history of the loop, including tool_use / tool_result blocks. */
21
+ messages: Message[]
22
+ /** Number of tool-use rounds. `0` when the model answered without tools. */
23
+ iterations: number
24
+ /**
25
+ * Terminal stop reason. Either the provider's stop_reason (typically
26
+ * `'end_turn'`) or the framework-specific `'max_iterations'` when
27
+ * the loop hit its iteration ceiling.
28
+ */
29
+ stopReason: string
30
+ /** Token usage summed across every model call in the loop. */
31
+ usage: ChatUsage
32
+ /**
33
+ * Final provider response id when the provider exposes stateful
34
+ * conversations (OpenAI Responses API). Captured from the last
35
+ * model turn so apps that persist the conversation can resume
36
+ * via `ChatOptions.previousResponseId`. Undefined elsewhere.
37
+ */
38
+ responseId?: string
39
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * `AgentRunner` — fluent builder returned by `BrainManager.agent(Class)`.
3
+ *
4
+ * Carries the agent instance + an input message + an optional
5
+ * per-run context bag + an optional structured-output schema.
6
+ * `run()` translates the agent's declarative configuration into
7
+ * either a `runWithTools` call (default) or a `generate` call (when
8
+ * `.output(schema)` was used) and returns the matching result type.
9
+ *
10
+ * Designed to chain:
11
+ *
12
+ * ```ts
13
+ * brain.agent(R).input(text).context({...}).run()
14
+ * brain.agent(R).input(text).output(schema).run() // → AgentGenerateResult<T>
15
+ * ```
16
+ *
17
+ * Apps that need the full Message-array surface bypass the runner
18
+ * and call `BrainManager.runTools(messages, tools, options)` or
19
+ * `BrainManager.generate(input, schema, options)` directly.
20
+ */
21
+
22
+ import type { Agent } from './agent.ts'
23
+ import type { AgentGenerateResult } from './agent_generate_result.ts'
24
+ import type { AgentResult } from './agent_result.ts'
25
+ import type { AgentStreamEvent } from './agent_stream_event.ts'
26
+ import type { BrainManager } from './brain_manager.ts'
27
+ import { BrainError } from './brain_error.ts'
28
+ import type { OutputSchema } from './output_schema.ts'
29
+ import type {
30
+ ChatOptions,
31
+ Message,
32
+ ToolUseBlock,
33
+ } from './types.ts'
34
+ import type { RunWithToolsOptions } from './brain_driver.ts'
35
+ import type {
36
+ SuspendedRun,
37
+ SuspendedState,
38
+ ToolResultInput,
39
+ } from './suspended_run.ts'
40
+
41
+ /**
42
+ * Conditional return shape for `AgentRunner.run()`. With the default
43
+ * generic (`T = never`), `run()` returns `AgentResult` — the
44
+ * tool-loop shape. When the runner has been switched into
45
+ * structured-output mode via `.output(schema)`, `T` carries the
46
+ * inferred type and `run()` returns `AgentGenerateResult<T>`.
47
+ *
48
+ * The `[T] extends [never]` form is the standard "is this still the
49
+ * default never?" check — `T extends never` would distribute over
50
+ * union types and break.
51
+ */
52
+ export type AgentRunResult<T> = [T] extends [never] ? AgentResult : AgentGenerateResult<T>
53
+
54
+ /**
55
+ * Conditional return shape that flips when the runner has opted in
56
+ * to suspension via `.suspend(gate)`. The phantom `S` generic on
57
+ * `AgentRunner<T, S>` carries the bit; `S extends true` widens the
58
+ * union so callers must narrow with `isSuspended(...)` before
59
+ * touching `result.value` / `result.text`.
60
+ */
61
+ export type AgentRunMaybeSuspended<T, S extends boolean> = [S] extends [true]
62
+ ? AgentRunResult<T> | SuspendedRun
63
+ : AgentRunResult<T>
64
+
65
+ export class AgentRunner<T = never, S extends boolean = false> {
66
+ private prompt: string | undefined
67
+ private contextBag: Record<string, unknown> = {}
68
+ private schema: OutputSchema<T> | undefined
69
+ private suspendGate:
70
+ | ((call: ToolUseBlock, context?: Record<string, unknown>) => boolean | Promise<boolean>)
71
+ | undefined
72
+
73
+ constructor(
74
+ private readonly brain: BrainManager,
75
+ private readonly agent: Agent<unknown>,
76
+ ) {}
77
+
78
+ /**
79
+ * Install a human-in-the-loop gate. Called before each tool
80
+ * execution inside the agent loop; when it returns `true`, the
81
+ * run pauses and `.run()` resolves with a `SuspendedRun` instead
82
+ * of `AgentResult`. Apps obtain results out-of-band and call
83
+ * `.resume(state, results)` to continue.
84
+ *
85
+ * Throws `BrainError` if the runner is also in structured-output
86
+ * mode (`.output(schema)`) — schema + suspend is a deferred slice.
87
+ */
88
+ suspend(
89
+ gate: (call: ToolUseBlock, context?: Record<string, unknown>) => boolean | Promise<boolean>,
90
+ ): AgentRunner<T, true> {
91
+ this.suspendGate = gate
92
+ return this as unknown as AgentRunner<T, true>
93
+ }
94
+
95
+ /** Set the user input. Required before `run()`. */
96
+ input(text: string): this {
97
+ this.prompt = text
98
+ return this
99
+ }
100
+
101
+ /**
102
+ * Attach context that every tool's `execute(input, ctx)` will see
103
+ * on `ctx.context`. Useful for per-request data the agent's tools
104
+ * need but the model shouldn't see directly (auth identity,
105
+ * tenant id, request-id for tracing).
106
+ */
107
+ context(data: Record<string, unknown>): this {
108
+ this.contextBag = { ...this.contextBag, ...data }
109
+ return this
110
+ }
111
+
112
+ /**
113
+ * Switch the runner into structured-output mode. `run()` then
114
+ * delegates to `BrainManager.generate(...)` and returns an
115
+ * `AgentGenerateResult<U>` shaped to the supplied schema.
116
+ *
117
+ * V1 caveat: structured output and tool use can't be combined yet.
118
+ * Agents that declare `tools` or `mcpServers` AND call `.output()`
119
+ * throw a `BrainError` at `run()` with a clear "this combination is
120
+ * deferred" message. Apps that need both today run them in two
121
+ * steps — `runTools(...)` for the loop, then `generate(...)` for
122
+ * the structured summary.
123
+ */
124
+ output<U>(schema: OutputSchema<U>): AgentRunner<U> {
125
+ // Mutate in place + cast — the runtime state is a single object;
126
+ // the generic narrows only the static return type. This avoids
127
+ // cloning the prompt + contextBag fields.
128
+ this.schema = schema as unknown as OutputSchema<T>
129
+ return this as unknown as AgentRunner<U>
130
+ }
131
+
132
+ /**
133
+ * Streaming variant of `run()`. Returns an
134
+ * `AsyncIterable<AgentStreamEvent<T>>` — yields text deltas,
135
+ * tool-use/result boundaries, and a terminal `stop` event with
136
+ * the full trace.
137
+ *
138
+ * Default (no `.output(schema)` set): the terminal `stop` has the
139
+ * plain shape and `T` defaults to `never`.
140
+ *
141
+ * With `.output(schema)`: the terminal `stop` event carries the
142
+ * parsed `value: T` + raw `text` alongside the loop bookkeeping,
143
+ * and the runner delegates to
144
+ * `BrainManager.streamGenerateWithTools`.
145
+ */
146
+ stream(): AsyncIterable<AgentStreamEvent<T>> {
147
+ if (this.prompt === undefined) {
148
+ throw new BrainError('AgentRunner.stream: input() must be called before stream().')
149
+ }
150
+ const messages: Message[] = [{ role: 'user', content: this.prompt }]
151
+ const options: RunWithToolsOptions = {
152
+ ...this.buildChatOptions(),
153
+ maxIterations: this.agent.maxIterations,
154
+ context: this.contextBag,
155
+ }
156
+ if (this.agent.mcpServers.length > 0) options.mcpServers = this.agent.mcpServers
157
+ if (this.schema !== undefined) {
158
+ return this.brain.streamGenerateWithTools<T>(
159
+ messages,
160
+ this.schema,
161
+ this.agent.tools,
162
+ options,
163
+ )
164
+ }
165
+ return this.brain.streamTools(messages, this.agent.tools, options) as AsyncIterable<
166
+ AgentStreamEvent<T>
167
+ >
168
+ }
169
+
170
+ async run(): Promise<AgentRunMaybeSuspended<T, S>> {
171
+ if (this.prompt === undefined) {
172
+ throw new BrainError('AgentRunner.run: input() must be called before run().')
173
+ }
174
+ if (this.suspendGate !== undefined && this.schema !== undefined) {
175
+ throw new BrainError(
176
+ 'AgentRunner.run: `.suspend(...)` and `.output(schema)` cannot be combined in V1 — the schema variants don\'t yet model pause/resume. Run tools first with suspension, then call brain.generate(...) on the result for the structured summary.',
177
+ )
178
+ }
179
+ const messages: Message[] = [{ role: 'user', content: this.prompt }]
180
+
181
+ if (this.schema !== undefined) {
182
+ const hasTools = this.agent.tools.length > 0 || this.agent.mcpServers.length > 0
183
+ if (hasTools) {
184
+ const toolOptions: RunWithToolsOptions = {
185
+ ...this.buildChatOptions(),
186
+ maxIterations: this.agent.maxIterations,
187
+ context: this.contextBag,
188
+ }
189
+ if (this.agent.mcpServers.length > 0) toolOptions.mcpServers = this.agent.mcpServers
190
+ const result = await this.brain.generateWithTools<T>(
191
+ messages,
192
+ this.schema,
193
+ this.agent.tools,
194
+ toolOptions,
195
+ )
196
+ return result as AgentRunResult<T>
197
+ }
198
+ const generateOptions = this.buildChatOptions()
199
+ const result = await this.brain.generate<T>(messages, this.schema, generateOptions)
200
+ const generateResult: AgentGenerateResult<T> = {
201
+ value: result.value,
202
+ text: result.text,
203
+ messages: [
204
+ ...messages,
205
+ { role: 'assistant', content: result.text },
206
+ ],
207
+ iterations: 0,
208
+ stopReason: result.stopReason ?? 'stop',
209
+ usage: result.usage,
210
+ }
211
+ return generateResult as AgentRunResult<T>
212
+ }
213
+
214
+ const options: RunWithToolsOptions = {
215
+ ...this.buildChatOptions(),
216
+ maxIterations: this.agent.maxIterations,
217
+ context: this.contextBag,
218
+ }
219
+ if (this.agent.mcpServers.length > 0) options.mcpServers = this.agent.mcpServers
220
+ if (this.suspendGate !== undefined) options.shouldSuspend = this.suspendGate
221
+ const result = await this.brain.runTools(messages, this.agent.tools, options)
222
+ return result as AgentRunMaybeSuspended<T, S>
223
+ }
224
+
225
+ /**
226
+ * Resume a previously-suspended run. Takes the `SuspendedRun.state`
227
+ * snapshot and the results gathered for each `pendingToolCalls`
228
+ * entry; the loop continues from where it paused.
229
+ *
230
+ * The runner's `suspend()` gate carries over so the same
231
+ * approval logic applies to any further tool calls — pass a
232
+ * fresh gate via `suspend()` before `resume()` to change the
233
+ * policy.
234
+ */
235
+ async resume(
236
+ state: SuspendedState,
237
+ results: readonly ToolResultInput[],
238
+ ): Promise<AgentRunMaybeSuspended<T, true>> {
239
+ if (this.schema !== undefined) {
240
+ throw new BrainError(
241
+ 'AgentRunner.resume: structured-output runners cannot be resumed in V1 — `.output(schema)` is incompatible with pause/resume.',
242
+ )
243
+ }
244
+ const options: RunWithToolsOptions = {
245
+ ...this.buildChatOptions(),
246
+ maxIterations: this.agent.maxIterations,
247
+ context: this.contextBag,
248
+ }
249
+ if (this.agent.mcpServers.length > 0) options.mcpServers = this.agent.mcpServers
250
+ if (this.suspendGate !== undefined) options.shouldSuspend = this.suspendGate
251
+ const result = await this.brain.resumeTools(state, results, this.agent.tools, options)
252
+ return result as AgentRunMaybeSuspended<T, true>
253
+ }
254
+
255
+ private buildChatOptions(): ChatOptions {
256
+ const options: ChatOptions = {
257
+ tier: this.agent.tier,
258
+ maxTokens: this.agent.maxTokens,
259
+ system: this.agent.instructions,
260
+ }
261
+ if (this.agent.model !== undefined) options.model = this.agent.model
262
+ if (this.agent.provider !== undefined) options.provider = this.agent.provider
263
+ return options
264
+ }
265
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * `AgentStreamEvent<T>` — the union yielded by streaming agentic
3
+ * runs (`Provider.streamWithTools` / `BrainManager.streamTools` /
4
+ * `AgentRunner.stream`).
5
+ *
6
+ * The generic `T` carries the structured-output type when the run
7
+ * was schema-constrained:
8
+ * - With the default `T = never`, the terminal `stop` event is the
9
+ * plain shape: `{ stopReason, iterations, usage, messages }`.
10
+ * - With `T` set (via `BrainManager.streamGenerateWithTools` or
11
+ * `AgentRunner.stream()` after `.output(schema)`), the `stop`
12
+ * event additionally carries `{ value: T; text: string }` — the
13
+ * parsed JSON shaped to the schema and the raw text the model
14
+ * produced.
15
+ *
16
+ * Event vocabulary:
17
+ *
18
+ * - `iteration_start` — fired before each model call. `iteration`
19
+ * starts at `0` and increments per round.
20
+ * - `text` — a text delta from the assistant turn currently in
21
+ * flight. Same shape as `StreamEvent.text` so apps can reuse
22
+ * UI code.
23
+ * - `tool_use_start` — a tool call has begun streaming. Fires
24
+ * as soon as the model emits the call's `id` + `name`,
25
+ * before the arguments finish streaming. Apps that render
26
+ * "(calling X with …)" indicators show the tool name here.
27
+ * Anthropic + OpenAI emit this from streaming chunks; Gemini
28
+ * doesn't stream tool arguments (parts arrive complete) and
29
+ * skips both `tool_use_start` and `tool_use_delta`.
30
+ * - `tool_use_delta` — a chunk of the tool-call's argument JSON.
31
+ * Apps that render the model composing the call (e.g. typing
32
+ * `search(q='current state of bun.sql ...`) accumulate
33
+ * `argsDelta` per `id` and re-render. The argument JSON is
34
+ * partial / possibly malformed mid-stream — only the final
35
+ * `tool_use` event carries the parsed input.
36
+ * - `tool_use` — the assistant turn finished and the framework
37
+ * parsed a tool call. Emitted with the parsed `input` before
38
+ * the framework runs the tool. Source-of-truth for tool calls;
39
+ * cross-provider consumers can rely on this even when the
40
+ * start/delta events aren't fired.
41
+ * - `tool_result` — the framework executed the tool and is about
42
+ * to feed the result back to the model. `isError` reflects
43
+ * whether the tool reported a failure (V1: only false today —
44
+ * thrown executions abort the loop with `ToolExecutionError`;
45
+ * graceful tool-error recovery is a later slice).
46
+ * - `iteration_end` — the assistant turn fully drained, including
47
+ * its `stopReason`. Apps that render per-iteration boundaries
48
+ * use this.
49
+ * - `stop` — terminal event. Carries the full `messages` trace,
50
+ * iteration count, aggregated usage, and the framework
51
+ * stop reason (`'end_turn'`, `'max_iterations'`, etc.). When the
52
+ * run was schema-constrained, also carries `value` (parsed JSON)
53
+ * and `text` (the raw JSON the model emitted).
54
+ *
55
+ * What's NOT in V1:
56
+ * - `error` events. Failures throw out of the iterator (the
57
+ * consumer's `for await` rejects). Apps that want resilient
58
+ * loops catch around the consumer.
59
+ */
60
+
61
+ import type { ChatUsage, Message } from './types.ts'
62
+
63
+ interface BaseStopEvent {
64
+ type: 'stop'
65
+ stopReason: string
66
+ iterations: number
67
+ usage: ChatUsage
68
+ messages: Message[]
69
+ }
70
+
71
+ interface ValueStopEvent<T> extends BaseStopEvent {
72
+ /** Parsed JSON shaped to the supplied `OutputSchema<T>`. */
73
+ value: T
74
+ /** Raw JSON text the model emitted on its terminal turn. */
75
+ text: string
76
+ }
77
+
78
+ /**
79
+ * The `stop` variant narrows when the stream was schema-constrained
80
+ * — the `[T] extends [never]` form is the standard "is this still
81
+ * the default never?" check (a bare `T extends never` would
82
+ * distribute over union types and break).
83
+ */
84
+ type StopEvent<T> = [T] extends [never] ? BaseStopEvent : ValueStopEvent<T>
85
+
86
+ export type AgentStreamEvent<T = never> =
87
+ | { type: 'iteration_start'; iteration: number }
88
+ | { type: 'text'; delta: string }
89
+ | { type: 'tool_use_start'; id: string; name: string }
90
+ | { type: 'tool_use_delta'; id: string; argsDelta: string }
91
+ | { type: 'tool_use'; id: string; name: string; input: unknown }
92
+ | {
93
+ type: 'tool_result'
94
+ id: string
95
+ name: string
96
+ content: string
97
+ isError: boolean
98
+ }
99
+ | { type: 'iteration_end'; iteration: number; stopReason: string | null }
100
+ | StopEvent<T>