@strav/brain 1.0.0-alpha.9 → 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 (73) hide show
  1. package/package.json +23 -7
  2. package/src/agent.ts +43 -5
  3. package/src/agent_generate_result.ts +32 -0
  4. package/src/agent_result.ts +7 -0
  5. package/src/agent_runner.ts +218 -14
  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 +359 -11
  11. package/src/brain_provider.ts +79 -9
  12. package/src/drivers/anthropic/anthropic_brain_driver.ts +641 -0
  13. package/src/drivers/anthropic/anthropic_helpers.ts +65 -0
  14. package/src/drivers/anthropic/anthropic_message_builder.ts +258 -0
  15. package/src/drivers/anthropic/anthropic_response_mapper.ts +123 -0
  16. package/src/drivers/anthropic/anthropic_tool_loop.ts +246 -0
  17. package/src/drivers/anthropic/index.ts +1 -0
  18. package/src/drivers/deepseek/deepseek_brain_driver.ts +117 -0
  19. package/src/drivers/deepseek/index.ts +1 -0
  20. package/src/drivers/gemini/gemini_brain_driver.ts +1064 -0
  21. package/src/drivers/gemini/index.ts +1 -0
  22. package/src/drivers/minimax/index.ts +1 -0
  23. package/src/drivers/minimax/minimax_brain_driver.ts +84 -0
  24. package/src/drivers/ollama/index.ts +1 -0
  25. package/src/drivers/ollama/ollama_brain_driver.ts +86 -0
  26. package/src/drivers/openai/index.ts +1 -0
  27. package/src/drivers/openai/openai_brain_driver.ts +796 -0
  28. package/src/drivers/openai/openai_helpers.ts +58 -0
  29. package/src/drivers/openai/openai_message_builder.ts +187 -0
  30. package/src/drivers/openai/openai_response_mapper.ts +70 -0
  31. package/src/drivers/openai/openai_tool_dispatch.ts +127 -0
  32. package/src/drivers/openai/openai_tool_loop.ts +191 -0
  33. package/src/drivers/openai_compat/index.ts +1 -0
  34. package/src/drivers/openai_compat/openai_compat_brain_driver.ts +616 -0
  35. package/src/drivers/openai_responses/index.ts +1 -0
  36. package/src/drivers/openai_responses/openai_responses_brain_driver.ts +1015 -0
  37. package/src/drivers/openrouter/index.ts +1 -0
  38. package/src/drivers/openrouter/openrouter_brain_driver.ts +137 -0
  39. package/src/drivers/qwen/index.ts +1 -0
  40. package/src/drivers/qwen/qwen_brain_driver.ts +103 -0
  41. package/src/index.ts +75 -11
  42. package/src/mcp/client.ts +243 -0
  43. package/src/mcp/index.ts +23 -0
  44. package/src/mcp/oauth.ts +227 -0
  45. package/src/mcp/pool.ts +106 -0
  46. package/src/mcp/resolve_mcp_tools.ts +108 -0
  47. package/src/mcp_server.ts +63 -0
  48. package/src/output_schema.ts +72 -0
  49. package/src/persistence/brain_message.ts +34 -0
  50. package/src/persistence/brain_message_repository.ts +98 -0
  51. package/src/persistence/brain_store.ts +166 -0
  52. package/src/persistence/brain_suspended_run.ts +30 -0
  53. package/src/persistence/brain_suspended_run_repository.ts +59 -0
  54. package/src/persistence/brain_thread.ts +30 -0
  55. package/src/persistence/brain_thread_repository.ts +56 -0
  56. package/src/persistence/database_brain_store.ts +190 -0
  57. package/src/persistence/index.ts +48 -0
  58. package/src/persistence/schemas/brain_message_schema.ts +61 -0
  59. package/src/persistence/schemas/brain_suspended_run_schema.ts +58 -0
  60. package/src/persistence/schemas/brain_thread_schema.ts +50 -0
  61. package/src/persistence/schemas/index.ts +3 -0
  62. package/src/suspended_run.ts +153 -0
  63. package/src/thread.ts +40 -1
  64. package/src/tool.ts +7 -0
  65. package/src/tool_runner.ts +81 -0
  66. package/src/translate/index.ts +19 -0
  67. package/src/translate/translate_cache.ts +78 -0
  68. package/src/translate/translate_provider.ts +46 -0
  69. package/src/translate/translator.ts +271 -0
  70. package/src/types.ts +398 -1
  71. package/src/zod/index.ts +121 -0
  72. package/src/provider.ts +0 -74
  73. package/src/providers/anthropic_provider.ts +0 -397
