@strav/brain 1.0.0-alpha.16 → 1.0.0-alpha.18
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 +4 -2
- package/src/agent.ts +34 -5
- package/src/agent_generate_result.ts +2 -0
- package/src/agent_result.ts +7 -0
- package/src/agent_runner.ts +134 -15
- package/src/agent_stream_event.ts +100 -0
- package/src/brain_config.ts +91 -1
- package/src/brain_manager.ts +287 -6
- package/src/brain_provider.ts +25 -1
- package/src/index.ts +37 -2
- package/src/mcp/client.ts +99 -13
- package/src/mcp/index.ts +7 -0
- package/src/mcp/oauth.ts +227 -0
- package/src/mcp/pool.ts +106 -0
- package/src/mcp/resolve_mcp_tools.ts +31 -9
- package/src/mcp_server.ts +16 -0
- package/src/persistence/brain_message.ts +34 -0
- package/src/persistence/brain_message_repository.ts +106 -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 +68 -0
- package/src/persistence/brain_thread.ts +30 -0
- package/src/persistence/brain_thread_repository.ts +65 -0
- package/src/persistence/database_brain_store.ts +190 -0
- package/src/persistence/index.ts +48 -0
- package/src/persistence/schema/brain_message_schema.ts +61 -0
- package/src/persistence/schema/brain_suspended_run_schema.ts +58 -0
- package/src/persistence/schema/brain_thread_schema.ts +50 -0
- package/src/persistence/schema/index.ts +3 -0
- package/src/provider.ts +145 -1
- package/src/providers/anthropic_provider.ts +723 -38
- package/src/providers/deepseek_provider.ts +117 -0
- package/src/providers/gemini_provider.ts +625 -33
- package/src/providers/ollama_provider.ts +86 -0
- package/src/providers/openai_compat_provider.ts +616 -0
- package/src/providers/openai_provider.ts +801 -43
- package/src/providers/openai_responses_provider.ts +1015 -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/types.ts +343 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `brainSuspendedRunSchema` — a paused agentic loop awaiting
|
|
3
|
+
* human-in-the-loop tool approval.
|
|
4
|
+
*
|
|
5
|
+
* Two real use cases drive the shape:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Linked to a thread** — the suspending run was part of a
|
|
8
|
+
* conversational thread; the app wants the suspended state to
|
|
9
|
+
* reference its thread so the UI can show "thread X is paused
|
|
10
|
+
* waiting on Y." `thread_id` is the FK, nullable so detached
|
|
11
|
+
* runs are fine.
|
|
12
|
+
* 2. **Standalone** — the run came from a one-shot `runTools(...)`
|
|
13
|
+
* call (cron job, queued worker, ...). No thread context;
|
|
14
|
+
* `thread_id` stays NULL.
|
|
15
|
+
*
|
|
16
|
+
* Columns:
|
|
17
|
+
*
|
|
18
|
+
* - `id` ULID primary key. The id apps reference
|
|
19
|
+
* when resuming.
|
|
20
|
+
* - `thread_id` FK → `brain_thread`, NULLABLE,
|
|
21
|
+
* `onDelete: set null` — if the thread
|
|
22
|
+
* gets deleted, the suspended run keeps
|
|
23
|
+
* its data so the human approver can
|
|
24
|
+
* still inspect it.
|
|
25
|
+
* - `user_id` App-defined approver / owner.
|
|
26
|
+
* - `pending_tool_calls` JSONB — `ToolUseBlock[]` the model
|
|
27
|
+
* wants executed. Multi-call batches are
|
|
28
|
+
* captured together (mid-batch invariant).
|
|
29
|
+
* - `state` JSONB — `SuspendedState` snapshot. The
|
|
30
|
+
* framework's `brain.resumeTools(state,
|
|
31
|
+
* ...)` takes this as its first arg.
|
|
32
|
+
* - `status` `pending | resumed | cancelled`. Apps
|
|
33
|
+
* bulk-list pending runs and walk through
|
|
34
|
+
* an approval queue.
|
|
35
|
+
* - `timestamps` `created_at` for "how long pending?"
|
|
36
|
+
* sorts, `updated_at` for transition
|
|
37
|
+
* tracking.
|
|
38
|
+
*
|
|
39
|
+
* Tenanted: standard `tenant_id` + RLS.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { Archetype, defineSchema } from '@strav/database'
|
|
43
|
+
import { brainThreadSchema } from './brain_thread_schema.ts'
|
|
44
|
+
|
|
45
|
+
export const brainSuspendedRunSchema = defineSchema(
|
|
46
|
+
'brain_suspended_run',
|
|
47
|
+
Archetype.Entity,
|
|
48
|
+
(t) => {
|
|
49
|
+
t.id()
|
|
50
|
+
t.reference('thread_id').to(brainThreadSchema).onDelete('set null').nullable()
|
|
51
|
+
t.string('user_id').max(64).nullable()
|
|
52
|
+
t.json('pending_tool_calls').notNull()
|
|
53
|
+
t.json('state').notNull()
|
|
54
|
+
t.enum('status', ['pending', 'resumed', 'cancelled']).notNull().default('pending')
|
|
55
|
+
t.timestamps()
|
|
56
|
+
},
|
|
57
|
+
{ tenanted: true },
|
|
58
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `brainThreadSchema` — one row per conversation.
|
|
3
|
+
*
|
|
4
|
+
* Carries the per-thread defaults that `Thread` already serializes
|
|
5
|
+
* (`system`, `options`, `lastResponseId`) plus a few framework-side
|
|
6
|
+
* fields apps want to filter / sort on:
|
|
7
|
+
*
|
|
8
|
+
* - `id` ULID primary key. Hand the same value back to
|
|
9
|
+
* `BrainStore.loadThread(id)` to rehydrate.
|
|
10
|
+
* - `user_id` App-defined owner. Stored as `text` (no FK) —
|
|
11
|
+
* user table shape varies per app. Indexed in
|
|
12
|
+
* the recommended migration so "list threads
|
|
13
|
+
* for user X" stays fast.
|
|
14
|
+
* - `title` Human label. Apps set it from the first user
|
|
15
|
+
* turn or via an explicit "rename" UI.
|
|
16
|
+
* - `system` Thread-owned system prompt. Mirrors
|
|
17
|
+
* `ThreadState.system`. JSONB so the structured
|
|
18
|
+
* form (text + cache flag) round-trips.
|
|
19
|
+
* - `options` Thread defaults applied to every `send()`.
|
|
20
|
+
* Mirrors `ThreadState.options`.
|
|
21
|
+
* - `last_response_id` OpenAI Responses API stateful pointer.
|
|
22
|
+
* Mirrors `ThreadState.lastResponseId`. NULL for
|
|
23
|
+
* non-Responses providers.
|
|
24
|
+
* - `timestamps` `created_at` + `updated_at` for sort / audit.
|
|
25
|
+
*
|
|
26
|
+
* Tenanted: `tenant_id` FK + RLS policies auto-injected by
|
|
27
|
+
* `@strav/database`. Apps wrap calls in `tenants.withTenant(...)`
|
|
28
|
+
* and the database enforces isolation — no app-level filter needed.
|
|
29
|
+
*
|
|
30
|
+
* The per-turn message history lives in `brain_message`, joined by
|
|
31
|
+
* `thread_id`. This keeps every send to an O(1) INSERT and makes
|
|
32
|
+
* pagination / per-turn analytics cheap.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { Archetype, defineSchema } from '@strav/database'
|
|
36
|
+
|
|
37
|
+
export const brainThreadSchema = defineSchema(
|
|
38
|
+
'brain_thread',
|
|
39
|
+
Archetype.Entity,
|
|
40
|
+
(t) => {
|
|
41
|
+
t.id()
|
|
42
|
+
t.string('user_id').max(64).nullable()
|
|
43
|
+
t.string('title').max(255).nullable()
|
|
44
|
+
t.json('system').nullable()
|
|
45
|
+
t.json('options').nullable()
|
|
46
|
+
t.string('last_response_id').max(128).nullable()
|
|
47
|
+
t.timestamps()
|
|
48
|
+
},
|
|
49
|
+
{ tenanted: true },
|
|
50
|
+
)
|
package/src/provider.ts
CHANGED
|
@@ -12,16 +12,26 @@
|
|
|
12
12
|
* subclassing.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import type { AgentGenerateResult } from './agent_generate_result.ts'
|
|
15
16
|
import type { AgentResult } from './agent_result.ts'
|
|
17
|
+
import type { AgentStreamEvent } from './agent_stream_event.ts'
|
|
16
18
|
import type { MCPServer } from './mcp_server.ts'
|
|
17
19
|
import type { OutputSchema } from './output_schema.ts'
|
|
20
|
+
import type { SuspendedRun } from './suspended_run.ts'
|
|
18
21
|
import type { Tool } from './tool.ts'
|
|
22
|
+
import type { ToolExecutionError } from './tool_execution_error.ts'
|
|
19
23
|
import type {
|
|
24
|
+
AudioSource,
|
|
20
25
|
ChatOptions,
|
|
21
26
|
ChatResult,
|
|
27
|
+
EmbedOptions,
|
|
28
|
+
EmbedResult,
|
|
22
29
|
GenerateResult,
|
|
23
30
|
Message,
|
|
24
31
|
StreamEvent,
|
|
32
|
+
ToolUseBlock,
|
|
33
|
+
TranscribeOptions,
|
|
34
|
+
TranscribeResult,
|
|
25
35
|
} from './types.ts'
|
|
26
36
|
|
|
27
37
|
export interface RunWithToolsOptions extends ChatOptions {
|
|
@@ -37,6 +47,63 @@ export interface RunWithToolsOptions extends ChatOptions {
|
|
|
37
47
|
* resulting `mcp_tool_use` / `mcp_tool_result` blocks.
|
|
38
48
|
*/
|
|
39
49
|
mcpServers?: readonly MCPServer[]
|
|
50
|
+
/**
|
|
51
|
+
* Tool-error recovery hook. Called when a tool's `execute` throws
|
|
52
|
+
* — OR when the model called a tool that isn't registered. Two
|
|
53
|
+
* outcomes:
|
|
54
|
+
*
|
|
55
|
+
* - Return a string → the loop continues. The string lands as
|
|
56
|
+
* `tool_result.content` with `isError: true`, the model sees
|
|
57
|
+
* the error and can adapt (try a different approach, ask the
|
|
58
|
+
* user, give up). Recommended for production agents that
|
|
59
|
+
* should survive transient failures.
|
|
60
|
+
*
|
|
61
|
+
* - Return `undefined` (the default when this option is unset)
|
|
62
|
+
* → the framework throws `ToolExecutionError` and the loop
|
|
63
|
+
* aborts. Same behavior as before this option existed.
|
|
64
|
+
*
|
|
65
|
+
* The hook may inspect `error.cause` to filter — e.g., feed back
|
|
66
|
+
* transient HTTP errors but rethrow programmer errors:
|
|
67
|
+
*
|
|
68
|
+
* ```ts
|
|
69
|
+
* onToolError: (err) =>
|
|
70
|
+
* err.cause instanceof TransientError ? err.cause.message : undefined
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
onToolError?(error: ToolExecutionError): string | undefined
|
|
74
|
+
/**
|
|
75
|
+
* Human-in-the-loop gate. Called before each tool execution; when
|
|
76
|
+
* it returns `true`, the loop suspends and `runWithTools` returns
|
|
77
|
+
* a `SuspendedRun` carrying the pending tool calls + a JSON-
|
|
78
|
+
* serializable snapshot of the loop state. Apps obtain results
|
|
79
|
+
* out-of-band (human approval, queued worker, external system,
|
|
80
|
+
* ...) and call `brain.resumeTools(state, results, tools, options)`
|
|
81
|
+
* to continue.
|
|
82
|
+
*
|
|
83
|
+
* Mid-batch invariant: if a tool call inside a multi-call batch
|
|
84
|
+
* triggers suspension, the framework also captures all unexecuted
|
|
85
|
+
* siblings from the same assistant turn — the provider's
|
|
86
|
+
* `tool_use` / `tool_result` pairing must stay balanced on resume.
|
|
87
|
+
*
|
|
88
|
+
* V1 scope: only honored on non-streaming `runWithTools`. Pass it
|
|
89
|
+
* to `streamWithTools`, `runWithToolsAndSchema`, or
|
|
90
|
+
* `streamWithToolsAndSchema` and the framework throws `BrainError`
|
|
91
|
+
* — those entrypoints don't yet model the pause/resume protocol.
|
|
92
|
+
*/
|
|
93
|
+
shouldSuspend?(
|
|
94
|
+
call: ToolUseBlock,
|
|
95
|
+
context?: Record<string, unknown>,
|
|
96
|
+
): boolean | Promise<boolean>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Same as `RunWithToolsOptions` but with `shouldSuspend` required.
|
|
101
|
+
* Used to narrow the return type of `runWithTools` overloads — when
|
|
102
|
+
* apps opt in to the human-in-the-loop gate, the result widens to
|
|
103
|
+
* `AgentResult | SuspendedRun`; otherwise it's just `AgentResult`.
|
|
104
|
+
*/
|
|
105
|
+
export type RunWithToolsOptionsWithSuspend = RunWithToolsOptions & {
|
|
106
|
+
shouldSuspend: NonNullable<RunWithToolsOptions['shouldSuspend']>
|
|
40
107
|
}
|
|
41
108
|
|
|
42
109
|
export interface Provider {
|
|
@@ -81,7 +148,7 @@ export interface Provider {
|
|
|
81
148
|
messages: readonly Message[],
|
|
82
149
|
tools: readonly Tool[],
|
|
83
150
|
options?: RunWithToolsOptions,
|
|
84
|
-
): Promise<AgentResult>
|
|
151
|
+
): Promise<AgentResult | SuspendedRun>
|
|
85
152
|
|
|
86
153
|
/**
|
|
87
154
|
* Structured output. Sends `messages` to the model with a
|
|
@@ -99,4 +166,81 @@ export interface Provider {
|
|
|
99
166
|
schema: OutputSchema<T>,
|
|
100
167
|
options?: ChatOptions,
|
|
101
168
|
): Promise<GenerateResult<T>>
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Tool-loop + structured output combined. Runs the agentic loop
|
|
172
|
+
* with the same tool-handling as `runWithTools`, but pins a
|
|
173
|
+
* JSON-Schema constraint on every turn — so when the model
|
|
174
|
+
* finally answers without calling a tool, its text is JSON
|
|
175
|
+
* matching the schema. Returns the parsed value alongside the
|
|
176
|
+
* loop bookkeeping.
|
|
177
|
+
*
|
|
178
|
+
* Optional on the interface; `BrainManager.generateWithTools`
|
|
179
|
+
* throws `BrainError` when the configured provider lacks it.
|
|
180
|
+
*/
|
|
181
|
+
runWithToolsAndSchema?<T>(
|
|
182
|
+
messages: readonly Message[],
|
|
183
|
+
tools: readonly Tool[],
|
|
184
|
+
schema: OutputSchema<T>,
|
|
185
|
+
options?: RunWithToolsOptions,
|
|
186
|
+
): Promise<AgentGenerateResult<T>>
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Streaming variant of `runWithToolsAndSchema`. Same agentic loop,
|
|
190
|
+
* same schema constraint on every turn — yielded as
|
|
191
|
+
* `AgentStreamEvent<T>`s. The terminal `stop` event carries the
|
|
192
|
+
* parsed `value` + raw `text` alongside the loop bookkeeping.
|
|
193
|
+
*
|
|
194
|
+
* Optional; `BrainManager.streamGenerateWithTools` throws
|
|
195
|
+
* `BrainError` when the chosen provider doesn't implement it.
|
|
196
|
+
*/
|
|
197
|
+
streamWithToolsAndSchema?<T>(
|
|
198
|
+
messages: readonly Message[],
|
|
199
|
+
tools: readonly Tool[],
|
|
200
|
+
schema: OutputSchema<T>,
|
|
201
|
+
options?: RunWithToolsOptions,
|
|
202
|
+
): AsyncIterable<AgentStreamEvent<T>>
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Streaming variant of `runWithTools`. Yields `AgentStreamEvent`s
|
|
206
|
+
* as the loop progresses — text deltas during model turns,
|
|
207
|
+
* `tool_use` / `tool_result` boundaries around tool execution,
|
|
208
|
+
* `iteration_start` / `iteration_end` per round, a terminal
|
|
209
|
+
* `stop` with the full trace + usage.
|
|
210
|
+
*
|
|
211
|
+
* Optional — providers without a streaming tool-loop implementation
|
|
212
|
+
* can omit it; `BrainManager.streamTools` throws `BrainError` in
|
|
213
|
+
* that case.
|
|
214
|
+
*/
|
|
215
|
+
streamWithTools?(
|
|
216
|
+
messages: readonly Message[],
|
|
217
|
+
tools: readonly Tool[],
|
|
218
|
+
options?: RunWithToolsOptions,
|
|
219
|
+
): AsyncIterable<AgentStreamEvent>
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Embeddings — turn one or more text inputs into vectors for
|
|
223
|
+
* similarity search / RAG / clustering. Optional because not
|
|
224
|
+
* every provider exposes an embeddings endpoint (V1: Anthropic
|
|
225
|
+
* and DeepSeek don't; OpenAI, Gemini, Ollama do).
|
|
226
|
+
*/
|
|
227
|
+
embed?(
|
|
228
|
+
texts: readonly string[],
|
|
229
|
+
options?: EmbedOptions,
|
|
230
|
+
): Promise<EmbedResult>
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Audio transcription — convert an audio clip to text.
|
|
234
|
+
* Complements `AudioBlock` (which sends audio + text together
|
|
235
|
+
* to a multimodal chat model) by exposing the dedicated
|
|
236
|
+
* transcription endpoint where the provider has one. V1:
|
|
237
|
+
* OpenAI (Whisper / gpt-4o-transcribe), Ollama (inherits via
|
|
238
|
+
* OpenAI-compat), Gemini (chat-wrap fallback — internally
|
|
239
|
+
* sends an AudioBlock with a "transcribe verbatim" prompt).
|
|
240
|
+
* Anthropic + DeepSeek throw — no transcription API.
|
|
241
|
+
*/
|
|
242
|
+
transcribe?(
|
|
243
|
+
audio: AudioSource,
|
|
244
|
+
options?: TranscribeOptions,
|
|
245
|
+
): Promise<TranscribeResult>
|
|
102
246
|
}
|