@strav/brain 1.0.0-alpha.8 → 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.
Files changed (75) hide show
  1. package/package.json +23 -7
  2. package/src/agent.ts +97 -0
  3. package/src/agent_generate_result.ts +32 -0
  4. package/src/agent_result.ts +39 -0
  5. package/src/agent_runner.ts +265 -0
  6. package/src/agent_stream_event.ts +100 -0
  7. package/src/brain_config.ts +218 -1
  8. package/src/brain_driver.ts +247 -0
  9. package/src/brain_error.ts +86 -10
  10. package/src/brain_manager.ts +419 -5
  11. package/src/brain_provider.ts +89 -10
  12. package/src/define_tool.ts +42 -0
  13. package/src/drivers/anthropic/anthropic_brain_driver.ts +641 -0
  14. package/src/drivers/anthropic/anthropic_helpers.ts +65 -0
  15. package/src/drivers/anthropic/anthropic_message_builder.ts +258 -0
  16. package/src/drivers/anthropic/anthropic_response_mapper.ts +123 -0
  17. package/src/drivers/anthropic/anthropic_tool_loop.ts +246 -0
  18. package/src/drivers/anthropic/index.ts +1 -0
  19. package/src/drivers/deepseek/deepseek_brain_driver.ts +117 -0
  20. package/src/drivers/deepseek/index.ts +1 -0
  21. package/src/drivers/gemini/gemini_brain_driver.ts +1064 -0
  22. package/src/drivers/gemini/index.ts +1 -0
  23. package/src/drivers/minimax/index.ts +1 -0
  24. package/src/drivers/minimax/minimax_brain_driver.ts +84 -0
  25. package/src/drivers/ollama/index.ts +1 -0
  26. package/src/drivers/ollama/ollama_brain_driver.ts +86 -0
  27. package/src/drivers/openai/index.ts +1 -0
  28. package/src/drivers/openai/openai_brain_driver.ts +796 -0
  29. package/src/drivers/openai/openai_helpers.ts +58 -0
  30. package/src/drivers/openai/openai_message_builder.ts +187 -0
  31. package/src/drivers/openai/openai_response_mapper.ts +70 -0
  32. package/src/drivers/openai/openai_tool_dispatch.ts +127 -0
  33. package/src/drivers/openai/openai_tool_loop.ts +191 -0
  34. package/src/drivers/openai_compat/index.ts +1 -0
  35. package/src/drivers/openai_compat/openai_compat_brain_driver.ts +616 -0
  36. package/src/drivers/openai_responses/index.ts +1 -0
  37. package/src/drivers/openai_responses/openai_responses_brain_driver.ts +1015 -0
  38. package/src/drivers/openrouter/index.ts +1 -0
  39. package/src/drivers/openrouter/openrouter_brain_driver.ts +137 -0
  40. package/src/drivers/qwen/index.ts +1 -0
  41. package/src/drivers/qwen/qwen_brain_driver.ts +103 -0
  42. package/src/index.ts +86 -8
  43. package/src/mcp/client.ts +243 -0
  44. package/src/mcp/index.ts +23 -0
  45. package/src/mcp/oauth.ts +227 -0
  46. package/src/mcp/pool.ts +106 -0
  47. package/src/mcp/resolve_mcp_tools.ts +108 -0
  48. package/src/mcp_server.ts +63 -0
  49. package/src/output_schema.ts +72 -0
  50. package/src/persistence/brain_message.ts +34 -0
  51. package/src/persistence/brain_message_repository.ts +98 -0
  52. package/src/persistence/brain_store.ts +166 -0
  53. package/src/persistence/brain_suspended_run.ts +30 -0
  54. package/src/persistence/brain_suspended_run_repository.ts +59 -0
  55. package/src/persistence/brain_thread.ts +30 -0
  56. package/src/persistence/brain_thread_repository.ts +56 -0
  57. package/src/persistence/database_brain_store.ts +190 -0
  58. package/src/persistence/index.ts +48 -0
  59. package/src/persistence/schemas/brain_message_schema.ts +61 -0
  60. package/src/persistence/schemas/brain_suspended_run_schema.ts +58 -0
  61. package/src/persistence/schemas/brain_thread_schema.ts +50 -0
  62. package/src/persistence/schemas/index.ts +3 -0
  63. package/src/suspended_run.ts +153 -0
  64. package/src/thread.ts +40 -1
  65. package/src/tool.ts +42 -0
  66. package/src/tool_execution_error.ts +26 -0
  67. package/src/tool_runner.ts +81 -0
  68. package/src/translate/index.ts +19 -0
  69. package/src/translate/translate_cache.ts +78 -0
  70. package/src/translate/translate_provider.ts +46 -0
  71. package/src/translate/translator.ts +271 -0
  72. package/src/types.ts +431 -1
  73. package/src/zod/index.ts +121 -0
  74. package/src/provider.ts +0 -48
  75. package/src/providers/anthropic_provider.ts +0 -227