@@ -16,6 +16,7 @@
16
16
  * that want every long request to cache flip this to `true`.
17
17
  */
18
18
 
19
+ import type { MCPServer } from './mcp_server.ts'
19
20
  import type { ModelTier } from './types.ts'
20
21
 
21
22
  /** Anthropic-specific driver config. */
@@ -33,7 +34,216 @@ export interface AnthropicProviderConfig {
33
34
  betas?: readonly string[]
34
35
  }
35
36
 
36
- export type ProviderConfig = AnthropicProviderConfig // | OpenAIProviderConfig | … (later slices)
37
+ /**
38
+ * OpenAI Responses API driver config — backed by the `openai`
39
+ * SDK's `client.responses.create` endpoint. Use when you need
40
+ * OpenAI's server-side tools (web search, code interpreter) or
41
+ * the Responses API's reasoning surfaces. The chat completions
42
+ * provider (`driver: 'openai'`) covers everything else.
43
+ */
44
+ export interface OpenAIResponsesProviderConfig {
45
+ driver: 'openai-responses'
46
+ /** API key. Required. Most apps source from `env('OPENAI_API_KEY')`. */
47
+ apiKey: string
48
+ /** Optional override of the SDK's base URL. */
49
+ baseUrl?: string
50
+ /** Optional organization id. */
51
+ organization?: string
52
+ /** Default model. Defaults to `gpt-5`. */
53
+ defaultModel?: string
54
+ /** Default `max_output_tokens`. Defaults to 4096. */
55
+ defaultMaxTokens?: number
56
+ /** Default embedding model (inherited from chat completions endpoint). Defaults to `text-embedding-3-small`. */
57
+ defaultEmbedModel?: string
58
+ /** Default audio-transcription model. Defaults to `whisper-1`. */
59
+ defaultTranscribeModel?: string
60
+ }
61
+
62
+ /** OpenAI-specific driver config. */
63
+ export interface OpenAIProviderConfig {
64
+ driver: 'openai'
65
+ /** API key. Required. Most apps source from `env('OPENAI_API_KEY')`. */
66
+ apiKey: string
67
+ /** Optional override of the SDK's base URL — useful for proxies, Azure OpenAI, or test doubles. */
68
+ baseUrl?: string
69
+ /** Optional organization id. */
70
+ organization?: string
71
+ /** Default model when neither `options.model` nor `options.tier` is passed. Defaults to `gpt-5`. */
72
+ defaultModel?: string
73
+ /** Default `max_tokens` for `chat()` calls that don't specify one. */
74
+ defaultMaxTokens?: number
75
+ /** Default embedding model for `brain.embed(...)`. Defaults to `text-embedding-3-small`. */
76
+ defaultEmbedModel?: string
77
+ /** Default audio-transcription model for `brain.transcribe(...)`. Defaults to `whisper-1`. */
78
+ defaultTranscribeModel?: string
79
+ }
80
+
81
+ /** Google (Gemini) driver config — backed by `@google/genai`. */
82
+ export interface GeminiProviderConfig {
83
+ driver: 'google'
84
+ /** API key. Required. Most apps source from `env('GOOGLE_API_KEY')` or `env('GEMINI_API_KEY')`. */
85
+ apiKey: string
86
+ /** Optional override of the SDK's base URL — useful for proxies or test doubles. */
87
+ baseUrl?: string
88
+ /** Default model when neither `options.model` nor `options.tier` is passed. Defaults to `gemini-2.5-flash`. */
89
+ defaultModel?: string
90
+ /** Default `max_tokens` for `chat()` calls that don't specify one. */
91
+ defaultMaxTokens?: number
92
+ /** Optional API version pin (`v1` / `v1beta`). */
93
+ apiVersion?: string
94
+ /** Default embedding model for `brain.embed(...)`. Defaults to `text-embedding-004`. */
95
+ defaultEmbedModel?: string
96
+ }
97
+
98
+ /** DeepSeek driver config — backed by the `openai` SDK pointed at DeepSeek's OpenAI-compatible endpoint. */
99
+ export interface DeepSeekProviderConfig {
100
+ driver: 'deepseek'
101
+ /** API key. Required. Most apps source from `env('DEEPSEEK_API_KEY')`. */
102
+ apiKey: string
103
+ /** Optional override of the SDK's base URL. Defaults to `https://api.deepseek.com/v1`. */
104
+ baseUrl?: string
105
+ /** Default model when neither `options.model` nor `options.tier` is passed. Defaults to `deepseek-chat`. */
106
+ defaultModel?: string
107
+ /** Default `max_tokens` for `chat()` calls that don't specify one. */
108
+ defaultMaxTokens?: number
109
+ }
110
+
111
+ /**
112
+ * Ollama driver config — backed by the `openai` SDK pointed at a
113
+ * local Ollama server's OpenAI-compatible `/v1` endpoint. The same
114
+ * shape works against any OpenAI-compatible local server (LM Studio,
115
+ * llama.cpp's server, vLLM, …) by overriding `baseUrl`.
116
+ */
117
+ export interface OllamaProviderConfig {
118
+ driver: 'ollama'
119
+ /**
120
+ * Required — model must be already pulled on the Ollama server
121
+ * (`ollama pull <model>`). No universal default exists because
122
+ * apps install whichever models they need. Common picks for
123
+ * tool-calling: `llama3.2`, `llama3.1`, `qwen2.5`, `mistral`.
124
+ */
125
+ defaultModel: string
126
+ /** Optional override of the SDK's base URL. Defaults to `http://localhost:11434/v1`. */
127
+ baseUrl?: string
128
+ /**
129
+ * Optional API key. Ollama doesn't require one — the SDK demands
130
+ * a non-empty string, so a placeholder is fine and the default
131
+ * (`'ollama'`) works. Override only when running behind a proxy
132
+ * that adds its own auth layer.
133
+ */
134
+ apiKey?: string
135
+ /** Default `max_tokens` for `chat()` calls that don't specify one. */
136
+ defaultMaxTokens?: number
137
+ /**
138
+ * Default embedding model for `brain.embed(...)`. No universal
139
+ * default — apps pull an embedding-tuned model (e.g.
140
+ * `nomic-embed-text`, `mxbai-embed-large`) and reference it here.
141
+ * Calls that omit `options.model` without this set throw.
142
+ */
143
+ defaultEmbedModel?: string
144
+ /**
145
+ * Default audio-transcription model for `brain.transcribe(...)`.
146
+ * No universal default — Ollama versions vary on whether they
147
+ * expose a Whisper-style endpoint, and apps pull whatever
148
+ * model their build supports (e.g. `whisper`). Calls that omit
149
+ * `options.model` without this set throw.
150
+ */
151
+ defaultTranscribeModel?: string
152
+ }
153
+
154
+ /**
155
+ * Qwen (Alibaba DashScope) driver config — backed by the `openai`
156
+ * SDK pointed at DashScope's OpenAI-compatible `/compatible-mode/v1`
157
+ * endpoint.
158
+ *
159
+ * DashScope publishes regional endpoints (Singapore, Beijing, Hong Kong,
160
+ * US-Virginia). The default is the Singapore endpoint
161
+ * (`dashscope-intl`) to fit Strav's SEA-first positioning; apps in
162
+ * other regions override via `baseUrl`.
163
+ */
164
+ export interface QwenProviderConfig {
165
+ driver: 'qwen'
166
+ /** API key. Required. Most apps source from `env('DASHSCOPE_API_KEY')` or `env('QWEN_API_KEY')`. */
167
+ apiKey: string
168
+ /**
169
+ * Optional base URL override. Defaults to the Singapore endpoint
170
+ * `https://dashscope-intl.aliyuncs.com/compatible-mode/v1`. Other
171
+ * documented options:
172
+ * - `https://dashscope.aliyuncs.com/compatible-mode/v1` (Beijing)
173
+ * - `https://dashscope-us.aliyuncs.com/compatible-mode/v1` (US/Virginia)
174
+ * - `https://cn-hongkong.dashscope.aliyuncs.com/compatible-mode/v1` (Hong Kong)
175
+ */
176
+ baseUrl?: string
177
+ /** Default model. Defaults to `qwen-plus` (mid-tier; `qwen-turbo` is cheaper, `qwen-max` is heaviest). */
178
+ defaultModel?: string
179
+ /** Default `max_tokens` for `chat()` calls that don't specify one. */
180
+ defaultMaxTokens?: number
181
+ }
182
+
183
+ /**
184
+ * MiniMax driver config — backed by the `openai` SDK pointed at
185
+ * MiniMax's OpenAI-compatible endpoint at `https://api.minimax.io/v1`.
186
+ */
187
+ export interface MiniMaxProviderConfig {
188
+ driver: 'minimax'
189
+ /** API key. Required. Most apps source from `env('MINIMAX_API_KEY')`. */
190
+ apiKey: string
191
+ /** Optional base URL override. Defaults to `https://api.minimax.io/v1`. */
192
+ baseUrl?: string
193
+ /** Default model. Defaults to `MiniMax-M2`. */
194
+ defaultModel?: string
195
+ /** Default `max_tokens` for `chat()` calls that don't specify one. */
196
+ defaultMaxTokens?: number
197
+ }
198
+
199
+ /**
200
+ * OpenRouter driver config — backed by the `openai` SDK pointed at
201
+ * `https://openrouter.ai/api/v1`. OpenRouter multiplexes 300+ models
202
+ * behind one OpenAI-compatible endpoint; **the surface a given call
203
+ * supports depends on the chosen model**. Pin an explicit model slug
204
+ * (e.g. `anthropic/claude-sonnet-4`, `meta-llama/llama-3.3-70b`) per
205
+ * call; consult `https://openrouter.ai/api/v1/models` for the
206
+ * `supported_parameters` per model. Calls to `embed` / `transcribe`
207
+ * are not exposed; structured output uses the OpenAI-compat
208
+ * tool-forcing pattern and depends on the model supporting
209
+ * function-calling.
210
+ */
211
+ export interface OpenRouterProviderConfig {
212
+ driver: 'openrouter'
213
+ /** API key. Required. Most apps source from `env('OPENROUTER_API_KEY')`. */
214
+ apiKey: string
215
+ /** Optional base URL override. Defaults to `https://openrouter.ai/api/v1`. */
216
+ baseUrl?: string
217
+ /**
218
+ * Default model. No default — OpenRouter has no canonical pick;
219
+ * apps must choose a slug (e.g. `anthropic/claude-sonnet-4`).
220
+ */
221
+ defaultModel?: string
222
+ /** Default `max_tokens` for `chat()` calls that don't specify one. */
223
+ defaultMaxTokens?: number
224
+ /**
225
+ * Optional app URL — forwarded as the `HTTP-Referer` header.
226
+ * OpenRouter uses it to attribute traffic on their leaderboard
227
+ * and rankings page.
228
+ */
229
+ appUrl?: string
230
+ /**
231
+ * Optional app title — forwarded as the `X-Title` header. Paired
232
+ * with `appUrl` for OpenRouter's leaderboard.
233
+ */
234
+ appTitle?: string
235
+ }
236
+
237
+ export type ProviderConfig =
238
+ | AnthropicProviderConfig
239
+ | OpenAIProviderConfig
240
+ | OpenAIResponsesProviderConfig
241
+ | GeminiProviderConfig
242
+ | DeepSeekProviderConfig
243
+ | OllamaProviderConfig
244
+ | QwenProviderConfig
245
+ | MiniMaxProviderConfig
246
+ | OpenRouterProviderConfig
37
247
 
