@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.
- package/package.json +23 -7
- package/src/agent.ts +43 -5
- package/src/agent_generate_result.ts +32 -0
- package/src/agent_result.ts +7 -0
- package/src/agent_runner.ts +218 -14
- package/src/agent_stream_event.ts +100 -0
- package/src/brain_config.ts +218 -1
- package/src/brain_driver.ts +247 -0
- package/src/brain_error.ts +86 -10
- package/src/brain_manager.ts +359 -11
- package/src/brain_provider.ts +79 -9
- package/src/drivers/anthropic/anthropic_brain_driver.ts +641 -0
- package/src/drivers/anthropic/anthropic_helpers.ts +65 -0
- package/src/drivers/anthropic/anthropic_message_builder.ts +258 -0
- package/src/drivers/anthropic/anthropic_response_mapper.ts +123 -0
- package/src/drivers/anthropic/anthropic_tool_loop.ts +246 -0
- package/src/drivers/anthropic/index.ts +1 -0
- package/src/drivers/deepseek/deepseek_brain_driver.ts +117 -0
- package/src/drivers/deepseek/index.ts +1 -0
- package/src/drivers/gemini/gemini_brain_driver.ts +1064 -0
- package/src/drivers/gemini/index.ts +1 -0
- package/src/drivers/minimax/index.ts +1 -0
- package/src/drivers/minimax/minimax_brain_driver.ts +84 -0
- package/src/drivers/ollama/index.ts +1 -0
- package/src/drivers/ollama/ollama_brain_driver.ts +86 -0
- package/src/drivers/openai/index.ts +1 -0
- package/src/drivers/openai/openai_brain_driver.ts +796 -0
- package/src/drivers/openai/openai_helpers.ts +58 -0
- package/src/drivers/openai/openai_message_builder.ts +187 -0
- package/src/drivers/openai/openai_response_mapper.ts +70 -0
- package/src/drivers/openai/openai_tool_dispatch.ts +127 -0
- package/src/drivers/openai/openai_tool_loop.ts +191 -0
- package/src/drivers/openai_compat/index.ts +1 -0
- package/src/drivers/openai_compat/openai_compat_brain_driver.ts +616 -0
- package/src/drivers/openai_responses/index.ts +1 -0
- package/src/drivers/openai_responses/openai_responses_brain_driver.ts +1015 -0
- package/src/drivers/openrouter/index.ts +1 -0
- package/src/drivers/openrouter/openrouter_brain_driver.ts +137 -0
- package/src/drivers/qwen/index.ts +1 -0
- package/src/drivers/qwen/qwen_brain_driver.ts +103 -0
- package/src/index.ts +75 -11
- package/src/mcp/client.ts +243 -0
- package/src/mcp/index.ts +23 -0
- package/src/mcp/oauth.ts +227 -0
- package/src/mcp/pool.ts +106 -0
- package/src/mcp/resolve_mcp_tools.ts +108 -0
- package/src/mcp_server.ts +63 -0
- package/src/output_schema.ts +72 -0
- package/src/persistence/brain_message.ts +34 -0
- package/src/persistence/brain_message_repository.ts +98 -0
- package/src/persistence/brain_store.ts +166 -0
- package/src/persistence/brain_suspended_run.ts +30 -0
- package/src/persistence/brain_suspended_run_repository.ts +59 -0
- package/src/persistence/brain_thread.ts +30 -0
- package/src/persistence/brain_thread_repository.ts +56 -0
- package/src/persistence/database_brain_store.ts +190 -0
- package/src/persistence/index.ts +48 -0
- package/src/persistence/schemas/brain_message_schema.ts +61 -0
- package/src/persistence/schemas/brain_suspended_run_schema.ts +58 -0
- package/src/persistence/schemas/brain_thread_schema.ts +50 -0
- package/src/persistence/schemas/index.ts +3 -0
- package/src/suspended_run.ts +153 -0
- package/src/thread.ts +40 -1
- package/src/tool.ts +7 -0
- package/src/tool_runner.ts +81 -0
- package/src/translate/index.ts +19 -0
- package/src/translate/translate_cache.ts +78 -0
- package/src/translate/translate_provider.ts +46 -0
- package/src/translate/translator.ts +271 -0
- package/src/types.ts +398 -1
- package/src/zod/index.ts +121 -0
- package/src/provider.ts +0 -74
- package/src/providers/anthropic_provider.ts +0 -397
package/src/brain_config.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/brain_error.ts
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `BrainError` — typed
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
+
}
|