@strav/brain 1.0.0-alpha.17 → 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.
@@ -57,7 +57,12 @@ import type { AgentStreamEvent } from '../agent_stream_event.ts'
57
57
  import { resolveMcpTools, type ResolveMcpToolsOptions } from '../mcp/resolve_mcp_tools.ts'
58
58
  import { parseGenerated, type OutputSchema } from '../output_schema.ts'
59
59
  import { recoverOrThrow, runToolWithRecovery } from '../tool_runner.ts'
60
- import type { Provider, RunWithToolsOptions } from '../provider.ts'
60
+ import type {
61
+ Provider,
62
+ RunWithToolsOptions,
63
+ RunWithToolsOptionsWithSuspend,
64
+ } from '../provider.ts'
65
+ import type { SuspendedRun } from '../suspended_run.ts'
61
66
  import type { Tool } from '../tool.ts'
62
67
  import { ToolExecutionError } from '../tool_execution_error.ts'
63
68
  import type {
@@ -92,6 +97,16 @@ export interface OpenAIProviderOptions {
92
97
  * unset; the provider uses the default `MCPClient`.
93
98
  */
94
99
  mcpClientFactory?: ResolveMcpToolsOptions['clientFactory']
100
+ /**
101
+ * Optional MCP connection pool. When set, every `runWithTools`
102
+ * call (and its schema / streaming variants) borrows MCP clients
103
+ * from the pool instead of constructing fresh ones — and the
104
+ * per-call cleanup becomes a no-op so transports survive across
105
+ * calls. Apps construct one pool at boot and pass it to every
106
+ * provider that needs local MCP; pool ownership stays on the app
107
+ * via `pool.close()` at shutdown.
108
+ */
109
+ mcpPool?: ResolveMcpToolsOptions['pool']
95
110
  }
96
111
 
97
112
  export class OpenAIProvider implements Provider {
@@ -108,6 +123,7 @@ export class OpenAIProvider implements Provider {
108
123
  protected readonly defaultEmbedModel: string
109
124
  protected readonly defaultTranscribeModel: string
110
125
  protected readonly mcpClientFactory?: ResolveMcpToolsOptions['clientFactory']
126
+ protected readonly mcpPool?: ResolveMcpToolsOptions['pool']
111
127
 
112
128
  constructor(
113
129
  name: string,
@@ -120,6 +136,7 @@ export class OpenAIProvider implements Provider {
120
136
  this.defaultEmbedModel = config.defaultEmbedModel ?? DEFAULT_OPENAI_EMBED_MODEL
121
137
  this.defaultTranscribeModel = config.defaultTranscribeModel ?? DEFAULT_OPENAI_TRANSCRIBE_MODEL
122
138
  this.mcpClientFactory = options.mcpClientFactory
139
+ this.mcpPool = options.mcpPool
123
140
  this.client =
124
141
  options.client ??
125
142
  new OpenAI({
@@ -164,18 +181,22 @@ export class OpenAIProvider implements Provider {
164
181
  }
165
182
  }
166
183
 
184
+ runWithTools(
185
+ messages: readonly Message[],
186
+ tools: readonly Tool[],
187
+ options: RunWithToolsOptionsWithSuspend,
188
+ ): Promise<AgentResult | SuspendedRun>
189
+ runWithTools(
190
+ messages: readonly Message[],
191
+ tools: readonly Tool[],
192
+ options?: RunWithToolsOptions,
193
+ ): Promise<AgentResult>
167
194
  async runWithTools(
168
195
  messages: readonly Message[],
169
196
  tools: readonly Tool[],
170
197
  options: RunWithToolsOptions = {},
171
- ): Promise<AgentResult> {
172
- const mcpServers: readonly MCPServer[] = options.mcpServers ?? []
173
- const resolved =
174
- mcpServers.length > 0
175
- ? await resolveMcpTools(mcpServers, {
176
- ...(this.mcpClientFactory ? { clientFactory: this.mcpClientFactory } : {}),
177
- })
178
- : { tools: [] as Tool[], close: async () => {} }
198
+ ): Promise<AgentResult | SuspendedRun> {
199
+ const resolved = await this.resolveMcp(options.mcpServers ?? [])
179
200
  try {
180
201
  return await this._runLoop(messages, [...tools, ...resolved.tools], options)
181
202
  } finally {
@@ -187,7 +208,7 @@ export class OpenAIProvider implements Provider {
187
208
  messages: readonly Message[],
188
209
  tools: readonly Tool[],
189
210
  options: RunWithToolsOptions,
190
- ): Promise<AgentResult> {
211
+ ): Promise<AgentResult | SuspendedRun> {
191
212
  const maxIterations = options.maxIterations ?? 10
192
213
  const toolMap = new Map<string, Tool>(tools.map((t) => [t.name, t]))
193
214
  const workingMessages: Message[] = [...messages]
@@ -230,7 +251,8 @@ export class OpenAIProvider implements Provider {
230
251
  }
231
252
 
232
253
  const resultBlocks: ContentBlock[] = []
233
- for (const call of toolCalls) {
254
+ for (let i = 0; i < toolCalls.length; i++) {
255
+ const call = toolCalls[i]!
234
256
  if (call.type !== 'function') continue
235
257
  let parsedInput: unknown
236
258
  let parseFailed: { content: string; isError: boolean } | undefined
@@ -246,6 +268,38 @@ export class OpenAIProvider implements Provider {
246
268
  options,
247
269
  )
248
270
  }
271
+ if (options.shouldSuspend && !parseFailed) {
272
+ const frameworkCall: ToolUseBlock = {
273
+ type: 'tool_use',
274
+ id: call.id,
275
+ name: call.function.name,
276
+ input: (parsedInput ?? {}) as Record<string, unknown>,
277
+ }
278
+ if (await options.shouldSuspend(frameworkCall, options.context)) {
279
+ const pending: ToolUseBlock[] = []
280
+ for (let j = i; j < toolCalls.length; j++) {
281
+ const c = toolCalls[j]!
282
+ if (c.type !== 'function') continue
283
+ let pInput: unknown = {}
284
+ try {
285
+ pInput = c.function.arguments ? JSON.parse(c.function.arguments) : {}
286
+ } catch {
287
+ pInput = c.function.arguments ?? {}
288
+ }
289
+ pending.push({
290
+ type: 'tool_use',
291
+ id: c.id,
292
+ name: c.function.name,
293
+ input: pInput as Record<string, unknown>,
294
+ })
295
+ }
296
+ return {
297
+ status: 'suspended',
298
+ pendingToolCalls: pending,
299
+ state: { messages: workingMessages, iterations, usage: aggregated },
300
+ }
301
+ }
302
+ }
249
303
  const { content, isError } = parseFailed
250
304
  ?? (await runToolWithRecovery(
251
305
  toolMap.get(call.function.name),
@@ -282,13 +336,7 @@ export class OpenAIProvider implements Provider {
282
336
  schema: OutputSchema<T>,
283
337
  options: RunWithToolsOptions = {},
284
338
  ): Promise<AgentGenerateResult<T>> {
285
- const mcpServers: readonly MCPServer[] = options.mcpServers ?? []
286
- const resolved =
287
- mcpServers.length > 0
288
- ? await resolveMcpTools(mcpServers, {
289
- ...(this.mcpClientFactory ? { clientFactory: this.mcpClientFactory } : {}),
290
- })
291
- : { tools: [] as Tool[], close: async () => {} }
339
+ const resolved = await this.resolveMcp(options.mcpServers ?? [])
292
340
  try {
293
341
  return await this._runLoopWithSchema([...tools, ...resolved.tools], messages, schema, options)
294
342
  } finally {
@@ -404,13 +452,7 @@ export class OpenAIProvider implements Provider {
404
452
  tools: readonly Tool[],
405
453
  options: RunWithToolsOptions = {},
406
454
  ): AsyncIterable<AgentStreamEvent> {
407
- const mcpServers: readonly MCPServer[] = options.mcpServers ?? []
408
- const resolved =
409
- mcpServers.length > 0
410
- ? await resolveMcpTools(mcpServers, {
411
- ...(this.mcpClientFactory ? { clientFactory: this.mcpClientFactory } : {}),
412
- })
413
- : { tools: [] as Tool[], close: async () => {} }
455
+ const resolved = await this.resolveMcp(options.mcpServers ?? [])
414
456
  try {
415
457
  yield* this._streamLoop(messages, [...tools, ...resolved.tools], options)
416
458
  } finally {
@@ -598,13 +640,7 @@ export class OpenAIProvider implements Provider {
598
640
  schema: OutputSchema<T>,
599
641
  options: RunWithToolsOptions = {},
600
642
  ): AsyncIterable<AgentStreamEvent<T>> {
601
- const mcpServers: readonly MCPServer[] = options.mcpServers ?? []
602
- const resolved =
603
- mcpServers.length > 0
604
- ? await resolveMcpTools(mcpServers, {
605
- ...(this.mcpClientFactory ? { clientFactory: this.mcpClientFactory } : {}),
606
- })
607
- : { tools: [] as Tool[], close: async () => {} }
643
+ const resolved = await this.resolveMcp(options.mcpServers ?? [])
608
644
  try {
609
645
  yield* this._streamLoopWithSchema(
610
646
  [...tools, ...resolved.tools],
@@ -897,6 +933,25 @@ export class OpenAIProvider implements Provider {
897
933
  }
898
934
  }
899
935
 
936
+ /**
937
+ * Single resolve-MCP entry point used by every tool-loop variant.
938
+ * Threads both the test-only `clientFactory` and the optional
939
+ * `mcpPool` through. Caller invokes `resolved.close()` in
940
+ * `finally`; that's a no-op when the pool owns the lifetime.
941
+ */
942
+ protected resolveMcp(servers: readonly MCPServer[]): Promise<{
943
+ tools: Tool[]
944
+ close: () => Promise<void>
945
+ }> {
946
+ if (servers.length === 0) {
947
+ return Promise.resolve({ tools: [], close: async () => {} })
948
+ }
949
+ return resolveMcpTools(servers, {
950
+ ...(this.mcpClientFactory ? { clientFactory: this.mcpClientFactory } : {}),
951
+ ...(this.mcpPool ? { pool: this.mcpPool } : {}),
952
+ })
953
+ }
954
+
900
955
  // ─── Param translation ──────────────────────────────────────────────────
901
956
 
902
957
  protected buildParams(