38
248
  /** Cache-shape defaults applied when `ChatOptions.cache` is omitted. */
39
249
  export interface BrainCacheConfig {
@@ -55,6 +265,13 @@ export interface BrainConfigShape {
55
265
  tiers?: Partial<Record<ModelTier, string>>
56
266
  /** Prompt-cache defaults. */
57
267
  cache?: BrainCacheConfig
268
+ /**
269
+ * Default MCP servers — declared on every `runWithTools` call
270
+ * unless the per-call options provide their own list. Apps that
271
+ * need different MCP server sets per route override at the call
272
+ * site or via `Agent.mcpServers`.
273
+ */
274
+ mcpServers?: readonly MCPServer[]
58
275
  }
59
276
 
60
277
  /**
@@ -0,0 +1,247 @@
1
+ /**
2
+ * `BrainDriver` — the contract every brain backend implements.
3
+ *
4
+ * Each concrete driver (`AnthropicBrainDriver`, `OpenAIBrainDriver`,
5
+ * `GeminiBrainDriver`, `DeepSeekBrainDriver`, …) wraps the vendor's
6
+ * SDK and translates the framework shapes (`Message`, `ChatOptions`)
7
+ * into the vendor's native request, then translates the response back
8
+ * into `ChatResult` / `StreamEvent`.
9
+ *
10
+ * Drivers are values, not classes the app instantiates by name — apps
11
+ * use them via the `BrainManager` facade. The interface is exported so
12
+ * apps that need to plug in a custom backend (e.g. a local Ollama
13
+ * variant, or a private gateway) can do so without subclassing.
14
+ */
15
+
16
+ import type { AgentGenerateResult } from './agent_generate_result.ts'
17
+ import type { AgentResult } from './agent_result.ts'
18
+ import type { AgentStreamEvent } from './agent_stream_event.ts'
19
+ import type { MCPServer } from './mcp_server.ts'
20
+ import type { OutputSchema } from './output_schema.ts'
21
+ import type { SuspendedRun } from './suspended_run.ts'
22
+ import type { Tool } from './tool.ts'
23
+ import type { ToolExecutionError } from './tool_execution_error.ts'
24
+ import type {
25
+ AudioSource,
26
+ ChatOptions,
27
+ ChatResult,
28
+ EmbedOptions,
29
+ EmbedResult,
30
+ GenerateResult,
31
+ Message,
32
+ StreamEvent,
33
+ ToolUseBlock,
34
+ TranscribeOptions,
35
+ TranscribeResult,
36
+ } from './types.ts'
37
+
38
+ export interface RunWithToolsOptions extends ChatOptions {
39
+ /** Safety ceiling on tool-use round-trips. Default `10`. */
40
+ maxIterations?: number
41
+ /** Free-form context bag passed to every tool's `execute(input, ctx)`. */
42
+ context?: Record<string, unknown>
43
+ /**
44
+ * MCP servers Anthropic should connect to on this call. Merges
45
+ * with `config.brain.mcpServers` (per-call wins). Empty array or
46
+ * undefined → no MCP servers. Anthropic's backend handles tool
47
+ * discovery + invocation; the framework only surfaces the
48
+ * resulting `mcp_tool_use` / `mcp_tool_result` blocks.
49
+ */
50
+ mcpServers?: readonly MCPServer[]
51
+ /**
52
+ * Tool-error recovery hook. Called when a tool's `execute` throws
53
+ * — OR when the model called a tool that isn't registered. Two
54
+ * outcomes:
55
+ *
56
+ * - Return a string → the loop continues. The string lands as
57
+ * `tool_result.content` with `isError: true`, the model sees
58
+ * the error and can adapt (try a different approach, ask the
59
+ * user, give up). Recommended for production agents that
60
+ * should survive transient failures.
61
+ *
62
+ * - Return `undefined` (the default when this option is unset)
63
+ * → the framework throws `ToolExecutionError` and the loop
64
+ * aborts. Same behavior as before this option existed.
65
+ *
66
+ * The hook may inspect `error.cause` to filter — e.g., feed back
67
+ * transient HTTP errors but rethrow programmer errors:
68
+ *
69
+ * ```ts
70
+ * onToolError: (err) =>
71
+ * err.cause instanceof TransientError ? err.cause.message : undefined
72
+ * ```
73
+ */
74
+ onToolError?(error: ToolExecutionError): string | undefined
75
+ /**
76
+ * Human-in-the-loop gate. Called before each tool execution; when
77
+ * it returns `true`, the loop suspends and `runWithTools` returns
78
+ * a `SuspendedRun` carrying the pending tool calls + a JSON-
79
+ * serializable snapshot of the loop state. Apps obtain results
80
+ * out-of-band (human approval, queued worker, external system,
81
+ * ...) and call `brain.resumeTools(state, results, tools, options)`
82
+ * to continue.
83
+ *
84
+ * Mid-batch invariant: if a tool call inside a multi-call batch
85
+ * triggers suspension, the framework also captures all unexecuted
86
+ * siblings from the same assistant turn — the provider's
87
+ * `tool_use` / `tool_result` pairing must stay balanced on resume.
88
+ *
89
+ * V1 scope: only honored on non-streaming `runWithTools`. Pass it
90
+ * to `streamWithTools`, `runWithToolsAndSchema`, or
91
+ * `streamWithToolsAndSchema` and the framework throws `BrainError`
92
+ * — those entrypoints don't yet model the pause/resume protocol.
93
+ */
94
+ shouldSuspend?(
95
+ call: ToolUseBlock,
96
+ context?: Record<string, unknown>,
97
+ ): boolean | Promise<boolean>
98
+ }
99
+
100
+ /**
101
+ * Same as `RunWithToolsOptions` but with `shouldSuspend` required.
102
+ * Used to narrow the return type of `runWithTools` overloads — when
103
+ * apps opt in to the human-in-the-loop gate, the result widens to
104
+ * `AgentResult | SuspendedRun`; otherwise it's just `AgentResult`.
105
+ */
106
+ export type RunWithToolsOptionsWithSuspend = RunWithToolsOptions & {
107
+ shouldSuspend: NonNullable<RunWithToolsOptions['shouldSuspend']>
108
+ }
109
+
110
+ export interface BrainDriver {
111
+ /** Identifier — matches the `config.brain.providers` key. */
112
+ readonly name: string
113
+
114
+ /**
115
+ * Generate a single reply. Awaits the full response; for
116
+ * token-by-token rendering use `stream()`.
117
+ */
118
+ chat(messages: readonly Message[], options?: ChatOptions): Promise<ChatResult>
119
+
120
+ /**
121
+ * Stream the reply as it's generated. The async iterable yields
122
+ * `text` events for each delta and a final `stop` event with usage
123
+ * + stop-reason. Apps that want the full collected message at the
124
+ * end pass the same `messages` to `chat()` instead; this surface is
125
+ * for UI streaming, not for "make one call and get the message".
126
+ */
127
+ stream(messages: readonly Message[], options?: ChatOptions): AsyncIterable<StreamEvent>
128
+
129
+ /**
130
+ * Count input tokens for a given message set + options. Used by
131
+ * apps that need to budget context before sending. Optional — not
132
+ * every provider exposes a cheap token-count endpoint, so the
133
+ * implementation may approximate.
134
+ */
135
+ countTokens?(messages: readonly Message[], options?: ChatOptions): Promise<number>
136
+
137
+ /**
138
+ * Agentic loop. Sends the `messages` + `tools` to the model;
139
+ * detects tool-use blocks in the response; runs the matching
140
+ * tool's `execute`; appends the result and re-asks. Loops until
141
+ * the model returns `stop_reason: 'end_turn'` (or its
142
+ * provider-specific equivalent) or `maxIterations` is hit.
143
+ *
144
+ * Optional on the interface so providers that don't (yet) support
145
+ * tool use can omit it; `BrainManager.runTools` throws a
146
+ * `BrainError` when the configured provider lacks the method.
147
+ */
148
+ runWithTools?(
149
+ messages: readonly Message[],
150
+ tools: readonly Tool[],
151
+ options?: RunWithToolsOptions,
152
+ ): Promise<AgentResult | SuspendedRun>
153
+
154
+ /**
155
+ * Structured output. Sends `messages` to the model with a
156
+ * JSON-Schema constraint and returns the parsed object. Apps that
157
+ * supplied `schema.parse` get a runtime-validated value; otherwise
158
+ * the value is `T` by type assertion (the provider does its own
159
+ * upstream schema enforcement, but the framework doesn't validate).
160
+ *
161
+ * Optional on the interface so providers that lack a structured-
162
+ * output endpoint can omit it; `BrainManager.generate` throws a
163
+ * `BrainError` when the configured provider doesn't expose this.
164
+ */
165
+ generate?<T>(
166
+ messages: readonly Message[],
167
+ schema: OutputSchema<T>,
168
+ options?: ChatOptions,
169
+ ): Promise<GenerateResult<T>>
170
+
171
+ /**
172
+ * Tool-loop + structured output combined. Runs the agentic loop
173
+ * with the same tool-handling as `runWithTools`, but pins a
174
+ * JSON-Schema constraint on every turn — so when the model
175
+ * finally answers without calling a tool, its text is JSON
176
+ * matching the schema. Returns the parsed value alongside the
177
+ * loop bookkeeping.
178
+ *
179
+ * Optional on the interface; `BrainManager.generateWithTools`
180
+ * throws `BrainError` when the configured provider lacks it.
181
+ */
182
+ runWithToolsAndSchema?<T>(
183
+ messages: readonly Message[],
184
+ tools: readonly Tool[],
185
+ schema: OutputSchema<T>,
186
+ options?: RunWithToolsOptions,
187
+ ): Promise<AgentGenerateResult<T>>
188
+
189
+ /**
190
+ * Streaming variant of `runWithToolsAndSchema`. Same agentic loop,
191
+ * same schema constraint on every turn — yielded as
192
+ * `AgentStreamEvent<T>`s. The terminal `stop` event carries the
193
+ * parsed `value` + raw `text` alongside the loop bookkeeping.
194
+ *
195
+ * Optional; `BrainManager.streamGenerateWithTools` throws
196
+ * `BrainError` when the chosen provider doesn't implement it.
197
+ */
198
+ streamWithToolsAndSchema?<T>(
199
+ messages: readonly Message[],
200
+ tools: readonly Tool[],
201
+ schema: OutputSchema<T>,
202
+ options?: RunWithToolsOptions,
203
+ ): AsyncIterable<AgentStreamEvent<T>>
204
+
205
+ /**
206
+ * Streaming variant of `runWithTools`. Yields `AgentStreamEvent`s
207
+ * as the loop progresses — text deltas during model turns,
208
+ * `tool_use` / `tool_result` boundaries around tool execution,
209
+ * `iteration_start` / `iteration_end` per round, a terminal
210
+ * `stop` with the full trace + usage.
211
+ *
212
+ * Optional — providers without a streaming tool-loop implementation
213
+ * can omit it; `BrainManager.streamTools` throws `BrainError` in
214
+ * that case.
215
+ */
216
+ streamWithTools?(
217
+ messages: readonly Message[],
218
+ tools: readonly Tool[],
219
+ options?: RunWithToolsOptions,
220
+ ): AsyncIterable<AgentStreamEvent>
221
+
222
+ /**
223
+ * Embeddings — turn one or more text inputs into vectors for
224
+ * similarity search / RAG / clustering. Optional because not
225
+ * every provider exposes an embeddings endpoint (V1: Anthropic
226
+ * and DeepSeek don't; OpenAI, Gemini, Ollama do).
227
+ */
228
+ embed?(
229
+ texts: readonly string[],
230
+ options?: EmbedOptions,
231
+ ): Promise<EmbedResult>
232
+
233
+ /**
234
+ * Audio transcription — convert an audio clip to text.
235
+ * Complements `AudioBlock` (which sends audio + text together
236
+ * to a multimodal chat model) by exposing the dedicated
237
+ * transcription endpoint where the provider has one. V1:
238
+ * OpenAI (Whisper / gpt-4o-transcribe), Ollama (inherits via
239
+ * OpenAI-compat), Gemini (chat-wrap fallback — internally
240
+ * sends an AudioBlock with a "transcribe verbatim" prompt).
241
+ * Anthropic + DeepSeek throw — no transcription API.
242
+ */
243
+ transcribe?(
244
+ audio: AudioSource,
245
+ options?: TranscribeOptions,
246
+ ): Promise<TranscribeResult>
247
+ }
@@ -1,16 +1,32 @@
1
1
  /**
2
- * `BrainError` — typed wrapper for failures originating in the brain
3
- * stack. Provider-native errors (e.g. `Anthropic.RateLimitError`) are
4
- * preserved on `.cause` so apps can `instanceof`-check them when they
5
- * need provider-specific recovery; the wrapping just gives the
6
- * framework a consistent `StravError` to render through the standard
2
+ * `BrainError` hierarchy — typed wrappers for failures originating
3
+ * in the brain stack.
4
+ *
5
+ * Provider-native errors (e.g. `Anthropic.RateLimitError`) are
6
+ * preserved on `.cause` so apps can `instanceof`-check them when
7
+ * they need vendor-specific recovery; the framework wrapping gives
8
+ * a consistent `StravError` to render through the standard
7
9
  * exception handler.
8
10
  *
9
- * Subclassing surface deferred V1 has one error type. When a real
10
- * use case appears for distinguishing "model refused" vs "rate
11
- * limited" at the framework level (rather than `instanceof
12
- * Anthropic.RateLimitError` at the call site), a typed hierarchy
13
- * lands.
11
+ * Subclasses ship in v1 for the boot / lookup / usage paths.
12
+ * Vendor-side runtime errors use `BrainProviderError` as the
13
+ * generic wrapper. Granular vendor classes (rate-limit, content
14
+ * filter, etc.) land when apps actually need to branch on them at
15
+ * the framework level — until then, `instanceof Anthropic.RateLimitError`
16
+ * on `.cause` is the call-site pattern.
17
+ *
18
+ * - `BrainConfigError` — boot-time misconfiguration (missing
19
+ * provider in `config.brain.providers`, default key absent).
20
+ *
21
+ * - `UnknownProviderError` — `brain.provider(name)` for a name
22
+ * that wasn't registered.
23
+ *
24
+ * - `BrainUsageError` — pre-condition violations from the
25
+ * framework's own API contract (e.g. `AgentRunner.run` called
26
+ * before `input()`).
27
+ *
28
+ * - `BrainProviderError` — wraps a vendor exception. `cause` is
29
+ * preserved; default status 502.
14
30
  */
15
31
 
16
32
  import { StravError } from '@strav/kernel'
@@ -27,3 +43,63 @@ export class BrainError extends StravError {
27
43
  )
28
44
  }