@@ -0,0 +1,227 @@
1
+ /**
2
+ * OAuth support for the local MCP client.
3
+ *
4
+ * Most real-world MCP servers (Linear, Notion, GitHub, Asana,
5
+ * Atlassian) are OAuth-protected. The local client at
6
+ * `@strav/brain/mcp` already supports static bearer tokens via
7
+ * `MCPServer.authorizationToken` — fine for self-hosted servers,
8
+ * useless against any commercial server. This module closes the
9
+ * gap.
10
+ *
11
+ * The flow apps see:
12
+ *
13
+ * ```ts
14
+ * const store = new MemoryOAuthStore()
15
+ * const linear: MCPServer = {
16
+ * name: 'linear',
17
+ * url: 'https://mcp.linear.app',
18
+ * oauth: {
19
+ * redirectUri: 'https://myapp.com/mcp/linear/callback',
20
+ * scope: 'read',
21
+ * store,
22
+ * },
23
+ * }
24
+ *
25
+ * try {
26
+ * const client = new MCPClient(linear)
27
+ * await client.connect()
28
+ * } catch (err) {
29
+ * if (err instanceof MCPAuthRequiredError) {
30
+ * // Redirect the user to err.authorizationUrl and remember
31
+ * // who they were (so the callback handler can rebuild the
32
+ * // store with the right per-user state).
33
+ * }
34
+ * }
35
+ *
36
+ * // Later, in the callback handler:
37
+ * const client = new MCPClient(linear)
38
+ * await client.completeAuthorization(req.query.code)
39
+ * // The store now has tokens; subsequent connect()s succeed.
40
+ * ```
41
+ *
42
+ * The framework is server-side and headless — it can't redirect
43
+ * the user inline. So instead of blocking on `connect()`, we
44
+ * surface `MCPAuthRequiredError` carrying the authorization URL.
45
+ * Apps redirect the user themselves, then call
46
+ * `MCPClient.completeAuthorization(code)` from their callback
47
+ * route.
48
+ *
49
+ * Multi-tenancy: build a fresh `MCPOAuthStore` per `(user, server)`
50
+ * with the user id baked into the storage keys. The store
51
+ * interface is intentionally per-server (no `userId` arg) so apps
52
+ * pick the boundary that matches their data model.
53
+ */
54
+
55
+ import type {
56
+ OAuthClientInformation,
57
+ OAuthClientInformationFull,
58
+ OAuthClientMetadata,
59
+ OAuthTokens,
60
+ } from '@modelcontextprotocol/sdk/shared/auth.js'
61
+ import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
62
+ import { BrainError } from '../brain_error.ts'
63
+
64
+ /**
65
+ * Persistence contract for one MCP server's OAuth state.
66
+ *
67
+ * Methods may be sync or async — implementations are free to call
68
+ * a DB, a Redis cache, a file, or hold the state in memory. The
69
+ * framework awaits whichever shape returns.
70
+ *
71
+ * Per-(server) only by design — apps that need per-(user, server)
72
+ * scoping construct a fresh store per request with the user id
73
+ * baked into the underlying storage keys.
74
+ */
75
+ export interface MCPOAuthStore {
76
+ /** Load the dynamic-client-registration record, or undefined if not registered. */
77
+ clientInformation():
78
+ | OAuthClientInformation
79
+ | undefined
80
+ | Promise<OAuthClientInformation | undefined>
81
+ /** Persist the dynamic-client-registration record after registration succeeds. */
82
+ saveClientInformation(info: OAuthClientInformationFull): void | Promise<void>
83
+ /** Load the active token set, or undefined if the user hasn't authorized. */
84
+ tokens(): OAuthTokens | undefined | Promise<OAuthTokens | undefined>
85
+ /** Persist tokens after authorization completes or a refresh succeeds. */
86
+ saveTokens(tokens: OAuthTokens): void | Promise<void>
87
+ /** Load the PKCE code verifier saved during the authorize step. */
88
+ codeVerifier(): string | Promise<string>
89
+ /** Persist the PKCE code verifier before redirecting to authorize. */
90
+ saveCodeVerifier(verifier: string): void | Promise<void>
91
+ }
92
+
93
+ /** Per-server OAuth configuration on `MCPServer.oauth`. */
94
+ export interface MCPOAuthConfig {
95
+ /** Where the user comes back after authorizing. Must match a registered redirect URI on the OAuth server. */
96
+ redirectUri: string
97
+ /** Optional OAuth scopes to request. Some servers require specific scopes. */
98
+ scope?: string
99
+ /** Per-server token + client-info storage. */
100
+ store: MCPOAuthStore
101
+ /** Optional client metadata for dynamic client registration. Defaults to a minimal sane shape. */
102
+ clientMetadata?: Partial<OAuthClientMetadata>
103
+ }
104
+
105
+ /**
106
+ * `MemoryOAuthStore` — in-memory `MCPOAuthStore` implementation.
107
+ *
108
+ * Fine for tests and single-process dev. Production apps with
109
+ * multiple processes or restarts persist to a DB / Redis / KV.
110
+ */
111
+ export class MemoryOAuthStore implements MCPOAuthStore {
112
+ private _clientInfo: OAuthClientInformationFull | undefined
113
+ private _tokens: OAuthTokens | undefined
114
+ private _verifier: string | undefined
115
+
116
+ clientInformation(): OAuthClientInformation | undefined {
117
+ return this._clientInfo
118
+ }
119
+ saveClientInformation(info: OAuthClientInformationFull): void {
120
+ this._clientInfo = info
121
+ }
122
+ tokens(): OAuthTokens | undefined {
123
+ return this._tokens
124
+ }
125
+ saveTokens(tokens: OAuthTokens): void {
126
+ this._tokens = tokens
127
+ }
128
+ codeVerifier(): string {
129
+ if (this._verifier === undefined) {
130
+ throw new BrainError(
131
+ 'MemoryOAuthStore.codeVerifier(): no PKCE verifier saved. The authorization flow must call saveCodeVerifier before requesting the verifier.',
132
+ )
133
+ }
134
+ return this._verifier
135
+ }
136
+ saveCodeVerifier(verifier: string): void {
137
+ this._verifier = verifier
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Thrown when an MCP server requires the user to authorize before
143
+ * the framework can connect. Apps catch this on `MCPClient.connect()`,
144
+ * redirect the user to `authorizationUrl`, and on the OAuth callback
145
+ * route call `MCPClient.completeAuthorization(code)` to finish the
146
+ * flow.
147
+ *
148
+ * `BrainError` subclass so the typed-exception handler renders it
149
+ * cleanly through the standard pathway.
150
+ */
151
+ export class MCPAuthRequiredError extends BrainError {
152
+ /** URL the user should be redirected to in order to authorize. */
153
+ readonly authorizationUrl: string
154
+
155
+ constructor(serverName: string, authorizationUrl: string) {
156
+ super(
157
+ `MCPClient(${serverName}): authorization required. Redirect the user to authorizationUrl and call completeAuthorization(code) from your callback route.`,
158
+ {
159
+ context: { server: serverName, authorizationUrl },
160
+ },
161
+ )
162
+ this.authorizationUrl = authorizationUrl
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Default client metadata used when dynamic client registration
168
+ * fires. Apps override per-server via
169
+ * `MCPOAuthConfig.clientMetadata`.
170
+ */
171
+ function defaultClientMetadata(redirectUri: string): OAuthClientMetadata {
172
+ return {
173
+ redirect_uris: [redirectUri],
174
+ token_endpoint_auth_method: 'none',
175
+ grant_types: ['authorization_code', 'refresh_token'],
176
+ response_types: ['code'],
177
+ client_name: 'strav-brain',
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Internal — implements the SDK's `OAuthClientProvider` against an
183
+ * `MCPOAuthStore`. Holds the auth URL captured from the SDK so
184
+ * `MCPClient.connect()` can surface it on `MCPAuthRequiredError`.
185
+ */
186
+ export class StoreBackedOAuthProvider implements OAuthClientProvider {
187
+ /** Captured auth URL — populated by the SDK when authorization is needed. */
188
+ capturedAuthorizationUrl: URL | undefined
189
+
190
+ constructor(
191
+ private readonly config: MCPOAuthConfig,
192
+ ) {}
193
+
194
+ get redirectUrl(): string {
195
+ return this.config.redirectUri
196
+ }
197
+
198
+ get clientMetadata(): OAuthClientMetadata {
199
+ return {
200
+ ...defaultClientMetadata(this.config.redirectUri),
201
+ ...(this.config.scope !== undefined ? { scope: this.config.scope } : {}),
202
+ ...(this.config.clientMetadata ?? {}),
203
+ }
204
+ }
205
+
206
+ async clientInformation(): Promise<OAuthClientInformation | undefined> {
207
+ return await this.config.store.clientInformation()
208
+ }
209
+ async saveClientInformation(info: OAuthClientInformationFull): Promise<void> {
210
+ await this.config.store.saveClientInformation(info)
211
+ }
212
+ async tokens(): Promise<OAuthTokens | undefined> {
213
+ return await this.config.store.tokens()
214
+ }
215
+ async saveTokens(tokens: OAuthTokens): Promise<void> {
216
+ await this.config.store.saveTokens(tokens)
217
+ }
218
+ async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
219
+ this.capturedAuthorizationUrl = authorizationUrl
220
+ }
221
+ async saveCodeVerifier(verifier: string): Promise<void> {
222
+ await this.config.store.saveCodeVerifier(verifier)
223
+ }
224
+ async codeVerifier(): Promise<string> {
225
+ return await this.config.store.codeVerifier()
226
+ }
227
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * `MCPClientPool` — long-lived, per-server `MCPClient` cache.
3
+ *
4
+ * Default `resolveMcpTools` flow constructs a fresh `MCPClient` per
5
+ * call to `runTools` / `runWithTools` / etc., handshakes the
6
+ * Streamable HTTP transport, lists tools, executes them, then
7
+ * closes the transport in a `finally`. For one-shot calls that's
8
+ * fine. For long-running agent workers — chat servers, background
9
+ * job processors — the per-call handshake adds noticeable
10
+ * latency and burns connection slots upstream.
11
+ *
12
+ * The pool keeps one connected `MCPClient` per `(server.name,
13
+ * server.url)` pair for the lifetime of the pool. `borrow(server)`
14
+ * returns the pooled client (lazily creating + connecting on
15
+ * first use). When the pool is in play, `resolveMcpTools` skips
16
+ * the per-call `close()` — the pool owns the lifetime — so
17
+ * subsequent calls reuse the existing transport.
18
+ *
19
+ * Apps own the pool's lifetime. Construct one at app boot, hand it
20
+ * to every provider (or to `BrainProvider` if using the DI
21
+ * helper), and call `pool.close()` on shutdown.
22
+ *
23
+ * ```ts
24
+ * const pool = new MCPClientPool()
25
+ *
26
+ * const openai = new OpenAIBrainDriver(
27
+ * 'openai',
28
+ * { driver: 'openai', apiKey: ... },
29
+ * { mcpPool: pool },
30
+ * )
31
+ *
32
+ * // ... many runTools calls later, on graceful shutdown:
33
+ * await pool.close()
34
+ * ```
35
+ *
36
+ * Concurrency: `borrow()` is synchronous; `MCPClient.connect()`
37
+ * itself dedupes concurrent calls. Two parallel `runTools` calls
38
+ * sharing the same pooled client both await one handshake.
39
+ *
40
+ * Re-auth: when a borrowed client throws `MCPAuthRequiredError`,
41
+ * the pool keeps the (still un-authorized) client. Apps call
42
+ * `pool.evict(server)` after running `completeAuthorization` on
43
+ * a fresh client so subsequent borrows see the renewed state —
44
+ * or just reuse the same client the app authorized via the
45
+ * standard `MCPClient.completeAuthorization` flow.
46
+ */
47
+
48
+ import type { MCPServer } from '../mcp_server.ts'
49
+ import { MCPClient } from './client.ts'
50
+
51
+ /** Internal — factory injection for tests. Defaults to `new MCPClient(server)`. */
52
+ export type MCPClientFactory = (server: MCPServer) => MCPClient
53
+
54
+ export class MCPClientPool {
55
+ private readonly clients: Map<string, MCPClient> = new Map()
56
+ private readonly factory: MCPClientFactory
57
+
58
+ constructor(factory: MCPClientFactory = (s) => new MCPClient(s)) {
59
+ this.factory = factory
60
+ }
61
+
62
+ /**
63
+ * Return the pooled client for `server`, constructing + caching it on
64
+ * first call. The client is NOT eagerly connected — the first
65
+ * `listTools` / `callTool` invocation triggers `connect()` once.
66
+ */
67
+ borrow(server: MCPServer): MCPClient {
68
+ const key = poolKey(server)
69
+ const existing = this.clients.get(key)
70
+ if (existing) return existing
71
+ const client = this.factory(server)
72
+ this.clients.set(key, client)
73
+ return client
74
+ }
75
+
76
+ /**
77
+ * Drop the cached client for `server` and close its transport.
78
+ * Useful after the app re-authorizes an OAuth server, or after a
79
+ * transient failure where the connection state is suspect and a
80
+ * fresh handshake on next borrow is preferable.
81
+ */
82
+ async evict(server: MCPServer): Promise<void> {
83
+ const key = poolKey(server)
84
+ const client = this.clients.get(key)
85
+ if (!client) return
86
+ this.clients.delete(key)
87
+ await client.close()
88
+ }
89
+
90
+ /** Close every pooled client. Call on app shutdown. */
91
+ async close(): Promise<void> {
92
+ const all = [...this.clients.values()]
93
+ this.clients.clear()
94
+ await Promise.all(all.map((c) => c.close()))
95
+ }
96
+
97
+ /** Whether the pool currently holds a client for `server`. Used by tests. */
98
+ has(server: MCPServer): boolean {
99
+ return this.clients.has(poolKey(server))
100
+ }
101
+ }
102
+
103
+ /** Pool key: name + url, so two `MCPServer`s with the same name but different URLs don't collide. */
104
+ function poolKey(server: MCPServer): string {
105
+ return `${server.name}|${server.url}`
106
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * `resolveMcpTools` — connects to each `MCPServer`, discovers its
3
+ * tools, and surfaces them as framework `Tool`s the standard agentic
4
+ * loop already knows how to invoke.
5
+ *
6
+ * Honors the per-server config in `MCPServer.tools`:
7
+ * - `enabled: false` → server is skipped entirely.
8
+ * - `allowedTools` → only those tool names are exposed.
9
+ *
10
+ * Naming: discovered tools are namespaced as `<server>__<tool>` to
11
+ * keep names unique when multiple servers expose overlapping names.
12
+ * The `Tool.execute` then routes back to the correct server. The
13
+ * underscore separator (not `.` or `/`) is chosen because OpenAI's
14
+ * tool-name regex rejects `.` and `/`.
15
+ *
16
+ * Lifecycle: this helper returns `{ tools, close }`. `close` runs all
17
+ * client `close()` calls in parallel — providers must call it in a
18
+ * `finally` to avoid leaking transports.
19
+ */
20
+
21
+ import type { MCPServer } from '../mcp_server.ts'
22
+ import type { Tool, ToolContext } from '../tool.ts'
23
+ import { MCPClient } from './client.ts'
24
+ import type { MCPClientPool } from './pool.ts'
25
+
26
+ export interface ResolvedMcpTools {
27
+ tools: Tool[]
28
+ close(): Promise<void>
29
+ }
30
+
31
+ export interface ResolveMcpToolsOptions {
32
+ /** Override the client factory — tests inject mock clients per server here. */
33
+ clientFactory?(server: MCPServer): MCPClient
34
+ /**
35
+ * When set, clients are borrowed from the pool instead of being
36
+ * constructed fresh per call, and the returned `close` becomes a
37
+ * no-op — the pool owns the lifetime, and apps call
38
+ * `pool.close()` on shutdown. Mutually beneficial with
39
+ * `clientFactory` (tests pass a factory to the pool itself).
40
+ */
41
+ pool?: MCPClientPool
42
+ }
43
+
44
+ const NAME_SEPARATOR = '__'
45
+
46
+ export async function resolveMcpTools(
47
+ servers: readonly MCPServer[],
48
+ options: ResolveMcpToolsOptions = {},
49
+ ): Promise<ResolvedMcpTools> {
50
+ const clients: MCPClient[] = []
51
+ const tools: Tool[] = []
52
+ const pooled = options.pool !== undefined
53
+
54
+ for (const server of servers) {
55
+ if (server.tools?.enabled === false) continue
56
+ const client = options.pool
57
+ ? options.pool.borrow(server)
58
+ : options.clientFactory
59
+ ? options.clientFactory(server)
60
+ : new MCPClient(server)
61
+ if (!pooled) clients.push(client)
62
+
63
+ const allowed = server.tools?.allowedTools
64
+ const allowedSet = allowed ? new Set(allowed) : null
65
+
66
+ const descriptors = await client.listTools()
67
+ for (const descriptor of descriptors) {
68
+ if (allowedSet && !allowedSet.has(descriptor.name)) continue
69
+ tools.push(buildTool(server.name, client, descriptor))
70
+ }
71
+ }
72
+
73
+ return {
74
+ tools,
75
+ // Pooled clients live across calls — `close` becomes a no-op
76
+ // and the pool owns the lifetime. Non-pooled clients close
77
+ // here so each `runWithTools` invocation cleans up its own
78
+ // transports.
79
+ close: pooled
80
+ ? async () => {}
81
+ : async () => {
82
+ await Promise.all(clients.map((c) => c.close()))
83
+ },
84
+ }
85
+ }
86
+
87
+ function buildTool(
88
+ serverName: string,
89
+ client: MCPClient,
90
+ descriptor: { name: string; description: string; inputSchema: Record<string, unknown> },
91
+ ): Tool {
92
+ return {
93
+ name: `${serverName}${NAME_SEPARATOR}${descriptor.name}`,
94
+ description: descriptor.description,
95
+ inputSchema: descriptor.inputSchema,
96
+ async execute(input: unknown, ctx: ToolContext): Promise<string> {
97
+ const result = await client.callTool(
98
+ descriptor.name,
99
+ input,
100
+ ctx.signal !== undefined ? { signal: ctx.signal } : {},
101
+ )
102
+ if (result.isError) {
103
+ return `MCP tool error: ${result.content}`
104
+ }
105
+ return result.content
106
+ },
107
+ }
108
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * `MCPServer` — declarative configuration for a remote MCP server
3
+ * that Anthropic's backend invokes on behalf of the model.
4
+ *
5
+ * V1 leverages Anthropic's server-side MCP support: apps declare
6
+ * server URLs (with optional bearer auth) on the request; the
7
+ * backend connects to them, discovers their tools, surfaces them to
8
+ * the model, and runs the tool calls itself. The agentic loop here
9
+ * doesn't intercept MCP tool calls — they appear in the response as
10
+ * `MCPToolUseBlock` / `MCPToolResultBlock` content blocks for
11
+ * observability and audit-trail rendering.
12
+ *
13
+ * For OpenAI / Gemini / DeepSeek providers (later slices), a local
14
+ * MCP client implementation will live alongside this to translate
15
+ * MCP-discovered tools into `Tool` records and let the framework run
16
+ * the loop. The V1 contract stays the same; the per-provider
17
+ * implementation differs.
18
+ *
19
+ * `allowedTools` opts into a subset of the server's exposed tools —
20
+ * useful for narrowing surface area when the MCP server exposes more
21
+ * capabilities than the agent should be able to invoke. `enabled`
22
+ * defaults to `true`; set to `false` to declare the server without
23
+ * routing model calls to it (rare, but handy for temporary
24
+ * disablement without re-deploying config).
25
+ */
26
+
27
+ export interface MCPServerToolConfig {
28
+ /** Whitelist of tool names the agent can call. Omit for "all tools the server exposes." */
29
+ allowedTools?: readonly string[]
30
+ /** Default `true`. Set `false` to declare-but-disable. */
31
+ enabled?: boolean
32
+ }
33
+
34
+ export interface MCPServer {
35
+ /** Server identifier — used in MCPToolUseBlock.serverName + logging. */
36
+ name: string
37
+ /** HTTPS URL of the MCP server. */
38
+ url: string
39
+ /**
40
+ * Optional bearer token. Apps source from env vars / secrets
41
+ * managers — never hardcode. The framework forwards this verbatim
42
+ * to the provider's `authorization_token` field.
43
+ *
44
+ * Mutually exclusive with `oauth`. Use `authorizationToken` for
45
+ * self-hosted servers where you control the token; use `oauth`
46
+ * for commercial servers (Linear, Notion, GitHub, ...).
47
+ */
48
+ authorizationToken?: string
49
+ /**
50
+ * OAuth configuration for servers that require it. When set, the
51
+ * local MCP client (`@strav/brain/mcp`) drives the
52
+ * authorization-code-with-PKCE flow against the server's OAuth
53
+ * endpoints, storing tokens via the supplied `store`. The
54
+ * Anthropic server-side path doesn't use this — Anthropic's
55
+ * connector handles its own auth.
56
+ *
57
+ * Mutually exclusive with `authorizationToken`. See the OAuth
58
+ * section of `docs/brain/guides/mcp.md`.
59
+ */
60
+ oauth?: import('./mcp/oauth.ts').MCPOAuthConfig
61
+ /** Per-server tool config (allowlist / enable flag). */
62
+ tools?: MCPServerToolConfig
63
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `OutputSchema<T>` — declarative description of a structured-output
3
+ * target. Apps pass one of these to `BrainManager.generate(...)` and
4
+ * get back a `GenerateResult<T>` whose `value` is the parsed JSON
5
+ * the model produced.
6
+ *
7
+ * Schema shape:
8
+ * - `name` — short identifier; OpenAI requires it on
9
+ * `response_format.json_schema.name`. Other providers ignore it
10
+ * but apps should still set something stable and meaningful for
11
+ * logs / telemetry.
12
+ * - `description` — optional hint the model sees (some providers
13
+ * surface it next to the schema; others embed it in the prompt
14
+ * translation).
15
+ * - `jsonSchema` — plain JSON Schema (draft 2020-12 compatible).
16
+ * The framework deliberately doesn't depend on Zod; apps that
17
+ * want Zod use `zod-to-json-schema` (or similar) at the call
18
+ * site and feed the result here.
19
+ * - `parse` — optional runtime validator/parser. When set, the
20
+ * framework runs every model response through it before
21
+ * returning. Apps that just want type-level inference (and
22
+ * trust the model + provider validation) can omit it; the
23
+ * return type is then `T` by type assertion only.
24
+ *
25
+ * Why no built-in Zod dep:
26
+ * Same reason `Tool.inputSchema` is plain JSON Schema — Strav
27
+ * doesn't pin apps to a schema library. A thin `@strav/brain-zod`
28
+ * helper that produces `OutputSchema<T>` from a Zod schema is
29
+ * straightforward to ship later without touching this file.
30
+ */
31
+
32
+ export interface OutputSchema<T = unknown> {
33
+ /** Short identifier — provider-specific; some surface it, others ignore. */
34
+ name: string
35
+ /** Optional hint shown to the model alongside the schema. */
36
+ description?: string
37
+ /** JSON Schema describing the expected shape. */
38
+ jsonSchema: Record<string, unknown>
39
+ /** Optional runtime parser/validator. Apps that use Zod plug it in here. */
40
+ parse?(value: unknown): T
41
+ }
42
+
43
+ /**
44
+ * Shared helper used by every provider's `generate` implementation:
45
+ * parse the raw JSON response, run the optional `parse` hook, and
46
+ * wrap parse failures in `BrainError` with the raw text on `.context`
47
+ * so apps can inspect what came back when validation rejects.
48
+ */
49
+ import { BrainError } from './brain_error.ts'
50
+
51
+ export function parseGenerated<T>(text: string, schema: OutputSchema<T>): T {
52
+ let parsed: unknown
53
+ try {
54
+ parsed = JSON.parse(text)
55
+ } catch (cause) {
56
+ throw new BrainError(
57
+ `BrainProvider.generate: response was not valid JSON for schema "${schema.name}".`,
58
+ { context: { schema: schema.name, text }, cause },
59
+ )
60
+ }
61
+ if (schema.parse) {
62
+ try {
63
+ return schema.parse(parsed)
64
+ } catch (cause) {
65
+ throw new BrainError(
66
+ `BrainProvider.generate: response failed schema.parse for "${schema.name}".`,
67
+ { context: { schema: schema.name, text }, cause },
68
+ )
69
+ }
70
+ }
71
+ return parsed as T
72
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `BrainMessage` — the typed row of `brain_message`. One per turn.
3
+ *
4
+ * `content` mirrors `Message.content` — string for plain text or
5
+ * `ContentBlock[]` when the turn carries structured blocks
6
+ * (tool_use, tool_result, image, compaction, ...). JSONB hydration
7
+ * is automatic.
8
+ *
9
+ * Assistant turns carry `model` / `usage` / `stop_reason` /
10
+ * `response_id`; user turns leave them NULL. The repository's
11
+ * `appendTurn` helper writes the right shape per role.
12
+ */
13
+
14
+ import { Model } from '@strav/database'
15
+ import type { ChatUsage, ContentBlock } from '../types.ts'
16
+ import { brainMessageSchema } from './schemas/brain_message_schema.ts'
17
+
18
+ export type BrainMessageRole = 'user' | 'assistant'
19
+
20
+ export class BrainMessage extends Model {
21
+ static override readonly schema = brainMessageSchema
22
+
23
+ id!: string
24
+ tenant_id!: string
25
+ thread_id!: string
26
+ turn_index!: number
27
+ role!: BrainMessageRole
28
+ content!: string | ContentBlock[]
29
+ model!: string | null
30
+ usage!: ChatUsage | null
31
+ stop_reason!: string | null
32
+ response_id!: string | null
33
+ created_at!: Date
34
+ }