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