@strav/brain 1.0.0-alpha.15 → 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_generate_result.ts +30 -0
- package/src/agent_runner.ts +140 -14
- 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 +19 -1
- 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
|
@@ -27,9 +27,9 @@ import Anthropic from '@anthropic-ai/sdk'
|
|
|
27
27
|
import type { AgentResult } from '../agent_result.ts'
|
|
28
28
|
import type { AnthropicProviderConfig } from '../brain_config.ts'
|
|
29
29
|
import { DEFAULT_MODEL } from '../brain_config.ts'
|
|
30
|
+
import { BrainError } from '../brain_error.ts'
|
|
30
31
|
import type { Provider, RunWithToolsOptions } from '../provider.ts'
|
|
31
32
|
import type { Tool } from '../tool.ts'
|
|
32
|
-
import { ToolExecutionError } from '../tool_execution_error.ts'
|
|
33
33
|
import type {
|
|
34
34
|
ChatOptions,
|
|
35
35
|
ChatResult,
|
|
@@ -39,13 +39,17 @@ import type {
|
|
|
39
39
|
MCPToolResultBlock,
|
|
40
40
|
MCPToolUseBlock,
|
|
41
41
|
Message,
|
|
42
|
+
ServerTool,
|
|
42
43
|
StreamEvent,
|
|
43
44
|
SystemPrompt,
|
|
44
45
|
TextBlock,
|
|
45
46
|
ToolResultBlock,
|
|
46
47
|
ToolUseBlock,
|
|
47
48
|
} from '../types.ts'
|
|
49
|
+
import type { AgentGenerateResult } from '../agent_generate_result.ts'
|
|
50
|
+
import type { AgentStreamEvent } from '../agent_stream_event.ts'
|
|
48
51
|
import { parseGenerated, type OutputSchema } from '../output_schema.ts'
|
|
52
|
+
import { runToolWithRecovery } from '../tool_runner.ts'
|
|
49
53
|
|
|
50
54
|
const EPHEMERAL_CACHE = { type: 'ephemeral' } as const
|
|
51
55
|
|
|
@@ -78,7 +82,7 @@ export class AnthropicProvider implements Provider {
|
|
|
78
82
|
|
|
79
83
|
async chat(messages: readonly Message[], options: ChatOptions = {}): Promise<ChatResult> {
|
|
80
84
|
const params = this.buildParams(messages, options)
|
|
81
|
-
const response = await this.client.messages.create(params)
|
|
85
|
+
const response = await this.client.messages.create(params, reqOpts(options))
|
|
82
86
|
return this.toChatResult(response)
|
|
83
87
|
}
|
|
84
88
|
|
|
@@ -87,7 +91,7 @@ export class AnthropicProvider implements Provider {
|
|
|
87
91
|
options: ChatOptions = {},
|
|
88
92
|
): AsyncIterable<StreamEvent> {
|
|
89
93
|
const params = this.buildParams(messages, options)
|
|
90
|
-
const stream = this.client.messages.stream(params)
|
|
94
|
+
const stream = this.client.messages.stream(params, reqOpts(options))
|
|
91
95
|
for await (const event of stream) {
|
|
92
96
|
if (
|
|
93
97
|
event.type === 'content_block_delta' &&
|
|
@@ -111,12 +115,15 @@ export class AnthropicProvider implements Provider {
|
|
|
111
115
|
const base = this.buildParams(messages, options)
|
|
112
116
|
// count_tokens only accepts a subset of MessageCreateParams; build
|
|
113
117
|
// a focused payload that matches what apps actually need to budget.
|
|
114
|
-
const result = await this.client.messages.countTokens(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
const result = await this.client.messages.countTokens(
|
|
119
|
+
{
|
|
120
|
+
model: base.model,
|
|
121
|
+
messages: base.messages,
|
|
122
|
+
...(base.system !== undefined ? { system: base.system } : {}),
|
|
123
|
+
...(base.thinking !== undefined ? { thinking: base.thinking } : {}),
|
|
124
|
+
},
|
|
125
|
+
reqOpts(options),
|
|
126
|
+
)
|
|
120
127
|
return result.input_tokens
|
|
121
128
|
}
|
|
122
129
|
|
|
@@ -151,10 +158,13 @@ export class AnthropicProvider implements Provider {
|
|
|
151
158
|
const useMcpBeta = mcpServers.length > 0
|
|
152
159
|
|
|
153
160
|
while (true) {
|
|
161
|
+
checkAborted(options.signal)
|
|
154
162
|
const params = this.buildParams(workingMessages, options) as Anthropic.MessageCreateParamsNonStreaming & {
|
|
155
163
|
mcp_servers?: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition[]
|
|
156
164
|
}
|
|
157
165
|
params.tools = [
|
|
166
|
+
// Server tools placed first when present (from buildParams).
|
|
167
|
+
...((params.tools ?? []) as Anthropic.ToolUnion[]),
|
|
158
168
|
...tools.map((t) => ({
|
|
159
169
|
name: t.name,
|
|
160
170
|
description: t.description,
|
|
@@ -193,9 +203,10 @@ export class AnthropicProvider implements Provider {
|
|
|
193
203
|
: [...baseBetas, 'mcp-client-2025-11-20']
|
|
194
204
|
response = (await this.client.beta.messages.create(
|
|
195
205
|
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsNonStreaming,
|
|
206
|
+
reqOpts(options),
|
|
196
207
|
)) as unknown as Anthropic.Message
|
|
197
208
|
} else {
|
|
198
|
-
response = await this.client.messages.create(params)
|
|
209
|
+
response = await this.client.messages.create(params, reqOpts(options))
|
|
199
210
|
}
|
|
200
211
|
addUsage(aggregated, response.usage)
|
|
201
212
|
lastStopReason = response.stop_reason ?? null
|
|
@@ -226,27 +237,142 @@ export class AnthropicProvider implements Provider {
|
|
|
226
237
|
)
|
|
227
238
|
const resultBlocks: ContentBlock[] = []
|
|
228
239
|
for (const block of toolUseBlocks) {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
240
|
+
const { content, isError } = await runToolWithRecovery(
|
|
241
|
+
toolMap.get(block.name),
|
|
242
|
+
block.name,
|
|
243
|
+
block.id,
|
|
244
|
+
block.input,
|
|
245
|
+
options,
|
|
246
|
+
)
|
|
247
|
+
const resultBlock: ToolResultBlock = {
|
|
248
|
+
type: 'tool_result',
|
|
249
|
+
toolUseId: block.id,
|
|
250
|
+
content,
|
|
251
|
+
...(isError ? { isError: true } : {}),
|
|
252
|
+
}
|
|
253
|
+
resultBlocks.push(resultBlock)
|
|
254
|
+
}
|
|
255
|
+
workingMessages.push({ role: 'user', content: resultBlocks })
|
|
256
|
+
|
|
257
|
+
iterations++
|
|
258
|
+
if (iterations >= maxIterations) {
|
|
259
|
+
return {
|
|
260
|
+
text: collectText(response.content),
|
|
261
|
+
messages: workingMessages,
|
|
262
|
+
iterations,
|
|
263
|
+
stopReason: 'max_iterations',
|
|
264
|
+
usage: aggregated,
|
|
236
265
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async runWithToolsAndSchema<T>(
|
|
271
|
+
messages: readonly Message[],
|
|
272
|
+
tools: readonly Tool[],
|
|
273
|
+
schema: OutputSchema<T>,
|
|
274
|
+
options: RunWithToolsOptions = {},
|
|
275
|
+
): Promise<AgentGenerateResult<T>> {
|
|
276
|
+
const maxIterations = options.maxIterations ?? 10
|
|
277
|
+
const toolMap = new Map<string, Tool>(tools.map((t) => [t.name, t]))
|
|
278
|
+
const workingMessages: Message[] = [...messages]
|
|
279
|
+
const aggregated: ChatUsage = {
|
|
280
|
+
inputTokens: 0,
|
|
281
|
+
outputTokens: 0,
|
|
282
|
+
cacheReadTokens: 0,
|
|
283
|
+
cacheCreationTokens: 0,
|
|
284
|
+
}
|
|
285
|
+
let iterations = 0
|
|
286
|
+
let lastStopReason: string | null = null
|
|
287
|
+
|
|
288
|
+
const mcpServers = options.mcpServers ?? []
|
|
289
|
+
const useMcpBeta = mcpServers.length > 0
|
|
290
|
+
|
|
291
|
+
while (true) {
|
|
292
|
+
checkAborted(options.signal)
|
|
293
|
+
const params = this.buildParams(workingMessages, options) as Anthropic.MessageCreateParamsNonStreaming & {
|
|
294
|
+
mcp_servers?: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition[]
|
|
295
|
+
}
|
|
296
|
+
params.tools = [
|
|
297
|
+
// Server tools placed first when present (from buildParams).
|
|
298
|
+
...((params.tools ?? []) as Anthropic.ToolUnion[]),
|
|
299
|
+
...tools.map((t) => ({
|
|
300
|
+
name: t.name,
|
|
301
|
+
description: t.description,
|
|
302
|
+
input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
|
|
303
|
+
})),
|
|
304
|
+
...mcpServers
|
|
305
|
+
.filter((s) => s.tools?.enabled !== false)
|
|
306
|
+
.map((s) => ({
|
|
307
|
+
type: 'mcp_toolset' as const,
|
|
308
|
+
mcp_server_name: s.name,
|
|
309
|
+
...(s.tools?.allowedTools ? { allowed_tools: [...s.tools.allowedTools] } : {}),
|
|
310
|
+
})),
|
|
311
|
+
] as unknown as Anthropic.MessageCreateParams['tools']
|
|
312
|
+
params.output_config = {
|
|
313
|
+
...(params.output_config ?? {}),
|
|
314
|
+
format: { type: 'json_schema', schema: schema.jsonSchema },
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let response: Anthropic.Message
|
|
318
|
+
if (useMcpBeta) {
|
|
319
|
+
params.mcp_servers = mcpServers.map((s) => {
|
|
320
|
+
const def: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition = {
|
|
321
|
+
type: 'url',
|
|
322
|
+
name: s.name,
|
|
323
|
+
url: s.url,
|
|
324
|
+
}
|
|
325
|
+
if (s.authorizationToken !== undefined) def.authorization_token = s.authorizationToken
|
|
326
|
+
return def
|
|
327
|
+
})
|
|
328
|
+
const baseBetas = (params as { betas?: readonly string[] }).betas ?? []
|
|
329
|
+
;(params as { betas?: string[] }).betas = baseBetas.includes('mcp-client-2025-11-20')
|
|
330
|
+
? [...baseBetas]
|
|
331
|
+
: [...baseBetas, 'mcp-client-2025-11-20']
|
|
332
|
+
response = (await this.client.beta.messages.create(
|
|
333
|
+
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsNonStreaming,
|
|
334
|
+
reqOpts(options),
|
|
335
|
+
)) as unknown as Anthropic.Message
|
|
336
|
+
} else {
|
|
337
|
+
response = await this.client.messages.create(params, reqOpts(options))
|
|
338
|
+
}
|
|
339
|
+
addUsage(aggregated, response.usage)
|
|
340
|
+
lastStopReason = response.stop_reason ?? null
|
|
341
|
+
|
|
342
|
+
workingMessages.push({
|
|
343
|
+
role: 'assistant',
|
|
344
|
+
content: fromAnthropicContent(response.content),
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
if (response.stop_reason !== 'tool_use') {
|
|
348
|
+
const text = collectText(response.content)
|
|
349
|
+
return {
|
|
350
|
+
value: parseGenerated(text, schema),
|
|
351
|
+
text,
|
|
352
|
+
messages: workingMessages,
|
|
353
|
+
iterations,
|
|
354
|
+
stopReason: lastStopReason ?? 'end_turn',
|
|
355
|
+
usage: aggregated,
|
|
245
356
|
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const toolUseBlocks = response.content.filter(
|
|
360
|
+
(b): b is Anthropic.ToolUseBlock => b.type === 'tool_use',
|
|
361
|
+
)
|
|
362
|
+
const resultBlocks: ContentBlock[] = []
|
|
363
|
+
for (const block of toolUseBlocks) {
|
|
364
|
+
const { content, isError } = await runToolWithRecovery(
|
|
365
|
+
toolMap.get(block.name),
|
|
366
|
+
block.name,
|
|
367
|
+
block.id,
|
|
368
|
+
block.input,
|
|
369
|
+
options,
|
|
370
|
+
)
|
|
246
371
|
const resultBlock: ToolResultBlock = {
|
|
247
372
|
type: 'tool_result',
|
|
248
373
|
toolUseId: block.id,
|
|
249
|
-
content
|
|
374
|
+
content,
|
|
375
|
+
...(isError ? { isError: true } : {}),
|
|
250
376
|
}
|
|
251
377
|
resultBlocks.push(resultBlock)
|
|
252
378
|
}
|
|
@@ -254,8 +380,12 @@ export class AnthropicProvider implements Provider {
|
|
|
254
380
|
|
|
255
381
|
iterations++
|
|
256
382
|
if (iterations >= maxIterations) {
|
|
383
|
+
const text = collectText(response.content)
|
|
384
|
+
// Last turn was a tool_use response, so text may be empty —
|
|
385
|
+
// surface what we have but the value will likely fail parse.
|
|
257
386
|
return {
|
|
258
|
-
|
|
387
|
+
value: parseGenerated(text, schema),
|
|
388
|
+
text,
|
|
259
389
|
messages: workingMessages,
|
|
260
390
|
iterations,
|
|
261
391
|
stopReason: 'max_iterations',
|
|
@@ -265,6 +395,338 @@ export class AnthropicProvider implements Provider {
|
|
|
265
395
|
}
|
|
266
396
|
}
|
|
267
397
|
|
|
398
|
+
async *streamWithTools(
|
|
399
|
+
messages: readonly Message[],
|
|
400
|
+
tools: readonly Tool[],
|
|
401
|
+
options: RunWithToolsOptions = {},
|
|
402
|
+
): AsyncIterable<AgentStreamEvent> {
|
|
403
|
+
const maxIterations = options.maxIterations ?? 10
|
|
404
|
+
const toolMap = new Map<string, Tool>(tools.map((t) => [t.name, t]))
|
|
405
|
+
const workingMessages: Message[] = [...messages]
|
|
406
|
+
const aggregated: ChatUsage = {
|
|
407
|
+
inputTokens: 0,
|
|
408
|
+
outputTokens: 0,
|
|
409
|
+
cacheReadTokens: 0,
|
|
410
|
+
cacheCreationTokens: 0,
|
|
411
|
+
}
|
|
412
|
+
let iterations = 0
|
|
413
|
+
|
|
414
|
+
const mcpServers = options.mcpServers ?? []
|
|
415
|
+
const useMcpBeta = mcpServers.length > 0
|
|
416
|
+
|
|
417
|
+
while (true) {
|
|
418
|
+
checkAborted(options.signal)
|
|
419
|
+
yield { type: 'iteration_start', iteration: iterations }
|
|
420
|
+
|
|
421
|
+
const params = this.buildParams(workingMessages, options) as Anthropic.MessageCreateParamsNonStreaming & {
|
|
422
|
+
mcp_servers?: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition[]
|
|
423
|
+
}
|
|
424
|
+
params.tools = [
|
|
425
|
+
// Server tools placed first when present (from buildParams).
|
|
426
|
+
...((params.tools ?? []) as Anthropic.ToolUnion[]),
|
|
427
|
+
...tools.map((t) => ({
|
|
428
|
+
name: t.name,
|
|
429
|
+
description: t.description,
|
|
430
|
+
input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
|
|
431
|
+
})),
|
|
432
|
+
...mcpServers
|
|
433
|
+
.filter((s) => s.tools?.enabled !== false)
|
|
434
|
+
.map((s) => ({
|
|
435
|
+
type: 'mcp_toolset' as const,
|
|
436
|
+
mcp_server_name: s.name,
|
|
437
|
+
...(s.tools?.allowedTools ? { allowed_tools: [...s.tools.allowedTools] } : {}),
|
|
438
|
+
})),
|
|
439
|
+
] as unknown as Anthropic.MessageCreateParams['tools']
|
|
440
|
+
|
|
441
|
+
if (useMcpBeta) {
|
|
442
|
+
params.mcp_servers = mcpServers.map((s) => {
|
|
443
|
+
const def: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition = {
|
|
444
|
+
type: 'url',
|
|
445
|
+
name: s.name,
|
|
446
|
+
url: s.url,
|
|
447
|
+
}
|
|
448
|
+
if (s.authorizationToken !== undefined) def.authorization_token = s.authorizationToken
|
|
449
|
+
return def
|
|
450
|
+
})
|
|
451
|
+
const baseBetas = (params as { betas?: readonly string[] }).betas ?? []
|
|
452
|
+
;(params as { betas?: string[] }).betas = baseBetas.includes('mcp-client-2025-11-20')
|
|
453
|
+
? [...baseBetas]
|
|
454
|
+
: [...baseBetas, 'mcp-client-2025-11-20']
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const stream = useMcpBeta
|
|
458
|
+
? this.client.beta.messages.stream(
|
|
459
|
+
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsStreaming,
|
|
460
|
+
reqOpts(options),
|
|
461
|
+
)
|
|
462
|
+
: this.client.messages.stream(params, reqOpts(options))
|
|
463
|
+
|
|
464
|
+
// Track tool_use content blocks by their stream index so
|
|
465
|
+
// `input_json_delta` events can be paired with the correct id.
|
|
466
|
+
// Anthropic's streaming protocol issues a `content_block_start`
|
|
467
|
+
// carrying the tool's id + name, then a sequence of
|
|
468
|
+
// `input_json_delta`s with `partial_json` chunks, then a
|
|
469
|
+
// `content_block_stop`.
|
|
470
|
+
const toolBlockIdByIndex = new Map<number, string>()
|
|
471
|
+
for await (const event of stream) {
|
|
472
|
+
if (
|
|
473
|
+
event.type === 'content_block_start' &&
|
|
474
|
+
event.content_block.type === 'tool_use'
|
|
475
|
+
) {
|
|
476
|
+
toolBlockIdByIndex.set(event.index, event.content_block.id)
|
|
477
|
+
yield {
|
|
478
|
+
type: 'tool_use_start',
|
|
479
|
+
id: event.content_block.id,
|
|
480
|
+
name: event.content_block.name,
|
|
481
|
+
}
|
|
482
|
+
} else if (event.type === 'content_block_delta') {
|
|
483
|
+
if (event.delta.type === 'text_delta' && event.delta.text.length > 0) {
|
|
484
|
+
yield { type: 'text', delta: event.delta.text }
|
|
485
|
+
} else if (event.delta.type === 'input_json_delta') {
|
|
486
|
+
const id = toolBlockIdByIndex.get(event.index)
|
|
487
|
+
if (id !== undefined && event.delta.partial_json.length > 0) {
|
|
488
|
+
yield { type: 'tool_use_delta', id, argsDelta: event.delta.partial_json }
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const final = (await stream.finalMessage()) as unknown as Anthropic.Message
|
|
494
|
+
addUsage(aggregated, final.usage)
|
|
495
|
+
const finishReason: string | null = final.stop_reason ?? null
|
|
496
|
+
|
|
497
|
+
yield { type: 'iteration_end', iteration: iterations, stopReason: finishReason }
|
|
498
|
+
|
|
499
|
+
workingMessages.push({
|
|
500
|
+
role: 'assistant',
|
|
501
|
+
content: fromAnthropicContent(final.content),
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
if (final.stop_reason !== 'tool_use') {
|
|
505
|
+
yield {
|
|
506
|
+
type: 'stop',
|
|
507
|
+
stopReason: finishReason ?? 'end_turn',
|
|
508
|
+
iterations,
|
|
509
|
+
usage: aggregated,
|
|
510
|
+
messages: workingMessages,
|
|
511
|
+
}
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const toolUseBlocks = final.content.filter(
|
|
516
|
+
(b): b is Anthropic.ToolUseBlock => b.type === 'tool_use',
|
|
517
|
+
)
|
|
518
|
+
const resultBlocks: ContentBlock[] = []
|
|
519
|
+
for (const block of toolUseBlocks) {
|
|
520
|
+
yield { type: 'tool_use', id: block.id, name: block.name, input: block.input }
|
|
521
|
+
const { content, isError } = await runToolWithRecovery(
|
|
522
|
+
toolMap.get(block.name),
|
|
523
|
+
block.name,
|
|
524
|
+
block.id,
|
|
525
|
+
block.input,
|
|
526
|
+
options,
|
|
527
|
+
)
|
|
528
|
+
resultBlocks.push({
|
|
529
|
+
type: 'tool_result',
|
|
530
|
+
toolUseId: block.id,
|
|
531
|
+
content,
|
|
532
|
+
...(isError ? { isError: true } : {}),
|
|
533
|
+
} satisfies ToolResultBlock)
|
|
534
|
+
yield {
|
|
535
|
+
type: 'tool_result',
|
|
536
|
+
id: block.id,
|
|
537
|
+
name: block.name,
|
|
538
|
+
content,
|
|
539
|
+
isError,
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
workingMessages.push({ role: 'user', content: resultBlocks })
|
|
543
|
+
|
|
544
|
+
iterations++
|
|
545
|
+
if (iterations >= maxIterations) {
|
|
546
|
+
yield {
|
|
547
|
+
type: 'stop',
|
|
548
|
+
stopReason: 'max_iterations',
|
|
549
|
+
iterations,
|
|
550
|
+
usage: aggregated,
|
|
551
|
+
messages: workingMessages,
|
|
552
|
+
}
|
|
553
|
+
return
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async *streamWithToolsAndSchema<T>(
|
|
559
|
+
messages: readonly Message[],
|
|
560
|
+
tools: readonly Tool[],
|
|
561
|
+
schema: OutputSchema<T>,
|
|
562
|
+
options: RunWithToolsOptions = {},
|
|
563
|
+
): AsyncIterable<AgentStreamEvent<T>> {
|
|
564
|
+
const maxIterations = options.maxIterations ?? 10
|
|
565
|
+
const toolMap = new Map<string, Tool>(tools.map((t) => [t.name, t]))
|
|
566
|
+
const workingMessages: Message[] = [...messages]
|
|
567
|
+
const aggregated: ChatUsage = {
|
|
568
|
+
inputTokens: 0,
|
|
569
|
+
outputTokens: 0,
|
|
570
|
+
cacheReadTokens: 0,
|
|
571
|
+
cacheCreationTokens: 0,
|
|
572
|
+
}
|
|
573
|
+
let iterations = 0
|
|
574
|
+
|
|
575
|
+
const mcpServers = options.mcpServers ?? []
|
|
576
|
+
const useMcpBeta = mcpServers.length > 0
|
|
577
|
+
|
|
578
|
+
while (true) {
|
|
579
|
+
checkAborted(options.signal)
|
|
580
|
+
yield { type: 'iteration_start', iteration: iterations }
|
|
581
|
+
|
|
582
|
+
const params = this.buildParams(workingMessages, options) as Anthropic.MessageCreateParamsNonStreaming & {
|
|
583
|
+
mcp_servers?: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition[]
|
|
584
|
+
}
|
|
585
|
+
params.tools = [
|
|
586
|
+
// Server tools placed first when present (from buildParams).
|
|
587
|
+
...((params.tools ?? []) as Anthropic.ToolUnion[]),
|
|
588
|
+
...tools.map((t) => ({
|
|
589
|
+
name: t.name,
|
|
590
|
+
description: t.description,
|
|
591
|
+
input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
|
|
592
|
+
})),
|
|
593
|
+
...mcpServers
|
|
594
|
+
.filter((s) => s.tools?.enabled !== false)
|
|
595
|
+
.map((s) => ({
|
|
596
|
+
type: 'mcp_toolset' as const,
|
|
597
|
+
mcp_server_name: s.name,
|
|
598
|
+
...(s.tools?.allowedTools ? { allowed_tools: [...s.tools.allowedTools] } : {}),
|
|
599
|
+
})),
|
|
600
|
+
] as unknown as Anthropic.MessageCreateParams['tools']
|
|
601
|
+
params.output_config = {
|
|
602
|
+
...(params.output_config ?? {}),
|
|
603
|
+
format: { type: 'json_schema', schema: schema.jsonSchema },
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (useMcpBeta) {
|
|
607
|
+
params.mcp_servers = mcpServers.map((s) => {
|
|
608
|
+
const def: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition = {
|
|
609
|
+
type: 'url',
|
|
610
|
+
name: s.name,
|
|
611
|
+
url: s.url,
|
|
612
|
+
}
|
|
613
|
+
if (s.authorizationToken !== undefined) def.authorization_token = s.authorizationToken
|
|
614
|
+
return def
|
|
615
|
+
})
|
|
616
|
+
const baseBetas = (params as { betas?: readonly string[] }).betas ?? []
|
|
617
|
+
;(params as { betas?: string[] }).betas = baseBetas.includes('mcp-client-2025-11-20')
|
|
618
|
+
? [...baseBetas]
|
|
619
|
+
: [...baseBetas, 'mcp-client-2025-11-20']
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const stream = useMcpBeta
|
|
623
|
+
? this.client.beta.messages.stream(
|
|
624
|
+
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsStreaming,
|
|
625
|
+
reqOpts(options),
|
|
626
|
+
)
|
|
627
|
+
: this.client.messages.stream(params, reqOpts(options))
|
|
628
|
+
|
|
629
|
+
// Track tool_use content blocks by their stream index so
|
|
630
|
+
// `input_json_delta` events can be paired with the correct id.
|
|
631
|
+
// Anthropic's streaming protocol issues a `content_block_start`
|
|
632
|
+
// carrying the tool's id + name, then a sequence of
|
|
633
|
+
// `input_json_delta`s with `partial_json` chunks, then a
|
|
634
|
+
// `content_block_stop`.
|
|
635
|
+
const toolBlockIdByIndex = new Map<number, string>()
|
|
636
|
+
for await (const event of stream) {
|
|
637
|
+
if (
|
|
638
|
+
event.type === 'content_block_start' &&
|
|
639
|
+
event.content_block.type === 'tool_use'
|
|
640
|
+
) {
|
|
641
|
+
toolBlockIdByIndex.set(event.index, event.content_block.id)
|
|
642
|
+
yield {
|
|
643
|
+
type: 'tool_use_start',
|
|
644
|
+
id: event.content_block.id,
|
|
645
|
+
name: event.content_block.name,
|
|
646
|
+
}
|
|
647
|
+
} else if (event.type === 'content_block_delta') {
|
|
648
|
+
if (event.delta.type === 'text_delta' && event.delta.text.length > 0) {
|
|
649
|
+
yield { type: 'text', delta: event.delta.text }
|
|
650
|
+
} else if (event.delta.type === 'input_json_delta') {
|
|
651
|
+
const id = toolBlockIdByIndex.get(event.index)
|
|
652
|
+
if (id !== undefined && event.delta.partial_json.length > 0) {
|
|
653
|
+
yield { type: 'tool_use_delta', id, argsDelta: event.delta.partial_json }
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const final = (await stream.finalMessage()) as unknown as Anthropic.Message
|
|
659
|
+
addUsage(aggregated, final.usage)
|
|
660
|
+
const finishReason: string | null = final.stop_reason ?? null
|
|
661
|
+
yield { type: 'iteration_end', iteration: iterations, stopReason: finishReason }
|
|
662
|
+
|
|
663
|
+
workingMessages.push({
|
|
664
|
+
role: 'assistant',
|
|
665
|
+
content: fromAnthropicContent(final.content),
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
if (final.stop_reason !== 'tool_use') {
|
|
669
|
+
const text = collectText(final.content)
|
|
670
|
+
const value = parseGenerated(text, schema)
|
|
671
|
+
yield {
|
|
672
|
+
type: 'stop',
|
|
673
|
+
stopReason: finishReason ?? 'end_turn',
|
|
674
|
+
iterations,
|
|
675
|
+
usage: aggregated,
|
|
676
|
+
messages: workingMessages,
|
|
677
|
+
value,
|
|
678
|
+
text,
|
|
679
|
+
} as AgentStreamEvent<T>
|
|
680
|
+
return
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const toolUseBlocks = final.content.filter(
|
|
684
|
+
(b): b is Anthropic.ToolUseBlock => b.type === 'tool_use',
|
|
685
|
+
)
|
|
686
|
+
const resultBlocks: ContentBlock[] = []
|
|
687
|
+
for (const block of toolUseBlocks) {
|
|
688
|
+
yield { type: 'tool_use', id: block.id, name: block.name, input: block.input }
|
|
689
|
+
const { content, isError } = await runToolWithRecovery(
|
|
690
|
+
toolMap.get(block.name),
|
|
691
|
+
block.name,
|
|
692
|
+
block.id,
|
|
693
|
+
block.input,
|
|
694
|
+
options,
|
|
695
|
+
)
|
|
696
|
+
resultBlocks.push({
|
|
697
|
+
type: 'tool_result',
|
|
698
|
+
toolUseId: block.id,
|
|
699
|
+
content,
|
|
700
|
+
...(isError ? { isError: true } : {}),
|
|
701
|
+
} satisfies ToolResultBlock)
|
|
702
|
+
yield {
|
|
703
|
+
type: 'tool_result',
|
|
704
|
+
id: block.id,
|
|
705
|
+
name: block.name,
|
|
706
|
+
content,
|
|
707
|
+
isError,
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
workingMessages.push({ role: 'user', content: resultBlocks })
|
|
711
|
+
|
|
712
|
+
iterations++
|
|
713
|
+
if (iterations >= maxIterations) {
|
|
714
|
+
const text = collectText(final.content)
|
|
715
|
+
const value = parseGenerated(text, schema)
|
|
716
|
+
yield {
|
|
717
|
+
type: 'stop',
|
|
718
|
+
stopReason: 'max_iterations',
|
|
719
|
+
iterations,
|
|
720
|
+
usage: aggregated,
|
|
721
|
+
messages: workingMessages,
|
|
722
|
+
value,
|
|
723
|
+
text,
|
|
724
|
+
} as AgentStreamEvent<T>
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
268
730
|
async generate<T>(
|
|
269
731
|
messages: readonly Message[],
|
|
270
732
|
schema: OutputSchema<T>,
|
|
@@ -275,7 +737,7 @@ export class AnthropicProvider implements Provider {
|
|
|
275
737
|
...(params.output_config ?? {}),
|
|
276
738
|
format: { type: 'json_schema', schema: schema.jsonSchema },
|
|
277
739
|
}
|
|
278
|
-
const response = await this.client.messages.create(params)
|
|
740
|
+
const response = await this.client.messages.create(params, reqOpts(options))
|
|
279
741
|
const text = collectText(response.content)
|
|
280
742
|
const value = parseGenerated(text, schema)
|
|
281
743
|
return {
|
|
@@ -325,6 +787,10 @@ export class AnthropicProvider implements Provider {
|
|
|
325
787
|
;(params as { betas?: readonly string[] }).betas = betas
|
|
326
788
|
}
|
|
327
789
|
|
|
790
|
+
if (options.serverTools && options.serverTools.length > 0) {
|
|
791
|
+
params.tools = anthropicServerTools(options.serverTools)
|
|
792
|
+
}
|
|
793
|
+
|
|
328
794
|
return params
|
|
329
795
|
}
|
|
330
796
|
|
|
@@ -345,6 +811,18 @@ export class AnthropicProvider implements Provider {
|
|
|
345
811
|
|
|
346
812
|
// ─── Shape converters ─────────────────────────────────────────────────────
|
|
347
813
|
|
|
814
|
+
/** Build the request-options bag forwarded to the SDK. Only `signal` for now. */
|
|
815
|
+
function reqOpts(options: { signal?: AbortSignal }): { signal?: AbortSignal } | undefined {
|
|
816
|
+
return options.signal !== undefined ? { signal: options.signal } : undefined
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** Throw a DOMException-shaped abort error if the signal has fired. */
|
|
820
|
+
function checkAborted(signal: AbortSignal | undefined): void {
|
|
821
|
+
if (signal?.aborted) {
|
|
822
|
+
throw signal.reason ?? new DOMException('Aborted', 'AbortError')
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
348
826
|
function toUsage(u: Anthropic.Usage): ChatUsage {
|
|
349
827
|
return {
|
|
350
828
|
inputTokens: u.input_tokens,
|
|
@@ -392,6 +870,41 @@ function toMessageParam(message: Message): Anthropic.MessageParam {
|
|
|
392
870
|
if (block.isError) param.is_error = true
|
|
393
871
|
return param
|
|
394
872
|
}
|
|
873
|
+
if (block.type === 'image') {
|
|
874
|
+
return {
|
|
875
|
+
type: 'image',
|
|
876
|
+
source:
|
|
877
|
+
block.source.type === 'base64'
|
|
878
|
+
? {
|
|
879
|
+
type: 'base64',
|
|
880
|
+
media_type:
|
|
881
|
+
block.source.mediaType as Anthropic.Base64ImageSource['media_type'],
|
|
882
|
+
data: block.source.data,
|
|
883
|
+
}
|
|
884
|
+
: { type: 'url', url: block.source.url },
|
|
885
|
+
} satisfies Anthropic.ImageBlockParam
|
|
886
|
+
}
|
|
887
|
+
if (block.type === 'document') {
|
|
888
|
+
const documentParam: Anthropic.DocumentBlockParam = {
|
|
889
|
+
type: 'document',
|
|
890
|
+
source:
|
|
891
|
+
block.source.type === 'base64'
|
|
892
|
+
? {
|
|
893
|
+
type: 'base64',
|
|
894
|
+
media_type: 'application/pdf',
|
|
895
|
+
data: block.source.data,
|
|
896
|
+
}
|
|
897
|
+
: { type: 'url', url: block.source.url },
|
|
898
|
+
}
|
|
899
|
+
if (block.title !== undefined) documentParam.title = block.title
|
|
900
|
+
return documentParam
|
|
901
|
+
}
|
|
902
|
+
if (block.type === 'audio') {
|
|
903
|
+
throw new BrainError(
|
|
904
|
+
"AnthropicProvider: audio blocks are not supported. Anthropic's SDK does not expose an audio block type for chat messages. Route audio workloads to Gemini, or transcribe upstream and pass the text.",
|
|
905
|
+
{ context: { provider: 'anthropic' } },
|
|
906
|
+
)
|
|
907
|
+
}
|
|
395
908
|
const text: Anthropic.TextBlockParam = { type: 'text', text: block.text }
|
|
396
909
|
if (block.cache) text.cache_control = EPHEMERAL_CACHE
|
|
397
910
|
return text
|
|
@@ -416,6 +929,61 @@ function toSystemParam(
|
|
|
416
929
|
return [param]
|
|
417
930
|
}
|
|
418
931
|
|
|
932
|
+
/**
|
|
933
|
+
* Translate framework `ServerTool[]` into Anthropic's typed
|
|
934
|
+
* server-tool entries. Uses the latest SDK-known versions; the
|
|
935
|
+
* Anthropic backend is backward-compatible to older clients
|
|
936
|
+
* pinning earlier dates, but we standardize on current. Web fetch
|
|
937
|
+
* is Anthropic-only; `url_context` is rejected (Gemini-only).
|
|
938
|
+
*/
|
|
939
|
+
function anthropicServerTools(serverTools: readonly ServerTool[]): Anthropic.ToolUnion[] {
|
|
940
|
+
const out: Anthropic.ToolUnion[] = []
|
|
941
|
+
for (const t of serverTools) {
|
|
942
|
+
if (t.type === 'web_search') {
|
|
943
|
+
const tool: Anthropic.WebSearchTool20260209 = {
|
|
944
|
+
type: 'web_search_20260209',
|
|
945
|
+
name: 'web_search',
|
|
946
|
+
}
|
|
947
|
+
if (t.maxUses !== undefined) {
|
|
948
|
+
;(tool as { max_uses?: number }).max_uses = t.maxUses
|
|
949
|
+
}
|
|
950
|
+
if (t.allowedDomains !== undefined) {
|
|
951
|
+
tool.allowed_domains = [...t.allowedDomains]
|
|
952
|
+
}
|
|
953
|
+
if (t.blockedDomains !== undefined) {
|
|
954
|
+
tool.blocked_domains = [...t.blockedDomains]
|
|
955
|
+
}
|
|
956
|
+
out.push(tool)
|
|
957
|
+
} else if (t.type === 'code_execution') {
|
|
958
|
+
out.push({
|
|
959
|
+
type: 'code_execution_20260120',
|
|
960
|
+
name: 'code_execution',
|
|
961
|
+
} satisfies Anthropic.CodeExecutionTool20260120)
|
|
962
|
+
} else if (t.type === 'web_fetch') {
|
|
963
|
+
const tool: Anthropic.WebFetchTool20260309 = {
|
|
964
|
+
type: 'web_fetch_20260309',
|
|
965
|
+
name: 'web_fetch',
|
|
966
|
+
}
|
|
967
|
+
if (t.maxUses !== undefined) {
|
|
968
|
+
;(tool as { max_uses?: number }).max_uses = t.maxUses
|
|
969
|
+
}
|
|
970
|
+
if (t.allowedDomains !== undefined) {
|
|
971
|
+
tool.allowed_domains = [...t.allowedDomains]
|
|
972
|
+
}
|
|
973
|
+
if (t.blockedDomains !== undefined) {
|
|
974
|
+
tool.blocked_domains = [...t.blockedDomains]
|
|
975
|
+
}
|
|
976
|
+
out.push(tool)
|
|
977
|
+
} else if (t.type === 'url_context') {
|
|
978
|
+
throw new BrainError(
|
|
979
|
+
'AnthropicProvider: server tool `url_context` is Gemini-only. Use `web_fetch` for Anthropic or route the call to Gemini.',
|
|
980
|
+
{ context: { provider: 'anthropic' } },
|
|
981
|
+
)
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return out
|
|
985
|
+
}
|
|
986
|
+
|
|
419
987
|
function mergeBetas(
|
|
420
988
|
providerBetas: readonly string[],
|
|
421
989
|
callBetas: readonly string[] | undefined,
|