@strav/brain 1.0.0-alpha.16 → 1.0.0-alpha.17
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 +2 -2
- package/src/agent.ts +34 -5
- package/src/agent_runner.ts +54 -11
- package/src/agent_stream_event.ts +100 -0
- package/src/brain_config.ts +91 -1
- package/src/brain_manager.ts +168 -4
- package/src/brain_provider.ts +25 -1
- package/src/index.ts +17 -0
- package/src/mcp/client.ts +82 -13
- package/src/mcp/index.ts +6 -0
- package/src/mcp/oauth.ts +227 -0
- package/src/mcp/resolve_mcp_tools.ts +6 -2
- package/src/mcp_server.ts +16 -0
- package/src/provider.ts +109 -0
- package/src/providers/anthropic_provider.ts +596 -28
- package/src/providers/deepseek_provider.ts +117 -0
- package/src/providers/gemini_provider.ts +590 -21
- package/src/providers/ollama_provider.ts +86 -0
- package/src/providers/openai_compat_provider.ts +187 -0
- package/src/providers/openai_provider.ts +735 -32
- package/src/providers/openai_responses_provider.ts +700 -0
- package/src/tool.ts +7 -0
- package/src/tool_runner.ts +81 -0
- package/src/types.ts +233 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strav/brain",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.17",
|
|
4
4
|
"description": "Strav AI module — unified Provider interface, BrainManager, threads, prompt caching, tools / agents / MCP. Anthropic + OpenAI providers; Gemini / DeepSeek follow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@anthropic-ai/sdk": "^0.100.0",
|
|
25
25
|
"@google/genai": "^2.7.0",
|
|
26
26
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
27
|
-
"@strav/kernel": "1.0.0-alpha.
|
|
27
|
+
"@strav/kernel": "1.0.0-alpha.17",
|
|
28
28
|
"openai": "^6.0.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
package/src/agent.ts
CHANGED
|
@@ -21,17 +21,30 @@
|
|
|
21
21
|
* .run()
|
|
22
22
|
* ```
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* Structured output (typed result without `.output(schema)` at the call site):
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* class CityAgent extends Agent<CityAnswer> {
|
|
28
|
+
* override readonly instructions = 'You only emit verified city data.'
|
|
29
|
+
* override readonly outputSchema = citySchema // OutputSchema<CityAnswer>
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* const { value } = await brain.agent(CityAgent).input('Capital of France?').run()
|
|
33
|
+
* // ^? CityAnswer — runner is typed from the class generic
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* The generic threads `T` through `BrainManager.agent(Class)` →
|
|
37
|
+
* `AgentRunner<T>` → `AgentGenerateResult<T>`. Subclasses that
|
|
38
|
+
* don't declare an output type stay `Agent<never>` and `run()`
|
|
39
|
+
* returns `AgentResult` exactly as before.
|
|
28
40
|
*/
|
|
29
41
|
|
|
30
42
|
import type { MCPServer } from './mcp_server.ts'
|
|
43
|
+
import type { OutputSchema } from './output_schema.ts'
|
|
31
44
|
import type { ModelTier } from './types.ts'
|
|
32
45
|
import type { Tool } from './tool.ts'
|
|
33
46
|
|
|
34
|
-
export abstract class Agent {
|
|
47
|
+
export abstract class Agent<T = never> {
|
|
35
48
|
/** System prompt — the persona / instructions Claude sees on every turn. */
|
|
36
49
|
abstract readonly instructions: string
|
|
37
50
|
|
|
@@ -65,4 +78,20 @@ export abstract class Agent {
|
|
|
65
78
|
|
|
66
79
|
/** Hard cap on per-call response tokens. Default `4096`. */
|
|
67
80
|
readonly maxTokens: number = 4096
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Structured-output schema. Set on subclasses that extend
|
|
84
|
+
* `Agent<SomeType>` to declare the agent always returns that
|
|
85
|
+
* shape; `BrainManager.agent(Class)` then types the runner
|
|
86
|
+
* automatically so `.run()` returns `AgentGenerateResult<T>`
|
|
87
|
+
* without a per-call `.output(schema)`.
|
|
88
|
+
*
|
|
89
|
+
* Leave unset on `Agent<never>` subclasses (no structured
|
|
90
|
+
* output). The runner falls back to the standard tool-loop path
|
|
91
|
+
* and returns `AgentResult`.
|
|
92
|
+
*
|
|
93
|
+
* Apps that want a per-call override still chain
|
|
94
|
+
* `.output(otherSchema)` — that wins over the class-side value.
|
|
95
|
+
*/
|
|
96
|
+
readonly outputSchema?: OutputSchema<T>
|
|
68
97
|
}
|
package/src/agent_runner.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import type { Agent } from './agent.ts'
|
|
23
23
|
import type { AgentGenerateResult } from './agent_generate_result.ts'
|
|
24
24
|
import type { AgentResult } from './agent_result.ts'
|
|
25
|
+
import type { AgentStreamEvent } from './agent_stream_event.ts'
|
|
25
26
|
import type { BrainManager } from './brain_manager.ts'
|
|
26
27
|
import { BrainError } from './brain_error.ts'
|
|
27
28
|
import type { OutputSchema } from './output_schema.ts'
|
|
@@ -48,7 +49,7 @@ export class AgentRunner<T = never> {
|
|
|
48
49
|
|
|
49
50
|
constructor(
|
|
50
51
|
private readonly brain: BrainManager,
|
|
51
|
-
private readonly agent: Agent
|
|
52
|
+
private readonly agent: Agent<unknown>,
|
|
52
53
|
) {}
|
|
53
54
|
|
|
54
55
|
/** Set the user input. Required before `run()`. */
|
|
@@ -88,6 +89,44 @@ export class AgentRunner<T = never> {
|
|
|
88
89
|
return this as unknown as AgentRunner<U>
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Streaming variant of `run()`. Returns an
|
|
94
|
+
* `AsyncIterable<AgentStreamEvent<T>>` — yields text deltas,
|
|
95
|
+
* tool-use/result boundaries, and a terminal `stop` event with
|
|
96
|
+
* the full trace.
|
|
97
|
+
*
|
|
98
|
+
* Default (no `.output(schema)` set): the terminal `stop` has the
|
|
99
|
+
* plain shape and `T` defaults to `never`.
|
|
100
|
+
*
|
|
101
|
+
* With `.output(schema)`: the terminal `stop` event carries the
|
|
102
|
+
* parsed `value: T` + raw `text` alongside the loop bookkeeping,
|
|
103
|
+
* and the runner delegates to
|
|
104
|
+
* `BrainManager.streamGenerateWithTools`.
|
|
105
|
+
*/
|
|
106
|
+
stream(): AsyncIterable<AgentStreamEvent<T>> {
|
|
107
|
+
if (this.prompt === undefined) {
|
|
108
|
+
throw new BrainError('AgentRunner.stream: input() must be called before stream().')
|
|
109
|
+
}
|
|
110
|
+
const messages: Message[] = [{ role: 'user', content: this.prompt }]
|
|
111
|
+
const options: RunWithToolsOptions = {
|
|
112
|
+
...this.buildChatOptions(),
|
|
113
|
+
maxIterations: this.agent.maxIterations,
|
|
114
|
+
context: this.contextBag,
|
|
115
|
+
}
|
|
116
|
+
if (this.agent.mcpServers.length > 0) options.mcpServers = this.agent.mcpServers
|
|
117
|
+
if (this.schema !== undefined) {
|
|
118
|
+
return this.brain.streamGenerateWithTools<T>(
|
|
119
|
+
messages,
|
|
120
|
+
this.schema,
|
|
121
|
+
this.agent.tools,
|
|
122
|
+
options,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
return this.brain.streamTools(messages, this.agent.tools, options) as AsyncIterable<
|
|
126
|
+
AgentStreamEvent<T>
|
|
127
|
+
>
|
|
128
|
+
}
|
|
129
|
+
|
|
91
130
|
async run(): Promise<AgentRunResult<T>> {
|
|
92
131
|
if (this.prompt === undefined) {
|
|
93
132
|
throw new BrainError('AgentRunner.run: input() must be called before run().')
|
|
@@ -95,17 +134,21 @@ export class AgentRunner<T = never> {
|
|
|
95
134
|
const messages: Message[] = [{ role: 'user', content: this.prompt }]
|
|
96
135
|
|
|
97
136
|
if (this.schema !== undefined) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
137
|
+
const hasTools = this.agent.tools.length > 0 || this.agent.mcpServers.length > 0
|
|
138
|
+
if (hasTools) {
|
|
139
|
+
const toolOptions: RunWithToolsOptions = {
|
|
140
|
+
...this.buildChatOptions(),
|
|
141
|
+
maxIterations: this.agent.maxIterations,
|
|
142
|
+
context: this.contextBag,
|
|
143
|
+
}
|
|
144
|
+
if (this.agent.mcpServers.length > 0) toolOptions.mcpServers = this.agent.mcpServers
|
|
145
|
+
const result = await this.brain.generateWithTools<T>(
|
|
146
|
+
messages,
|
|
147
|
+
this.schema,
|
|
148
|
+
this.agent.tools,
|
|
149
|
+
toolOptions,
|
|
108
150
|
)
|
|
151
|
+
return result as AgentRunResult<T>
|
|
109
152
|
}
|
|
110
153
|
const generateOptions = this.buildChatOptions()
|
|
111
154
|
const result = await this.brain.generate<T>(messages, this.schema, generateOptions)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `AgentStreamEvent<T>` — the union yielded by streaming agentic
|
|
3
|
+
* runs (`Provider.streamWithTools` / `BrainManager.streamTools` /
|
|
4
|
+
* `AgentRunner.stream`).
|
|
5
|
+
*
|
|
6
|
+
* The generic `T` carries the structured-output type when the run
|
|
7
|
+
* was schema-constrained:
|
|
8
|
+
* - With the default `T = never`, the terminal `stop` event is the
|
|
9
|
+
* plain shape: `{ stopReason, iterations, usage, messages }`.
|
|
10
|
+
* - With `T` set (via `BrainManager.streamGenerateWithTools` or
|
|
11
|
+
* `AgentRunner.stream()` after `.output(schema)`), the `stop`
|
|
12
|
+
* event additionally carries `{ value: T; text: string }` — the
|
|
13
|
+
* parsed JSON shaped to the schema and the raw text the model
|
|
14
|
+
* produced.
|
|
15
|
+
*
|
|
16
|
+
* Event vocabulary:
|
|
17
|
+
*
|
|
18
|
+
* - `iteration_start` — fired before each model call. `iteration`
|
|
19
|
+
* starts at `0` and increments per round.
|
|
20
|
+
* - `text` — a text delta from the assistant turn currently in
|
|
21
|
+
* flight. Same shape as `StreamEvent.text` so apps can reuse
|
|
22
|
+
* UI code.
|
|
23
|
+
* - `tool_use_start` — a tool call has begun streaming. Fires
|
|
24
|
+
* as soon as the model emits the call's `id` + `name`,
|
|
25
|
+
* before the arguments finish streaming. Apps that render
|
|
26
|
+
* "(calling X with …)" indicators show the tool name here.
|
|
27
|
+
* Anthropic + OpenAI emit this from streaming chunks; Gemini
|
|
28
|
+
* doesn't stream tool arguments (parts arrive complete) and
|
|
29
|
+
* skips both `tool_use_start` and `tool_use_delta`.
|
|
30
|
+
* - `tool_use_delta` — a chunk of the tool-call's argument JSON.
|
|
31
|
+
* Apps that render the model composing the call (e.g. typing
|
|
32
|
+
* `search(q='current state of bun.sql ...`) accumulate
|
|
33
|
+
* `argsDelta` per `id` and re-render. The argument JSON is
|
|
34
|
+
* partial / possibly malformed mid-stream — only the final
|
|
35
|
+
* `tool_use` event carries the parsed input.
|
|
36
|
+
* - `tool_use` — the assistant turn finished and the framework
|
|
37
|
+
* parsed a tool call. Emitted with the parsed `input` before
|
|
38
|
+
* the framework runs the tool. Source-of-truth for tool calls;
|
|
39
|
+
* cross-provider consumers can rely on this even when the
|
|
40
|
+
* start/delta events aren't fired.
|
|
41
|
+
* - `tool_result` — the framework executed the tool and is about
|
|
42
|
+
* to feed the result back to the model. `isError` reflects
|
|
43
|
+
* whether the tool reported a failure (V1: only false today —
|
|
44
|
+
* thrown executions abort the loop with `ToolExecutionError`;
|
|
45
|
+
* graceful tool-error recovery is a later slice).
|
|
46
|
+
* - `iteration_end` — the assistant turn fully drained, including
|
|
47
|
+
* its `stopReason`. Apps that render per-iteration boundaries
|
|
48
|
+
* use this.
|
|
49
|
+
* - `stop` — terminal event. Carries the full `messages` trace,
|
|
50
|
+
* iteration count, aggregated usage, and the framework
|
|
51
|
+
* stop reason (`'end_turn'`, `'max_iterations'`, etc.). When the
|
|
52
|
+
* run was schema-constrained, also carries `value` (parsed JSON)
|
|
53
|
+
* and `text` (the raw JSON the model emitted).
|
|
54
|
+
*
|
|
55
|
+
* What's NOT in V1:
|
|
56
|
+
* - `error` events. Failures throw out of the iterator (the
|
|
57
|
+
* consumer's `for await` rejects). Apps that want resilient
|
|
58
|
+
* loops catch around the consumer.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
import type { ChatUsage, Message } from './types.ts'
|
|
62
|
+
|
|
63
|
+
interface BaseStopEvent {
|
|
64
|
+
type: 'stop'
|
|
65
|
+
stopReason: string
|
|
66
|
+
iterations: number
|
|
67
|
+
usage: ChatUsage
|
|
68
|
+
messages: Message[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface ValueStopEvent<T> extends BaseStopEvent {
|
|
72
|
+
/** Parsed JSON shaped to the supplied `OutputSchema<T>`. */
|
|
73
|
+
value: T
|
|
74
|
+
/** Raw JSON text the model emitted on its terminal turn. */
|
|
75
|
+
text: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The `stop` variant narrows when the stream was schema-constrained
|
|
80
|
+
* — the `[T] extends [never]` form is the standard "is this still
|
|
81
|
+
* the default never?" check (a bare `T extends never` would
|
|
82
|
+
* distribute over union types and break).
|
|
83
|
+
*/
|
|
84
|
+
type StopEvent<T> = [T] extends [never] ? BaseStopEvent : ValueStopEvent<T>
|
|
85
|
+
|
|
86
|
+
export type AgentStreamEvent<T = never> =
|
|
87
|
+
| { type: 'iteration_start'; iteration: number }
|
|
88
|
+
| { type: 'text'; delta: string }
|
|
89
|
+
| { type: 'tool_use_start'; id: string; name: string }
|
|
90
|
+
| { type: 'tool_use_delta'; id: string; argsDelta: string }
|
|
91
|
+
| { type: 'tool_use'; id: string; name: string; input: unknown }
|
|
92
|
+
| {
|
|
93
|
+
type: 'tool_result'
|
|
94
|
+
id: string
|
|
95
|
+
name: string
|
|
96
|
+
content: string
|
|
97
|
+
isError: boolean
|
|
98
|
+
}
|
|
99
|
+
| { type: 'iteration_end'; iteration: number; stopReason: string | null }
|
|
100
|
+
| StopEvent<T>
|
package/src/brain_config.ts
CHANGED
|
@@ -34,6 +34,31 @@ export interface AnthropicProviderConfig {
|
|
|
34
34
|
betas?: readonly string[]
|
|
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
|
+
|
|
37
62
|
/** OpenAI-specific driver config. */
|
|
38
63
|
export interface OpenAIProviderConfig {
|
|
39
64
|
driver: 'openai'
|
|
@@ -47,6 +72,10 @@ export interface OpenAIProviderConfig {
|
|
|
47
72
|
defaultModel?: string
|
|
48
73
|
/** Default `max_tokens` for `chat()` calls that don't specify one. */
|
|
49
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
|
|
50
79
|
}
|
|
51
80
|
|
|
52
81
|
/** Google (Gemini) driver config — backed by `@google/genai`. */
|
|
@@ -62,12 +91,73 @@ export interface GeminiProviderConfig {
|
|
|
62
91
|
defaultMaxTokens?: number
|
|
63
92
|
/** Optional API version pin (`v1` / `v1beta`). */
|
|
64
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
|
|
65
152
|
}
|
|
66
153
|
|
|
67
154
|
export type ProviderConfig =
|
|
68
155
|
| AnthropicProviderConfig
|
|
69
156
|
| OpenAIProviderConfig
|
|
70
|
-
|
|
|
157
|
+
| OpenAIResponsesProviderConfig
|
|
158
|
+
| GeminiProviderConfig
|
|
159
|
+
| DeepSeekProviderConfig
|
|
160
|
+
| OllamaProviderConfig
|
|
71
161
|
|
|
72
162
|
/** Cache-shape defaults applied when `ChatOptions.cache` is omitted. */
|
|
73
163
|
export interface BrainCacheConfig {
|
package/src/brain_manager.ts
CHANGED
|
@@ -21,24 +21,31 @@
|
|
|
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'
|
|
24
25
|
import type { MCPServer } from './mcp_server.ts'
|
|
26
|
+
import type { AgentGenerateResult } from './agent_generate_result.ts'
|
|
25
27
|
import type { OutputSchema } from './output_schema.ts'
|
|
26
28
|
import { AgentRunner } from './agent_runner.ts'
|
|
27
29
|
import { BrainError } from './brain_error.ts'
|
|
28
30
|
import type { ModelTier } from './types.ts'
|
|
29
31
|
import type {
|
|
32
|
+
AudioSource,
|
|
30
33
|
ChatOptions,
|
|
31
34
|
ChatResult,
|
|
35
|
+
EmbedOptions,
|
|
36
|
+
EmbedResult,
|
|
32
37
|
GenerateResult,
|
|
33
38
|
Message,
|
|
34
39
|
StreamEvent,
|
|
40
|
+
TranscribeOptions,
|
|
41
|
+
TranscribeResult,
|
|
35
42
|
} from './types.ts'
|
|
36
43
|
import type { Provider, RunWithToolsOptions } from './provider.ts'
|
|
37
44
|
import type { Tool } from './tool.ts'
|
|
38
45
|
import { DEFAULT_TIERS } from './brain_config.ts'
|
|
39
46
|
|
|
40
47
|
/** Container-aware Agent constructor resolver — `BrainProvider` installs one wired to `app.resolve(...)`. */
|
|
41
|
-
export type AgentResolver = <A extends Agent
|
|
48
|
+
export type AgentResolver = <A extends Agent<unknown>>(cls: new (...args: never[]) => A) => A
|
|
42
49
|
|
|
43
50
|
export interface BrainManagerOptions {
|
|
44
51
|
/** Name of the default provider — must exist in `providers`. */
|
|
@@ -168,6 +175,96 @@ export class BrainManager {
|
|
|
168
175
|
return provider.runWithTools(messages, tools, resolved)
|
|
169
176
|
}
|
|
170
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Streaming variant of `generateWithTools`. Yields
|
|
180
|
+
* `AgentStreamEvent<T>`s as the loop progresses; the terminal
|
|
181
|
+
* `stop` event carries the parsed value + raw JSON text. Throws
|
|
182
|
+
* `BrainError` when the provider lacks
|
|
183
|
+
* `streamWithToolsAndSchema` (V1: all three providers
|
|
184
|
+
* implement it).
|
|
185
|
+
*/
|
|
186
|
+
streamGenerateWithTools<T>(
|
|
187
|
+
input: string | readonly Message[],
|
|
188
|
+
schema: OutputSchema<T>,
|
|
189
|
+
tools: readonly Tool[],
|
|
190
|
+
options: RunWithToolsOptions = {},
|
|
191
|
+
): AsyncIterable<AgentStreamEvent<T>> {
|
|
192
|
+
const provider = this.provider(options.provider)
|
|
193
|
+
if (!provider.streamWithToolsAndSchema) {
|
|
194
|
+
throw new BrainError(
|
|
195
|
+
`BrainManager.streamGenerateWithTools: provider "${provider.name}" does not implement streamWithToolsAndSchema.`,
|
|
196
|
+
{ context: { provider: provider.name } },
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
const messages = normalizeInput(input)
|
|
200
|
+
const resolved = this.applyDefaults(options) as RunWithToolsOptions
|
|
201
|
+
if (resolved.mcpServers === undefined && this.defaultMcpServers.length > 0) {
|
|
202
|
+
resolved.mcpServers = this.defaultMcpServers
|
|
203
|
+
}
|
|
204
|
+
return provider.streamWithToolsAndSchema<T>(messages, tools, schema, resolved)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Tool-loop + structured output combined. Runs the agentic loop
|
|
209
|
+
* with the supplied `tools` while pinning the output to `schema`
|
|
210
|
+
* on every turn; returns the parsed value when the model finally
|
|
211
|
+
* answers without calling a tool. MCP defaults + tier resolution
|
|
212
|
+
* + provider routing match `runTools` / `generate`.
|
|
213
|
+
*
|
|
214
|
+
* Throws `BrainError` when the chosen provider doesn't implement
|
|
215
|
+
* `runWithToolsAndSchema`. V1: all three providers do.
|
|
216
|
+
*/
|
|
217
|
+
async generateWithTools<T>(
|
|
218
|
+
input: string | readonly Message[],
|
|
219
|
+
schema: OutputSchema<T>,
|
|
220
|
+
tools: readonly Tool[],
|
|
221
|
+
options: RunWithToolsOptions = {},
|
|
222
|
+
): Promise<AgentGenerateResult<T>> {
|
|
223
|
+
const provider = this.provider(options.provider)
|
|
224
|
+
if (!provider.runWithToolsAndSchema) {
|
|
225
|
+
throw new BrainError(
|
|
226
|
+
`BrainManager.generateWithTools: provider "${provider.name}" does not implement runWithToolsAndSchema.`,
|
|
227
|
+
{ context: { provider: provider.name } },
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
const messages = normalizeInput(input)
|
|
231
|
+
const resolved = this.applyDefaults(options) as RunWithToolsOptions
|
|
232
|
+
if (resolved.mcpServers === undefined && this.defaultMcpServers.length > 0) {
|
|
233
|
+
resolved.mcpServers = this.defaultMcpServers
|
|
234
|
+
}
|
|
235
|
+
return provider.runWithToolsAndSchema<T>(messages, tools, schema, resolved)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Streaming variant of `runTools`. Yields `AgentStreamEvent`s
|
|
240
|
+
* as the agentic loop progresses — text deltas during model
|
|
241
|
+
* turns, `tool_use` / `tool_result` boundaries around tool
|
|
242
|
+
* execution, `iteration_start` / `iteration_end` per round, a
|
|
243
|
+
* terminal `stop` with the full trace + usage.
|
|
244
|
+
*
|
|
245
|
+
* Throws `BrainError` when the configured provider doesn't
|
|
246
|
+
* implement `streamWithTools`.
|
|
247
|
+
*/
|
|
248
|
+
streamTools(
|
|
249
|
+
input: string | readonly Message[],
|
|
250
|
+
tools: readonly Tool[],
|
|
251
|
+
options: RunWithToolsOptions = {},
|
|
252
|
+
): AsyncIterable<AgentStreamEvent> {
|
|
253
|
+
const provider = this.provider(options.provider)
|
|
254
|
+
if (!provider.streamWithTools) {
|
|
255
|
+
throw new BrainError(
|
|
256
|
+
`BrainManager.streamTools: provider "${provider.name}" does not implement streamWithTools.`,
|
|
257
|
+
{ context: { provider: provider.name } },
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
const messages = normalizeInput(input)
|
|
261
|
+
const resolved = this.applyDefaults(options) as RunWithToolsOptions
|
|
262
|
+
if (resolved.mcpServers === undefined && this.defaultMcpServers.length > 0) {
|
|
263
|
+
resolved.mcpServers = this.defaultMcpServers
|
|
264
|
+
}
|
|
265
|
+
return provider.streamWithTools(messages, tools, resolved)
|
|
266
|
+
}
|
|
267
|
+
|
|
171
268
|
/**
|
|
172
269
|
* Structured output. Sends `input` to the configured (or
|
|
173
270
|
* `options.provider`-overridden) provider with the JSON-Schema
|
|
@@ -194,21 +291,88 @@ export class BrainManager {
|
|
|
194
291
|
return provider.generate<T>(messages, schema, resolved)
|
|
195
292
|
}
|
|
196
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Turn one or more text inputs into embedding vectors. Accepts
|
|
296
|
+
* either a single string (returns one vector) or an array
|
|
297
|
+
* (batch — returns one vector per input in the same order).
|
|
298
|
+
*
|
|
299
|
+
* Throws `BrainError` when the configured (or
|
|
300
|
+
* `options.provider`-overridden) provider doesn't implement
|
|
301
|
+
* `embed`. V1: OpenAI, Gemini, Ollama support it; Anthropic +
|
|
302
|
+
* DeepSeek throw with a clear "route to a different provider"
|
|
303
|
+
* message.
|
|
304
|
+
*/
|
|
305
|
+
async embed(
|
|
306
|
+
input: string | readonly string[],
|
|
307
|
+
options: EmbedOptions = {},
|
|
308
|
+
): Promise<EmbedResult> {
|
|
309
|
+
const provider = this.provider(options.provider)
|
|
310
|
+
if (!provider.embed) {
|
|
311
|
+
throw new BrainError(
|
|
312
|
+
`BrainManager.embed: provider "${provider.name}" does not implement embed. Route to a provider with an embeddings API (V1: OpenAI / Gemini / Ollama).`,
|
|
313
|
+
{ context: { provider: provider.name } },
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
const texts = typeof input === 'string' ? [input] : input
|
|
317
|
+
return provider.embed(texts, options)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Transcribe one audio clip to text. Complements `AudioBlock`
|
|
322
|
+
* (which sends audio + a text prompt together to a multimodal
|
|
323
|
+
* chat model) by exposing the dedicated transcription endpoint
|
|
324
|
+
* where the provider has one. Apps that already have an
|
|
325
|
+
* `AudioBlock` can pass its `source` directly.
|
|
326
|
+
*
|
|
327
|
+
* Throws `BrainError` when the configured (or
|
|
328
|
+
* `options.provider`-overridden) provider doesn't implement
|
|
329
|
+
* `transcribe`. V1: OpenAI / Ollama (Whisper / gpt-4o-transcribe
|
|
330
|
+
* / local) and Gemini (chat-wrap fallback); Anthropic +
|
|
331
|
+
* DeepSeek throw.
|
|
332
|
+
*/
|
|
333
|
+
async transcribe(
|
|
334
|
+
audio: AudioSource,
|
|
335
|
+
options: TranscribeOptions = {},
|
|
336
|
+
): Promise<TranscribeResult> {
|
|
337
|
+
const provider = this.provider(options.provider)
|
|
338
|
+
if (!provider.transcribe) {
|
|
339
|
+
throw new BrainError(
|
|
340
|
+
`BrainManager.transcribe: provider "${provider.name}" does not implement transcribe. Route to a provider with audio support (V1: OpenAI / Ollama / Gemini).`,
|
|
341
|
+
{ context: { provider: provider.name } },
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
return provider.transcribe(audio, options)
|
|
345
|
+
}
|
|
346
|
+
|
|
197
347
|
/**
|
|
198
348
|
* Resolve an `Agent` subclass from the container and return an
|
|
199
349
|
* `AgentRunner` ready to receive `input(...)` and `run()`. Apps
|
|
200
350
|
* `@inject()`-decorate their Agent subclass so constructor
|
|
201
351
|
* injection of dependencies (Repositories, services, etc.) flows
|
|
202
352
|
* through normally.
|
|
353
|
+
*
|
|
354
|
+
* When the agent subclass extends `Agent<T>` for some `T` and
|
|
355
|
+
* declares `outputSchema`, the returned runner is typed as
|
|
356
|
+
* `AgentRunner<T>` and the schema is pre-applied — `.run()`
|
|
357
|
+
* returns `AgentGenerateResult<T>` without a per-call
|
|
358
|
+
* `.output(schema)`. Apps can still chain `.output(otherSchema)`
|
|
359
|
+
* to override.
|
|
203
360
|
*/
|
|
204
|
-
agent<
|
|
361
|
+
agent<T = never>(
|
|
362
|
+
AgentClass: new (...args: never[]) => Agent<T>,
|
|
363
|
+
instance?: Agent<T>,
|
|
364
|
+
): AgentRunner<T> {
|
|
205
365
|
const agent = instance ?? this.resolveAgent(AgentClass)
|
|
206
|
-
|
|
366
|
+
const runner = new AgentRunner<T>(this, agent)
|
|
367
|
+
if (agent.outputSchema !== undefined) {
|
|
368
|
+
return runner.output(agent.outputSchema)
|
|
369
|
+
}
|
|
370
|
+
return runner
|
|
207
371
|
}
|
|
208
372
|
|
|
209
373
|
// ─── Internal ────────────────────────────────────────────────────────────
|
|
210
374
|
|
|
211
|
-
private resolveAgent<A extends Agent
|
|
375
|
+
private resolveAgent<A extends Agent<unknown>>(AgentClass: new (...args: never[]) => A): A {
|
|
212
376
|
if (this.agentResolver) return this.agentResolver(AgentClass)
|
|
213
377
|
// Fallback: assume the Agent class is constructible without args.
|
|
214
378
|
// Apps that need DI on the agent register a resolver via
|
package/src/brain_provider.ts
CHANGED
|
@@ -28,8 +28,11 @@ import { type Application, ConfigError, ConfigRepository, ServiceProvider } from
|
|
|
28
28
|
import { BrainManager } from './brain_manager.ts'
|
|
29
29
|
import type { BrainConfigShape, ProviderConfig } from './brain_config.ts'
|
|
30
30
|
import { AnthropicProvider } from './providers/anthropic_provider.ts'
|
|
31
|
+
import { DeepSeekProvider } from './providers/deepseek_provider.ts'
|
|
31
32
|
import { GeminiProvider } from './providers/gemini_provider.ts'
|
|
33
|
+
import { OllamaProvider } from './providers/ollama_provider.ts'
|
|
32
34
|
import { OpenAIProvider } from './providers/openai_provider.ts'
|
|
35
|
+
import { OpenAIResponsesProvider } from './providers/openai_responses_provider.ts'
|
|
33
36
|
import type { Provider } from './provider.ts'
|
|
34
37
|
|
|
35
38
|
export class BrainProvider extends ServiceProvider {
|
|
@@ -102,6 +105,13 @@ function buildProvider(name: string, config: ProviderConfig): Provider {
|
|
|
102
105
|
)
|
|
103
106
|
}
|
|
104
107
|
return new OpenAIProvider(name, config)
|
|
108
|
+
case 'openai-responses':
|
|
109
|
+
if (!config.apiKey) {
|
|
110
|
+
throw new ConfigError(
|
|
111
|
+
`BrainProvider: openai-responses provider "${name}" is missing apiKey. Source from env('OPENAI_API_KEY').`,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
return new OpenAIResponsesProvider(name, config)
|
|
105
115
|
case 'google':
|
|
106
116
|
if (!config.apiKey) {
|
|
107
117
|
throw new ConfigError(
|
|
@@ -109,10 +119,24 @@ function buildProvider(name: string, config: ProviderConfig): Provider {
|
|
|
109
119
|
)
|
|
110
120
|
}
|
|
111
121
|
return new GeminiProvider(name, config)
|
|
122
|
+
case 'deepseek':
|
|
123
|
+
if (!config.apiKey) {
|
|
124
|
+
throw new ConfigError(
|
|
125
|
+
`BrainProvider: deepseek provider "${name}" is missing apiKey. Source from env('DEEPSEEK_API_KEY').`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
return new DeepSeekProvider(name, config)
|
|
129
|
+
case 'ollama':
|
|
130
|
+
if (!config.defaultModel) {
|
|
131
|
+
throw new ConfigError(
|
|
132
|
+
`BrainProvider: ollama provider "${name}" is missing defaultModel. Ollama models are user-installed — pick one you've pulled (e.g. 'llama3.2').`,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
return new OllamaProvider(name, config)
|
|
112
136
|
default: {
|
|
113
137
|
const exhaustiveCheck: never = config
|
|
114
138
|
throw new ConfigError(
|
|
115
|
-
`BrainProvider: unknown driver for provider "${name}". Known drivers: anthropic, openai, google.`,
|
|
139
|
+
`BrainProvider: unknown driver for provider "${name}". Known drivers: anthropic, openai, openai-responses, google, deepseek, ollama.`,
|
|
116
140
|
)
|
|
117
141
|
// (unreachable — kept for the exhaustive check to fire when a new driver lands)
|
|
118
142
|
// biome-ignore lint/correctness/noUnreachable: kept for the exhaustive-check above
|