@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_manager.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `BrainManager` — the per-app facade apps inject and call.
|
|
3
3
|
*
|
|
4
|
-
* Holds the configured `
|
|
4
|
+
* Holds the configured `BrainDriver` registry + the default-provider key
|
|
5
5
|
* + the tier-to-model map. Apps call `chat / stream / countTokens`
|
|
6
6
|
* with framework-native types; the manager resolves which provider
|
|
7
7
|
* runs the call (default unless `options.provider` overrides),
|
|
@@ -21,38 +21,61 @@
|
|
|
21
21
|
|
|
22
22
|
import type { Agent } from './agent.ts'
|
|
23
23
|
import type { AgentResult } from './agent_result.ts'
|
|
24
|
+
import type { AgentStreamEvent } from './agent_stream_event.ts'
|
|
25
|
+
import type { MCPServer } from './mcp_server.ts'
|
|
26
|
+
import type { AgentGenerateResult } from './agent_generate_result.ts'
|
|
27
|
+
import type { OutputSchema } from './output_schema.ts'
|
|
24
28
|
import { AgentRunner } from './agent_runner.ts'
|
|
25
29
|
import { BrainError } from './brain_error.ts'
|
|
26
30
|
import type { ModelTier } from './types.ts'
|
|
27
31
|
import type {
|
|
32
|
+
AudioSource,
|
|
28
33
|
ChatOptions,
|
|
29
34
|
ChatResult,
|
|
35
|
+
EmbedOptions,
|
|
36
|
+
EmbedResult,
|
|
37
|
+
GenerateResult,
|
|
30
38
|
Message,
|
|
31
39
|
StreamEvent,
|
|
40
|
+
TranscribeOptions,
|
|
41
|
+
TranscribeResult,
|
|
32
42
|
} from './types.ts'
|
|
33
|
-
import type {
|
|
43
|
+
import type {
|
|
44
|
+
BrainDriver,
|
|
45
|
+
RunWithToolsOptions,
|
|
46
|
+
RunWithToolsOptionsWithSuspend,
|
|
47
|
+
} from './brain_driver.ts'
|
|
48
|
+
import { appendResumeResults, type SuspendedRun, type SuspendedState, type ToolResultInput } from './suspended_run.ts'
|
|
34
49
|
import type { Tool } from './tool.ts'
|
|
35
50
|
import { DEFAULT_TIERS } from './brain_config.ts'
|
|
36
51
|
|
|
37
52
|
/** Container-aware Agent constructor resolver — `BrainProvider` installs one wired to `app.resolve(...)`. */
|
|
38
|
-
export type AgentResolver = <A extends Agent
|
|
53
|
+
export type AgentResolver = <A extends Agent<unknown>>(cls: new (...args: never[]) => A) => A
|
|
39
54
|
|
|
40
55
|
export interface BrainManagerOptions {
|
|
41
56
|
/** Name of the default provider — must exist in `providers`. */
|
|
42
57
|
default: string
|
|
43
58
|
/** Provider registry keyed by name. */
|
|
44
|
-
providers: Record<string,
|
|
59
|
+
providers: Record<string, BrainDriver>
|
|
45
60
|
/** Tier-to-model overrides; merged on top of the framework defaults. */
|
|
46
61
|
tiers?: Partial<Record<ModelTier, string>>
|
|
47
62
|
/** Default for `ChatOptions.cache` when the call site doesn't pass one. */
|
|
48
63
|
defaultCache?: boolean
|
|
64
|
+
/**
|
|
65
|
+
* Default MCP servers used on every `runTools` call when the per-call
|
|
66
|
+
* options don't specify them. Per-call `mcpServers` replaces the
|
|
67
|
+
* default outright (no merge) — apps that want additive behavior
|
|
68
|
+
* concat at the call site.
|
|
69
|
+
*/
|
|
70
|
+
defaultMcpServers?: readonly MCPServer[]
|
|
49
71
|
}
|
|
50
72
|
|
|
51
73
|
export class BrainManager {
|
|
52
74
|
readonly defaultProvider: string
|
|
53
|
-
private readonly providers: Map<string,
|
|
75
|
+
private readonly providers: Map<string, BrainDriver>
|
|
54
76
|
private readonly tiers: Record<ModelTier, string>
|
|
55
77
|
private readonly defaultCache: boolean
|
|
78
|
+
private readonly defaultMcpServers: readonly MCPServer[]
|
|
56
79
|
|
|
57
80
|
constructor(options: BrainManagerOptions) {
|
|
58
81
|
if (!options.providers[options.default]) {
|
|
@@ -65,10 +88,11 @@ export class BrainManager {
|
|
|
65
88
|
this.providers = new Map(Object.entries(options.providers))
|
|
66
89
|
this.tiers = { ...DEFAULT_TIERS, ...(options.tiers ?? {}) }
|
|
67
90
|
this.defaultCache = options.defaultCache ?? false
|
|
91
|
+
this.defaultMcpServers = options.defaultMcpServers ?? []
|
|
68
92
|
}
|
|
69
93
|
|
|
70
94
|
/** Resolve a provider by name. Default when no name passed. Throws when unknown. */
|
|
71
|
-
provider(name?: string):
|
|
95
|
+
provider(name?: string): BrainDriver {
|
|
72
96
|
const key = name ?? this.defaultProvider
|
|
73
97
|
const provider = this.providers.get(key)
|
|
74
98
|
if (!provider) {
|
|
@@ -79,6 +103,29 @@ export class BrainManager {
|
|
|
79
103
|
return provider
|
|
80
104
|
}
|
|
81
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Register an additional provider after construction. Apps that
|
|
108
|
+
* wire a custom adapter (a fine-tuned model server, a fork of
|
|
109
|
+
* Anthropic with extra knobs, an internal LLM) use this to add it
|
|
110
|
+
* to the registry without going through `config.brain.providers`.
|
|
111
|
+
*
|
|
112
|
+
* Overwrites any existing provider under the same name.
|
|
113
|
+
*
|
|
114
|
+
* ```ts
|
|
115
|
+
* brain.extend('internal', new InternalLlmProvider({ baseUrl }))
|
|
116
|
+
* const reply = await brain.chat(messages, { provider: 'internal' })
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* Mirrors `RagManager.extend(name, factory)` / `PaymentManager.extend(name, factory)`
|
|
120
|
+
* — the OCP escape hatch every multi-driver Strav manager exposes.
|
|
121
|
+
*/
|
|
122
|
+
extend(name: string, provider: BrainDriver): void {
|
|
123
|
+
if (!name) {
|
|
124
|
+
throw new BrainError('BrainManager.extend: provider name must be a non-empty string.')
|
|
125
|
+
}
|
|
126
|
+
this.providers.set(name, provider)
|
|
127
|
+
}
|
|
128
|
+
|
|
82
129
|
/**
|
|
83
130
|
* One-shot chat: send the messages, await the full reply.
|
|
84
131
|
*
|
|
@@ -131,13 +178,23 @@ export class BrainManager {
|
|
|
131
178
|
*
|
|
132
179
|
* Throws `BrainError` when the configured provider doesn't
|
|
133
180
|
* implement `runWithTools` (V1: OpenAI / Gemini / DeepSeek providers
|
|
134
|
-
* don't yet — only `
|
|
181
|
+
* don't yet — only `AnthropicBrainDriver`).
|
|
135
182
|
*/
|
|
183
|
+
runTools(
|
|
184
|
+
input: string | readonly Message[],
|
|
185
|
+
tools: readonly Tool[],
|
|
186
|
+
options: RunWithToolsOptionsWithSuspend,
|
|
187
|
+
): Promise<AgentResult | SuspendedRun>
|
|
188
|
+
runTools(
|
|
189
|
+
input: string | readonly Message[],
|
|
190
|
+
tools: readonly Tool[],
|
|
191
|
+
options?: RunWithToolsOptions,
|
|
192
|
+
): Promise<AgentResult>
|
|
136
193
|
async runTools(
|
|
137
194
|
input: string | readonly Message[],
|
|
138
195
|
tools: readonly Tool[],
|
|
139
196
|
options: RunWithToolsOptions = {},
|
|
140
|
-
): Promise<AgentResult> {
|
|
197
|
+
): Promise<AgentResult | SuspendedRun> {
|
|
141
198
|
const provider = this.provider(options.provider)
|
|
142
199
|
if (!provider.runWithTools) {
|
|
143
200
|
throw new BrainError(
|
|
@@ -147,24 +204,252 @@ export class BrainManager {
|
|
|
147
204
|
}
|
|
148
205
|
const messages = normalizeInput(input)
|
|
149
206
|
const resolved = this.applyDefaults(options) as RunWithToolsOptions
|
|
207
|
+
// MCP defaults — per-call override (when present) replaces the
|
|
208
|
+
// configured list outright; apps that want concat behavior
|
|
209
|
+
// construct the merged array themselves and pass it in.
|
|
210
|
+
if (resolved.mcpServers === undefined && this.defaultMcpServers.length > 0) {
|
|
211
|
+
resolved.mcpServers = this.defaultMcpServers
|
|
212
|
+
}
|
|
150
213
|
return provider.runWithTools(messages, tools, resolved)
|
|
151
214
|
}
|
|
152
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Resume a previously-suspended tool-use loop. Takes the
|
|
218
|
+
* `SuspendedRun.state` snapshot plus the results the integrator
|
|
219
|
+
* gathered for each `pendingToolCalls` entry; appends a `tool_result`
|
|
220
|
+
* block per entry; re-enters `runTools` so the model can continue
|
|
221
|
+
* (potentially suspending again on the next tool).
|
|
222
|
+
*
|
|
223
|
+
* Mid-batch invariant: every pending call MUST get a result —
|
|
224
|
+
* otherwise the provider rejects the next request because the
|
|
225
|
+
* assistant turn's `tool_use` blocks are no longer balanced.
|
|
226
|
+
* `resumeTools` throws `BrainError` when results are missing.
|
|
227
|
+
*
|
|
228
|
+
* The `previousResponseId` carried on the snapshot (when the
|
|
229
|
+
* provider supports stateful conversations) is threaded back via
|
|
230
|
+
* `options.previousResponseId` automatically — per-call
|
|
231
|
+
* `options.previousResponseId` wins if supplied explicitly.
|
|
232
|
+
*/
|
|
233
|
+
async resumeTools(
|
|
234
|
+
state: SuspendedState,
|
|
235
|
+
results: readonly ToolResultInput[],
|
|
236
|
+
tools: readonly Tool[],
|
|
237
|
+
options: RunWithToolsOptions = {},
|
|
238
|
+
): Promise<AgentResult | SuspendedRun> {
|
|
239
|
+
const resumed = appendResumeResults(state, results)
|
|
240
|
+
const merged: RunWithToolsOptions = { ...options }
|
|
241
|
+
if (merged.previousResponseId === undefined && state.responseId !== undefined) {
|
|
242
|
+
merged.previousResponseId = state.responseId
|
|
243
|
+
}
|
|
244
|
+
const out = await this.runTools(
|
|
245
|
+
resumed,
|
|
246
|
+
tools,
|
|
247
|
+
merged as RunWithToolsOptionsWithSuspend,
|
|
248
|
+
)
|
|
249
|
+
return mergeResumeCounters(out, state)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Streaming variant of `generateWithTools`. Yields
|
|
254
|
+
* `AgentStreamEvent<T>`s as the loop progresses; the terminal
|
|
255
|
+
* `stop` event carries the parsed value + raw JSON text. Throws
|
|
256
|
+
* `BrainError` when the provider lacks
|
|
257
|
+
* `streamWithToolsAndSchema` (V1: all three providers
|
|
258
|
+
* implement it).
|
|
259
|
+
*/
|
|
260
|
+
streamGenerateWithTools<T>(
|
|
261
|
+
input: string | readonly Message[],
|
|
262
|
+
schema: OutputSchema<T>,
|
|
263
|
+
tools: readonly Tool[],
|
|
264
|
+
options: RunWithToolsOptions = {},
|
|
265
|
+
): AsyncIterable<AgentStreamEvent<T>> {
|
|
266
|
+
rejectShouldSuspend(options, 'streamGenerateWithTools')
|
|
267
|
+
const provider = this.provider(options.provider)
|
|
268
|
+
if (!provider.streamWithToolsAndSchema) {
|
|
269
|
+
throw new BrainError(
|
|
270
|
+
`BrainManager.streamGenerateWithTools: provider "${provider.name}" does not implement streamWithToolsAndSchema.`,
|
|
271
|
+
{ context: { provider: provider.name } },
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
const messages = normalizeInput(input)
|
|
275
|
+
const resolved = this.applyDefaults(options) as RunWithToolsOptions
|
|
276
|
+
if (resolved.mcpServers === undefined && this.defaultMcpServers.length > 0) {
|
|
277
|
+
resolved.mcpServers = this.defaultMcpServers
|
|
278
|
+
}
|
|
279
|
+
return provider.streamWithToolsAndSchema<T>(messages, tools, schema, resolved)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Tool-loop + structured output combined. Runs the agentic loop
|
|
284
|
+
* with the supplied `tools` while pinning the output to `schema`
|
|
285
|
+
* on every turn; returns the parsed value when the model finally
|
|
286
|
+
* answers without calling a tool. MCP defaults + tier resolution
|
|
287
|
+
* + provider routing match `runTools` / `generate`.
|
|
288
|
+
*
|
|
289
|
+
* Throws `BrainError` when the chosen provider doesn't implement
|
|
290
|
+
* `runWithToolsAndSchema`. V1: all three providers do.
|
|
291
|
+
*/
|
|
292
|
+
async generateWithTools<T>(
|
|
293
|
+
input: string | readonly Message[],
|
|
294
|
+
schema: OutputSchema<T>,
|
|
295
|
+
tools: readonly Tool[],
|
|
296
|
+
options: RunWithToolsOptions = {},
|
|
297
|
+
): Promise<AgentGenerateResult<T>> {
|
|
298
|
+
rejectShouldSuspend(options, 'generateWithTools')
|
|
299
|
+
const provider = this.provider(options.provider)
|
|
300
|
+
if (!provider.runWithToolsAndSchema) {
|
|
301
|
+
throw new BrainError(
|
|
302
|
+
`BrainManager.generateWithTools: provider "${provider.name}" does not implement runWithToolsAndSchema.`,
|
|
303
|
+
{ context: { provider: provider.name } },
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
const messages = normalizeInput(input)
|
|
307
|
+
const resolved = this.applyDefaults(options) as RunWithToolsOptions
|
|
308
|
+
if (resolved.mcpServers === undefined && this.defaultMcpServers.length > 0) {
|
|
309
|
+
resolved.mcpServers = this.defaultMcpServers
|
|
310
|
+
}
|
|
311
|
+
return provider.runWithToolsAndSchema<T>(messages, tools, schema, resolved)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Streaming variant of `runTools`. Yields `AgentStreamEvent`s
|
|
316
|
+
* as the agentic loop progresses — text deltas during model
|
|
317
|
+
* turns, `tool_use` / `tool_result` boundaries around tool
|
|
318
|
+
* execution, `iteration_start` / `iteration_end` per round, a
|
|
319
|
+
* terminal `stop` with the full trace + usage.
|
|
320
|
+
*
|
|
321
|
+
* Throws `BrainError` when the configured provider doesn't
|
|
322
|
+
* implement `streamWithTools`.
|
|
323
|
+
*/
|
|
324
|
+
streamTools(
|
|
325
|
+
input: string | readonly Message[],
|
|
326
|
+
tools: readonly Tool[],
|
|
327
|
+
options: RunWithToolsOptions = {},
|
|
328
|
+
): AsyncIterable<AgentStreamEvent> {
|
|
329
|
+
rejectShouldSuspend(options, 'streamTools')
|
|
330
|
+
const provider = this.provider(options.provider)
|
|
331
|
+
if (!provider.streamWithTools) {
|
|
332
|
+
throw new BrainError(
|
|
333
|
+
`BrainManager.streamTools: provider "${provider.name}" does not implement streamWithTools.`,
|
|
334
|
+
{ context: { provider: provider.name } },
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
const messages = normalizeInput(input)
|
|
338
|
+
const resolved = this.applyDefaults(options) as RunWithToolsOptions
|
|
339
|
+
if (resolved.mcpServers === undefined && this.defaultMcpServers.length > 0) {
|
|
340
|
+
resolved.mcpServers = this.defaultMcpServers
|
|
341
|
+
}
|
|
342
|
+
return provider.streamWithTools(messages, tools, resolved)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Structured output. Sends `input` to the configured (or
|
|
347
|
+
* `options.provider`-overridden) provider with the JSON-Schema
|
|
348
|
+
* constraint described by `schema`; returns the parsed object.
|
|
349
|
+
*
|
|
350
|
+
* Throws `BrainError` when the chosen provider doesn't implement
|
|
351
|
+
* `generate`. All three V1 providers (Anthropic, OpenAI, Gemini)
|
|
352
|
+
* do.
|
|
353
|
+
*/
|
|
354
|
+
async generate<T>(
|
|
355
|
+
input: string | readonly Message[],
|
|
356
|
+
schema: OutputSchema<T>,
|
|
357
|
+
options: ChatOptions = {},
|
|
358
|
+
): Promise<GenerateResult<T>> {
|
|
359
|
+
const provider = this.provider(options.provider)
|
|
360
|
+
if (!provider.generate) {
|
|
361
|
+
throw new BrainError(
|
|
362
|
+
`BrainManager.generate: provider "${provider.name}" does not implement generate.`,
|
|
363
|
+
{ context: { provider: provider.name } },
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
const messages = normalizeInput(input)
|
|
367
|
+
const resolved = this.applyDefaults(options)
|
|
368
|
+
return provider.generate<T>(messages, schema, resolved)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Turn one or more text inputs into embedding vectors. Accepts
|
|
373
|
+
* either a single string (returns one vector) or an array
|
|
374
|
+
* (batch — returns one vector per input in the same order).
|
|
375
|
+
*
|
|
376
|
+
* Throws `BrainError` when the configured (or
|
|
377
|
+
* `options.provider`-overridden) provider doesn't implement
|
|
378
|
+
* `embed`. V1: OpenAI, Gemini, Ollama support it; Anthropic +
|
|
379
|
+
* DeepSeek throw with a clear "route to a different provider"
|
|
380
|
+
* message.
|
|
381
|
+
*/
|
|
382
|
+
async embed(
|
|
383
|
+
input: string | readonly string[],
|
|
384
|
+
options: EmbedOptions = {},
|
|
385
|
+
): Promise<EmbedResult> {
|
|
386
|
+
const provider = this.provider(options.provider)
|
|
387
|
+
if (!provider.embed) {
|
|
388
|
+
throw new BrainError(
|
|
389
|
+
`BrainManager.embed: provider "${provider.name}" does not implement embed. Route to a provider with an embeddings API (V1: OpenAI / Gemini / Ollama).`,
|
|
390
|
+
{ context: { provider: provider.name } },
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
const texts = typeof input === 'string' ? [input] : input
|
|
394
|
+
return provider.embed(texts, options)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Transcribe one audio clip to text. Complements `AudioBlock`
|
|
399
|
+
* (which sends audio + a text prompt together to a multimodal
|
|
400
|
+
* chat model) by exposing the dedicated transcription endpoint
|
|
401
|
+
* where the provider has one. Apps that already have an
|
|
402
|
+
* `AudioBlock` can pass its `source` directly.
|
|
403
|
+
*
|
|
404
|
+
* Throws `BrainError` when the configured (or
|
|
405
|
+
* `options.provider`-overridden) provider doesn't implement
|
|
406
|
+
* `transcribe`. V1: OpenAI / Ollama (Whisper / gpt-4o-transcribe
|
|
407
|
+
* / local) and Gemini (chat-wrap fallback); Anthropic +
|
|
408
|
+
* DeepSeek throw.
|
|
409
|
+
*/
|
|
410
|
+
async transcribe(
|
|
411
|
+
audio: AudioSource,
|
|
412
|
+
options: TranscribeOptions = {},
|
|
413
|
+
): Promise<TranscribeResult> {
|
|
414
|
+
const provider = this.provider(options.provider)
|
|
415
|
+
if (!provider.transcribe) {
|
|
416
|
+
throw new BrainError(
|
|
417
|
+
`BrainManager.transcribe: provider "${provider.name}" does not implement transcribe. Route to a provider with audio support (V1: OpenAI / Ollama / Gemini).`,
|
|
418
|
+
{ context: { provider: provider.name } },
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
return provider.transcribe(audio, options)
|
|
422
|
+
}
|
|
423
|
+
|
|
153
424
|
/**
|
|
154
425
|
* Resolve an `Agent` subclass from the container and return an
|
|
155
426
|
* `AgentRunner` ready to receive `input(...)` and `run()`. Apps
|
|
156
427
|
* `@inject()`-decorate their Agent subclass so constructor
|
|
157
428
|
* injection of dependencies (Repositories, services, etc.) flows
|
|
158
429
|
* through normally.
|
|
430
|
+
*
|
|
431
|
+
* When the agent subclass extends `Agent<T>` for some `T` and
|
|
432
|
+
* declares `outputSchema`, the returned runner is typed as
|
|
433
|
+
* `AgentRunner<T>` and the schema is pre-applied — `.run()`
|
|
434
|
+
* returns `AgentGenerateResult<T>` without a per-call
|
|
435
|
+
* `.output(schema)`. Apps can still chain `.output(otherSchema)`
|
|
436
|
+
* to override.
|
|
159
437
|
*/
|
|
160
|
-
agent<
|
|
438
|
+
agent<T = never>(
|
|
439
|
+
AgentClass: new (...args: never[]) => Agent<T>,
|
|
440
|
+
instance?: Agent<T>,
|
|
441
|
+
): AgentRunner<T> {
|
|
161
442
|
const agent = instance ?? this.resolveAgent(AgentClass)
|
|
162
|
-
|
|
443
|
+
const runner = new AgentRunner<T>(this, agent)
|
|
444
|
+
if (agent.outputSchema !== undefined) {
|
|
445
|
+
return runner.output(agent.outputSchema)
|
|
446
|
+
}
|
|
447
|
+
return runner
|
|
163
448
|
}
|
|
164
449
|
|
|
165
450
|
// ─── Internal ────────────────────────────────────────────────────────────
|
|
166
451
|
|
|
167
|
-
private resolveAgent<A extends Agent
|
|
452
|
+
private resolveAgent<A extends Agent<unknown>>(AgentClass: new (...args: never[]) => A): A {
|
|
168
453
|
if (this.agentResolver) return this.agentResolver(AgentClass)
|
|
169
454
|
// Fallback: assume the Agent class is constructible without args.
|
|
170
455
|
// Apps that need DI on the agent register a resolver via
|
|
@@ -203,3 +488,66 @@ function normalizeInput(input: string | readonly Message[]): readonly Message[]
|
|
|
203
488
|
}
|
|
204
489
|
return input
|
|
205
490
|
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* V1 scope guard. `shouldSuspend` is wired only into the non-
|
|
494
|
+
* streaming `runWithTools` loop; the streaming and schema variants
|
|
495
|
+
* don't yet model pause / resume, so silently ignoring would be
|
|
496
|
+
* worse than throwing. Apps that need both should run tools first
|
|
497
|
+
* (suspending as needed), then call `generate` for the structured
|
|
498
|
+
* summary in a separate step.
|
|
499
|
+
*/
|
|
500
|
+
/**
|
|
501
|
+
* Carry forward the pre-suspension iteration count + token usage so
|
|
502
|
+
* `result.iterations` / `result.usage` reflect the full run, not
|
|
503
|
+
* just the post-resume portion. When the resumed call suspends
|
|
504
|
+
* again, the new state's iterations + usage also get the carry-
|
|
505
|
+
* forward so apps see a running total across an arbitrary number
|
|
506
|
+
* of suspension cycles.
|
|
507
|
+
*/
|
|
508
|
+
function mergeResumeCounters(
|
|
509
|
+
out: AgentResult | SuspendedRun,
|
|
510
|
+
state: SuspendedState,
|
|
511
|
+
): AgentResult | SuspendedRun {
|
|
512
|
+
// +1 accounts for the suspended round itself — at suspension time
|
|
513
|
+
// the loop hadn't yet incremented `iterations` (we paused mid-
|
|
514
|
+
// batch, before tool execution). Supplying results to resume
|
|
515
|
+
// effectively completes that round.
|
|
516
|
+
const carryIter = state.iterations + 1
|
|
517
|
+
if ('status' in out) {
|
|
518
|
+
return {
|
|
519
|
+
...out,
|
|
520
|
+
state: {
|
|
521
|
+
...out.state,
|
|
522
|
+
iterations: out.state.iterations + carryIter,
|
|
523
|
+
usage: addUsage(out.state.usage, state.usage),
|
|
524
|
+
},
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
...out,
|
|
529
|
+
iterations: out.iterations + carryIter,
|
|
530
|
+
usage: addUsage(out.usage, state.usage),
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function addUsage(
|
|
535
|
+
a: SuspendedState['usage'],
|
|
536
|
+
b: SuspendedState['usage'],
|
|
537
|
+
): SuspendedState['usage'] {
|
|
538
|
+
return {
|
|
539
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
540
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
541
|
+
cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
|
|
542
|
+
cacheCreationTokens: a.cacheCreationTokens + b.cacheCreationTokens,
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function rejectShouldSuspend(options: RunWithToolsOptions, entry: string): void {
|
|
547
|
+
if (options.shouldSuspend !== undefined) {
|
|
548
|
+
throw new BrainError(
|
|
549
|
+
`BrainManager.${entry}: \`shouldSuspend\` is only supported on \`runTools\` (the non-streaming + no-schema entrypoint) in V1. Run tools first with suspension, then call \`generate\` for the structured summary as a separate step.`,
|
|
550
|
+
{ context: { entry } },
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
}
|
package/src/brain_provider.ts
CHANGED
|
@@ -25,10 +25,18 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { type Application, ConfigError, ConfigRepository, ServiceProvider } from '@strav/kernel'
|
|
28
|
-
import { BrainManager } from './brain_manager.ts'
|
|
29
28
|
import type { BrainConfigShape, ProviderConfig } from './brain_config.ts'
|
|
30
|
-
import {
|
|
31
|
-
import
|
|
29
|
+
import type { BrainDriver } from './brain_driver.ts'
|
|
30
|
+
import { BrainManager } from './brain_manager.ts'
|
|
31
|
+
import { AnthropicBrainDriver } from './drivers/anthropic/anthropic_brain_driver.ts'
|
|
32
|
+
import { DeepSeekBrainDriver } from './drivers/deepseek/deepseek_brain_driver.ts'
|
|
33
|
+
import { GeminiBrainDriver } from './drivers/gemini/gemini_brain_driver.ts'
|
|
34
|
+
import { MiniMaxBrainDriver } from './drivers/minimax/minimax_brain_driver.ts'
|
|
35
|
+
import { OllamaBrainDriver } from './drivers/ollama/ollama_brain_driver.ts'
|
|
36
|
+
import { OpenAIBrainDriver } from './drivers/openai/openai_brain_driver.ts'
|
|
37
|
+
import { OpenAIResponsesBrainDriver } from './drivers/openai_responses/openai_responses_brain_driver.ts'
|
|
38
|
+
import { OpenRouterBrainDriver } from './drivers/openrouter/openrouter_brain_driver.ts'
|
|
39
|
+
import { QwenBrainDriver } from './drivers/qwen/qwen_brain_driver.ts'
|
|
32
40
|
|
|
33
41
|
export class BrainProvider extends ServiceProvider {
|
|
34
42
|
override readonly name = 'brain'
|
|
@@ -53,9 +61,9 @@ export class BrainProvider extends ServiceProvider {
|
|
|
53
61
|
)
|
|
54
62
|
}
|
|
55
63
|
|
|
56
|
-
const providers: Record<string,
|
|
64
|
+
const providers: Record<string, BrainDriver> = {}
|
|
57
65
|
for (const [name, entry] of Object.entries(config.providers)) {
|
|
58
|
-
providers[name] =
|
|
66
|
+
providers[name] = buildBrainDriver(name, entry)
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
const options: ConstructorParameters<typeof BrainManager>[0] = {
|
|
@@ -64,6 +72,7 @@ export class BrainProvider extends ServiceProvider {
|
|
|
64
72
|
}
|
|
65
73
|
if (config.tiers !== undefined) options.tiers = config.tiers
|
|
66
74
|
if (config.cache?.auto !== undefined) options.defaultCache = config.cache.auto
|
|
75
|
+
if (config.mcpServers !== undefined) options.defaultMcpServers = config.mcpServers
|
|
67
76
|
const manager = new BrainManager(options)
|
|
68
77
|
// Plug in the container so `brain.agent(MyAgent)` resolves
|
|
69
78
|
// its constructor deps through `@inject()` like every other
|
|
@@ -83,7 +92,7 @@ export class BrainProvider extends ServiceProvider {
|
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
94
|
|
|
86
|
-
function
|
|
95
|
+
function buildBrainDriver(name: string, config: ProviderConfig): BrainDriver {
|
|
87
96
|
switch (config.driver) {
|
|
88
97
|
case 'anthropic':
|
|
89
98
|
if (!config.apiKey) {
|
|
@@ -91,10 +100,71 @@ function buildProvider(name: string, config: ProviderConfig): Provider {
|
|
|
91
100
|
`BrainProvider: anthropic provider "${name}" is missing apiKey. Source from env('ANTHROPIC_API_KEY').`,
|
|
92
101
|
)
|
|
93
102
|
}
|
|
94
|
-
return new
|
|
95
|
-
|
|
103
|
+
return new AnthropicBrainDriver(name, config)
|
|
104
|
+
case 'openai':
|
|
105
|
+
if (!config.apiKey) {
|
|
106
|
+
throw new ConfigError(
|
|
107
|
+
`BrainProvider: openai provider "${name}" is missing apiKey. Source from env('OPENAI_API_KEY').`,
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
return new OpenAIBrainDriver(name, config)
|
|
111
|
+
case 'openai-responses':
|
|
112
|
+
if (!config.apiKey) {
|
|
113
|
+
throw new ConfigError(
|
|
114
|
+
`BrainProvider: openai-responses provider "${name}" is missing apiKey. Source from env('OPENAI_API_KEY').`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
return new OpenAIResponsesBrainDriver(name, config)
|
|
118
|
+
case 'google':
|
|
119
|
+
if (!config.apiKey) {
|
|
120
|
+
throw new ConfigError(
|
|
121
|
+
`BrainProvider: google provider "${name}" is missing apiKey. Source from env('GOOGLE_API_KEY').`,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
return new GeminiBrainDriver(name, config)
|
|
125
|
+
case 'deepseek':
|
|
126
|
+
if (!config.apiKey) {
|
|
127
|
+
throw new ConfigError(
|
|
128
|
+
`BrainProvider: deepseek provider "${name}" is missing apiKey. Source from env('DEEPSEEK_API_KEY').`,
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
return new DeepSeekBrainDriver(name, config)
|
|
132
|
+
case 'ollama':
|
|
133
|
+
if (!config.defaultModel) {
|
|
134
|
+
throw new ConfigError(
|
|
135
|
+
`BrainProvider: ollama provider "${name}" is missing defaultModel. Ollama models are user-installed — pick one you've pulled (e.g. 'llama3.2').`,
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
return new OllamaBrainDriver(name, config)
|
|
139
|
+
case 'qwen':
|
|
140
|
+
if (!config.apiKey) {
|
|
141
|
+
throw new ConfigError(
|
|
142
|
+
`BrainProvider: qwen provider "${name}" is missing apiKey. Source from env('DASHSCOPE_API_KEY') or env('QWEN_API_KEY').`,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
return new QwenBrainDriver(name, config)
|
|
146
|
+
case 'minimax':
|
|
147
|
+
if (!config.apiKey) {
|
|
148
|
+
throw new ConfigError(
|
|
149
|
+
`BrainProvider: minimax provider "${name}" is missing apiKey. Source from env('MINIMAX_API_KEY').`,
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
return new MiniMaxBrainDriver(name, config)
|
|
153
|
+
case 'openrouter':
|
|
154
|
+
if (!config.apiKey) {
|
|
155
|
+
throw new ConfigError(
|
|
156
|
+
`BrainProvider: openrouter provider "${name}" is missing apiKey. Source from env('OPENROUTER_API_KEY').`,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
return new OpenRouterBrainDriver(name, config)
|
|
160
|
+
default: {
|
|
161
|
+
const exhaustiveCheck: never = config
|
|
96
162
|
throw new ConfigError(
|
|
97
|
-
`BrainProvider: unknown driver for provider "${name}". Known drivers: anthropic.`,
|
|
163
|
+
`BrainProvider: unknown driver for provider "${name}". Known drivers: anthropic, openai, openai-responses, google, deepseek, ollama, qwen, minimax, openrouter.`,
|
|
98
164
|
)
|
|
165
|
+
// (unreachable — kept for the exhaustive check to fire when a new driver lands)
|
|
166
|
+
// biome-ignore lint/correctness/noUnreachable: kept for the exhaustive-check above
|
|
167
|
+
return exhaustiveCheck
|
|
168
|
+
}
|
|
99
169
|
}
|
|
100
170
|
}
|