29
45
  }
46
+
47
+ export class BrainConfigError extends BrainError {
48
+ constructor(
49
+ message: string,
50
+ options: { context?: Record<string, unknown> } = {},
51
+ ) {
52
+ super(message, options)
53
+ // Reassign code/status via the underlying StravError props (the
54
+ // base constructor froze them with `brain.error`); we read them
55
+ // back through getters so subclass-specific overrides surface in
56
+ // logs.
57
+ Object.defineProperty(this, 'code', { value: 'brain.config' })
58
+ Object.defineProperty(this, 'status', { value: 500 })
59
+ }
60
+ }
61
+
62
+ export class UnknownProviderError extends BrainError {
63
+ constructor(name: string, available: readonly string[]) {
64
+ super(
65
+ `Brain provider "${name}" is not registered. Available: ${available.join(', ') || '<none>'}.`,
66
+ { context: { requested: name, available } },
67
+ )
68
+ Object.defineProperty(this, 'code', { value: 'brain.unknown_provider' })
69
+ Object.defineProperty(this, 'status', { value: 400 })
70
+ }
71
+ }
72
+
73
+ export class BrainUsageError extends BrainError {
74
+ constructor(
75
+ message: string,
76
+ options: { context?: Record<string, unknown> } = {},
77
+ ) {
78
+ super(message, options)
79
+ Object.defineProperty(this, 'code', { value: 'brain.usage' })
80
+ Object.defineProperty(this, 'status', { value: 500 })
81
+ }
82
+ }
83
+
84
+ export class BrainProviderError extends BrainError {
85
+ constructor(
86
+ message: string,
87
+ options: {
88
+ provider: string
89
+ operation: string
90
+ context?: Record<string, unknown>
91
+ cause?: unknown
92
+ },
93
+ ) {
94
+ super(message, {
95
+ context: {
96
+ provider: options.provider,
97
+ operation: options.operation,
98
+ ...(options.context ?? {}),
99
+ },
100
+ ...(options.cause !== undefined ? { cause: options.cause } : {}),
101
+ })
102
+ Object.defineProperty(this, 'code', { value: 'brain.provider_error' })
103
+ Object.defineProperty(this, 'status', { value: 502 })
104
+ }
105
+ }