@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
|
@@ -27,25 +27,35 @@ 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
|
|
30
|
+
import { BrainError } from '../brain_error.ts'
|
|
31
|
+
import type {
|
|
32
|
+
Provider,
|
|
33
|
+
RunWithToolsOptions,
|
|
34
|
+
RunWithToolsOptionsWithSuspend,
|
|
35
|
+
} from '../provider.ts'
|
|
36
|
+
import type { SuspendedRun } from '../suspended_run.ts'
|
|
31
37
|
import type { Tool } from '../tool.ts'
|
|
32
|
-
import { ToolExecutionError } from '../tool_execution_error.ts'
|
|
33
38
|
import type {
|
|
34
39
|
ChatOptions,
|
|
35
40
|
ChatResult,
|
|
36
41
|
ChatUsage,
|
|
42
|
+
CompactionBlock,
|
|
37
43
|
ContentBlock,
|
|
38
44
|
GenerateResult,
|
|
39
45
|
MCPToolResultBlock,
|
|
40
46
|
MCPToolUseBlock,
|
|
41
47
|
Message,
|
|
48
|
+
ServerTool,
|
|
42
49
|
StreamEvent,
|
|
43
50
|
SystemPrompt,
|
|
44
51
|
TextBlock,
|
|
45
52
|
ToolResultBlock,
|
|
46
53
|
ToolUseBlock,
|
|
47
54
|
} from '../types.ts'
|
|
55
|
+
import type { AgentGenerateResult } from '../agent_generate_result.ts'
|
|
56
|
+
import type { AgentStreamEvent } from '../agent_stream_event.ts'
|
|
48
57
|
import { parseGenerated, type OutputSchema } from '../output_schema.ts'
|
|
58
|
+
import { runToolWithRecovery } from '../tool_runner.ts'
|
|
49
59
|
|
|
50
60
|
const EPHEMERAL_CACHE = { type: 'ephemeral' } as const
|
|
51
61
|
|
|
@@ -78,7 +88,13 @@ export class AnthropicProvider implements Provider {
|
|
|
78
88
|
|
|
79
89
|
async chat(messages: readonly Message[], options: ChatOptions = {}): Promise<ChatResult> {
|
|
80
90
|
const params = this.buildParams(messages, options)
|
|
81
|
-
const
|
|
91
|
+
const useBeta = needsBetaRouting(params)
|
|
92
|
+
const response = useBeta
|
|
93
|
+
? ((await this.client.beta.messages.create(
|
|
94
|
+
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsNonStreaming,
|
|
95
|
+
reqOpts(options),
|
|
96
|
+
)) as unknown as Anthropic.Message)
|
|
97
|
+
: await this.client.messages.create(params, reqOpts(options))
|
|
82
98
|
return this.toChatResult(response)
|
|
83
99
|
}
|
|
84
100
|
|
|
@@ -87,7 +103,12 @@ export class AnthropicProvider implements Provider {
|
|
|
87
103
|
options: ChatOptions = {},
|
|
88
104
|
): AsyncIterable<StreamEvent> {
|
|
89
105
|
const params = this.buildParams(messages, options)
|
|
90
|
-
const stream =
|
|
106
|
+
const stream = needsBetaRouting(params)
|
|
107
|
+
? this.client.beta.messages.stream(
|
|
108
|
+
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsStreaming,
|
|
109
|
+
reqOpts(options),
|
|
110
|
+
)
|
|
111
|
+
: this.client.messages.stream(params, reqOpts(options))
|
|
91
112
|
for await (const event of stream) {
|
|
92
113
|
if (
|
|
93
114
|
event.type === 'content_block_delta' &&
|
|
@@ -111,12 +132,15 @@ export class AnthropicProvider implements Provider {
|
|
|
111
132
|
const base = this.buildParams(messages, options)
|
|
112
133
|
// count_tokens only accepts a subset of MessageCreateParams; build
|
|
113
134
|
// a focused payload that matches what apps actually need to budget.
|
|
114
|
-
const result = await this.client.messages.countTokens(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
const result = await this.client.messages.countTokens(
|
|
136
|
+
{
|
|
137
|
+
model: base.model,
|
|
138
|
+
messages: base.messages,
|
|
139
|
+
...(base.system !== undefined ? { system: base.system } : {}),
|
|
140
|
+
...(base.thinking !== undefined ? { thinking: base.thinking } : {}),
|
|
141
|
+
},
|
|
142
|
+
reqOpts(options),
|
|
143
|
+
)
|
|
120
144
|
return result.input_tokens
|
|
121
145
|
}
|
|
122
146
|
|
|
@@ -130,11 +154,21 @@ export class AnthropicProvider implements Provider {
|
|
|
130
154
|
* `tools` array each turn. Apps that care about cache hits keep
|
|
131
155
|
* the tool list stable across runs.
|
|
132
156
|
*/
|
|
157
|
+
runWithTools(
|
|
158
|
+
messages: readonly Message[],
|
|
159
|
+
tools: readonly Tool[],
|
|
160
|
+
options: RunWithToolsOptionsWithSuspend,
|
|
161
|
+
): Promise<AgentResult | SuspendedRun>
|
|
162
|
+
runWithTools(
|
|
163
|
+
messages: readonly Message[],
|
|
164
|
+
tools: readonly Tool[],
|
|
165
|
+
options?: RunWithToolsOptions,
|
|
166
|
+
): Promise<AgentResult>
|
|
133
167
|
async runWithTools(
|
|
134
168
|
messages: readonly Message[],
|
|
135
169
|
tools: readonly Tool[],
|
|
136
170
|
options: RunWithToolsOptions = {},
|
|
137
|
-
): Promise<AgentResult> {
|
|
171
|
+
): Promise<AgentResult | SuspendedRun> {
|
|
138
172
|
const maxIterations = options.maxIterations ?? 10
|
|
139
173
|
const toolMap = new Map<string, Tool>(tools.map((t) => [t.name, t]))
|
|
140
174
|
const workingMessages: Message[] = [...messages]
|
|
@@ -151,10 +185,13 @@ export class AnthropicProvider implements Provider {
|
|
|
151
185
|
const useMcpBeta = mcpServers.length > 0
|
|
152
186
|
|
|
153
187
|
while (true) {
|
|
188
|
+
checkAborted(options.signal)
|
|
154
189
|
const params = this.buildParams(workingMessages, options) as Anthropic.MessageCreateParamsNonStreaming & {
|
|
155
190
|
mcp_servers?: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition[]
|
|
156
191
|
}
|
|
157
192
|
params.tools = [
|
|
193
|
+
// Server tools placed first when present (from buildParams).
|
|
194
|
+
...((params.tools ?? []) as Anthropic.ToolUnion[]),
|
|
158
195
|
...tools.map((t) => ({
|
|
159
196
|
name: t.name,
|
|
160
197
|
description: t.description,
|
|
@@ -176,7 +213,6 @@ export class AnthropicProvider implements Provider {
|
|
|
176
213
|
|
|
177
214
|
// Declare MCP servers + flip to the beta surface when in use.
|
|
178
215
|
// Anthropic's MCP connector requires `mcp-client-2025-11-20`.
|
|
179
|
-
let response: Anthropic.Message
|
|
180
216
|
if (useMcpBeta) {
|
|
181
217
|
params.mcp_servers = mcpServers.map((s) => {
|
|
182
218
|
const def: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition = {
|
|
@@ -191,12 +227,15 @@ export class AnthropicProvider implements Provider {
|
|
|
191
227
|
;(params as { betas?: string[] }).betas = baseBetas.includes('mcp-client-2025-11-20')
|
|
192
228
|
? [...baseBetas]
|
|
193
229
|
: [...baseBetas, 'mcp-client-2025-11-20']
|
|
194
|
-
response = (await this.client.beta.messages.create(
|
|
195
|
-
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsNonStreaming,
|
|
196
|
-
)) as unknown as Anthropic.Message
|
|
197
|
-
} else {
|
|
198
|
-
response = await this.client.messages.create(params)
|
|
199
230
|
}
|
|
231
|
+
// Route via beta when either MCP servers OR compaction are in
|
|
232
|
+
// play — both live on the beta surface.
|
|
233
|
+
const response: Anthropic.Message = needsBetaRouting(params)
|
|
234
|
+
? ((await this.client.beta.messages.create(
|
|
235
|
+
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsNonStreaming,
|
|
236
|
+
reqOpts(options),
|
|
237
|
+
)) as unknown as Anthropic.Message)
|
|
238
|
+
: await this.client.messages.create(params, reqOpts(options))
|
|
200
239
|
addUsage(aggregated, response.usage)
|
|
201
240
|
lastStopReason = response.stop_reason ?? null
|
|
202
241
|
|
|
@@ -225,28 +264,40 @@ export class AnthropicProvider implements Provider {
|
|
|
225
264
|
(b): b is Anthropic.ToolUseBlock => b.type === 'tool_use',
|
|
226
265
|
)
|
|
227
266
|
const resultBlocks: ContentBlock[] = []
|
|
228
|
-
for (
|
|
229
|
-
const
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
block.id,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
267
|
+
for (let i = 0; i < toolUseBlocks.length; i++) {
|
|
268
|
+
const block = toolUseBlocks[i]!
|
|
269
|
+
if (options.shouldSuspend) {
|
|
270
|
+
const frameworkCall: ToolUseBlock = {
|
|
271
|
+
type: 'tool_use',
|
|
272
|
+
id: block.id,
|
|
273
|
+
name: block.name,
|
|
274
|
+
input: block.input as Record<string, unknown>,
|
|
275
|
+
}
|
|
276
|
+
if (await options.shouldSuspend(frameworkCall, options.context)) {
|
|
277
|
+
return {
|
|
278
|
+
status: 'suspended',
|
|
279
|
+
pendingToolCalls: toolUseBlocks.slice(i).map((b) => ({
|
|
280
|
+
type: 'tool_use',
|
|
281
|
+
id: b.id,
|
|
282
|
+
name: b.name,
|
|
283
|
+
input: b.input as Record<string, unknown>,
|
|
284
|
+
})),
|
|
285
|
+
state: { messages: workingMessages, iterations, usage: aggregated },
|
|
286
|
+
}
|
|
287
|
+
}
|
|
245
288
|
}
|
|
289
|
+
const { content, isError } = await runToolWithRecovery(
|
|
290
|
+
toolMap.get(block.name),
|
|
291
|
+
block.name,
|
|
292
|
+
block.id,
|
|
293
|
+
block.input,
|
|
294
|
+
options,
|
|
295
|
+
)
|
|
246
296
|
const resultBlock: ToolResultBlock = {
|
|
247
297
|
type: 'tool_result',
|
|
248
298
|
toolUseId: block.id,
|
|
249
|
-
content
|
|
299
|
+
content,
|
|
300
|
+
...(isError ? { isError: true } : {}),
|
|
250
301
|
}
|
|
251
302
|
resultBlocks.push(resultBlock)
|
|
252
303
|
}
|
|
@@ -265,6 +316,465 @@ export class AnthropicProvider implements Provider {
|
|
|
265
316
|
}
|
|
266
317
|
}
|
|
267
318
|
|
|
319
|
+
async runWithToolsAndSchema<T>(
|
|
320
|
+
messages: readonly Message[],
|
|
321
|
+
tools: readonly Tool[],
|
|
322
|
+
schema: OutputSchema<T>,
|
|
323
|
+
options: RunWithToolsOptions = {},
|
|
324
|
+
): Promise<AgentGenerateResult<T>> {
|
|
325
|
+
const maxIterations = options.maxIterations ?? 10
|
|
326
|
+
const toolMap = new Map<string, Tool>(tools.map((t) => [t.name, t]))
|
|
327
|
+
const workingMessages: Message[] = [...messages]
|
|
328
|
+
const aggregated: ChatUsage = {
|
|
329
|
+
inputTokens: 0,
|
|
330
|
+
outputTokens: 0,
|
|
331
|
+
cacheReadTokens: 0,
|
|
332
|
+
cacheCreationTokens: 0,
|
|
333
|
+
}
|
|
334
|
+
let iterations = 0
|
|
335
|
+
let lastStopReason: string | null = null
|
|
336
|
+
|
|
337
|
+
const mcpServers = options.mcpServers ?? []
|
|
338
|
+
const useMcpBeta = mcpServers.length > 0
|
|
339
|
+
|
|
340
|
+
while (true) {
|
|
341
|
+
checkAborted(options.signal)
|
|
342
|
+
const params = this.buildParams(workingMessages, options) as Anthropic.MessageCreateParamsNonStreaming & {
|
|
343
|
+
mcp_servers?: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition[]
|
|
344
|
+
}
|
|
345
|
+
params.tools = [
|
|
346
|
+
// Server tools placed first when present (from buildParams).
|
|
347
|
+
...((params.tools ?? []) as Anthropic.ToolUnion[]),
|
|
348
|
+
...tools.map((t) => ({
|
|
349
|
+
name: t.name,
|
|
350
|
+
description: t.description,
|
|
351
|
+
input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
|
|
352
|
+
})),
|
|
353
|
+
...mcpServers
|
|
354
|
+
.filter((s) => s.tools?.enabled !== false)
|
|
355
|
+
.map((s) => ({
|
|
356
|
+
type: 'mcp_toolset' as const,
|
|
357
|
+
mcp_server_name: s.name,
|
|
358
|
+
...(s.tools?.allowedTools ? { allowed_tools: [...s.tools.allowedTools] } : {}),
|
|
359
|
+
})),
|
|
360
|
+
] as unknown as Anthropic.MessageCreateParams['tools']
|
|
361
|
+
params.output_config = {
|
|
362
|
+
...(params.output_config ?? {}),
|
|
363
|
+
format: { type: 'json_schema', schema: schema.jsonSchema },
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (useMcpBeta) {
|
|
367
|
+
params.mcp_servers = mcpServers.map((s) => {
|
|
368
|
+
const def: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition = {
|
|
369
|
+
type: 'url',
|
|
370
|
+
name: s.name,
|
|
371
|
+
url: s.url,
|
|
372
|
+
}
|
|
373
|
+
if (s.authorizationToken !== undefined) def.authorization_token = s.authorizationToken
|
|
374
|
+
return def
|
|
375
|
+
})
|
|
376
|
+
const baseBetas = (params as { betas?: readonly string[] }).betas ?? []
|
|
377
|
+
;(params as { betas?: string[] }).betas = baseBetas.includes('mcp-client-2025-11-20')
|
|
378
|
+
? [...baseBetas]
|
|
379
|
+
: [...baseBetas, 'mcp-client-2025-11-20']
|
|
380
|
+
}
|
|
381
|
+
const response: Anthropic.Message = needsBetaRouting(params)
|
|
382
|
+
? ((await this.client.beta.messages.create(
|
|
383
|
+
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsNonStreaming,
|
|
384
|
+
reqOpts(options),
|
|
385
|
+
)) as unknown as Anthropic.Message)
|
|
386
|
+
: await this.client.messages.create(params, reqOpts(options))
|
|
387
|
+
addUsage(aggregated, response.usage)
|
|
388
|
+
lastStopReason = response.stop_reason ?? null
|
|
389
|
+
|
|
390
|
+
workingMessages.push({
|
|
391
|
+
role: 'assistant',
|
|
392
|
+
content: fromAnthropicContent(response.content),
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
if (response.stop_reason !== 'tool_use') {
|
|
396
|
+
const text = collectText(response.content)
|
|
397
|
+
return {
|
|
398
|
+
value: parseGenerated(text, schema),
|
|
399
|
+
text,
|
|
400
|
+
messages: workingMessages,
|
|
401
|
+
iterations,
|
|
402
|
+
stopReason: lastStopReason ?? 'end_turn',
|
|
403
|
+
usage: aggregated,
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const toolUseBlocks = response.content.filter(
|
|
408
|
+
(b): b is Anthropic.ToolUseBlock => b.type === 'tool_use',
|
|
409
|
+
)
|
|
410
|
+
const resultBlocks: ContentBlock[] = []
|
|
411
|
+
for (const block of toolUseBlocks) {
|
|
412
|
+
const { content, isError } = await runToolWithRecovery(
|
|
413
|
+
toolMap.get(block.name),
|
|
414
|
+
block.name,
|
|
415
|
+
block.id,
|
|
416
|
+
block.input,
|
|
417
|
+
options,
|
|
418
|
+
)
|
|
419
|
+
const resultBlock: ToolResultBlock = {
|
|
420
|
+
type: 'tool_result',
|
|
421
|
+
toolUseId: block.id,
|
|
422
|
+
content,
|
|
423
|
+
...(isError ? { isError: true } : {}),
|
|
424
|
+
}
|
|
425
|
+
resultBlocks.push(resultBlock)
|
|
426
|
+
}
|
|
427
|
+
workingMessages.push({ role: 'user', content: resultBlocks })
|
|
428
|
+
|
|
429
|
+
iterations++
|
|
430
|
+
if (iterations >= maxIterations) {
|
|
431
|
+
const text = collectText(response.content)
|
|
432
|
+
// Last turn was a tool_use response, so text may be empty —
|
|
433
|
+
// surface what we have but the value will likely fail parse.
|
|
434
|
+
return {
|
|
435
|
+
value: parseGenerated(text, schema),
|
|
436
|
+
text,
|
|
437
|
+
messages: workingMessages,
|
|
438
|
+
iterations,
|
|
439
|
+
stopReason: 'max_iterations',
|
|
440
|
+
usage: aggregated,
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async *streamWithTools(
|
|
447
|
+
messages: readonly Message[],
|
|
448
|
+
tools: readonly Tool[],
|
|
449
|
+
options: RunWithToolsOptions = {},
|
|
450
|
+
): AsyncIterable<AgentStreamEvent> {
|
|
451
|
+
const maxIterations = options.maxIterations ?? 10
|
|
452
|
+
const toolMap = new Map<string, Tool>(tools.map((t) => [t.name, t]))
|
|
453
|
+
const workingMessages: Message[] = [...messages]
|
|
454
|
+
const aggregated: ChatUsage = {
|
|
455
|
+
inputTokens: 0,
|
|
456
|
+
outputTokens: 0,
|
|
457
|
+
cacheReadTokens: 0,
|
|
458
|
+
cacheCreationTokens: 0,
|
|
459
|
+
}
|
|
460
|
+
let iterations = 0
|
|
461
|
+
|
|
462
|
+
const mcpServers = options.mcpServers ?? []
|
|
463
|
+
const useMcpBeta = mcpServers.length > 0
|
|
464
|
+
|
|
465
|
+
while (true) {
|
|
466
|
+
checkAborted(options.signal)
|
|
467
|
+
yield { type: 'iteration_start', iteration: iterations }
|
|
468
|
+
|
|
469
|
+
const params = this.buildParams(workingMessages, options) as Anthropic.MessageCreateParamsNonStreaming & {
|
|
470
|
+
mcp_servers?: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition[]
|
|
471
|
+
}
|
|
472
|
+
params.tools = [
|
|
473
|
+
// Server tools placed first when present (from buildParams).
|
|
474
|
+
...((params.tools ?? []) as Anthropic.ToolUnion[]),
|
|
475
|
+
...tools.map((t) => ({
|
|
476
|
+
name: t.name,
|
|
477
|
+
description: t.description,
|
|
478
|
+
input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
|
|
479
|
+
})),
|
|
480
|
+
...mcpServers
|
|
481
|
+
.filter((s) => s.tools?.enabled !== false)
|
|
482
|
+
.map((s) => ({
|
|
483
|
+
type: 'mcp_toolset' as const,
|
|
484
|
+
mcp_server_name: s.name,
|
|
485
|
+
...(s.tools?.allowedTools ? { allowed_tools: [...s.tools.allowedTools] } : {}),
|
|
486
|
+
})),
|
|
487
|
+
] as unknown as Anthropic.MessageCreateParams['tools']
|
|
488
|
+
|
|
489
|
+
if (useMcpBeta) {
|
|
490
|
+
params.mcp_servers = mcpServers.map((s) => {
|
|
491
|
+
const def: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition = {
|
|
492
|
+
type: 'url',
|
|
493
|
+
name: s.name,
|
|
494
|
+
url: s.url,
|
|
495
|
+
}
|
|
496
|
+
if (s.authorizationToken !== undefined) def.authorization_token = s.authorizationToken
|
|
497
|
+
return def
|
|
498
|
+
})
|
|
499
|
+
const baseBetas = (params as { betas?: readonly string[] }).betas ?? []
|
|
500
|
+
;(params as { betas?: string[] }).betas = baseBetas.includes('mcp-client-2025-11-20')
|
|
501
|
+
? [...baseBetas]
|
|
502
|
+
: [...baseBetas, 'mcp-client-2025-11-20']
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const stream = needsBetaRouting(params)
|
|
506
|
+
? this.client.beta.messages.stream(
|
|
507
|
+
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsStreaming,
|
|
508
|
+
reqOpts(options),
|
|
509
|
+
)
|
|
510
|
+
: this.client.messages.stream(params, reqOpts(options))
|
|
511
|
+
|
|
512
|
+
// Track tool_use content blocks by their stream index so
|
|
513
|
+
// `input_json_delta` events can be paired with the correct id.
|
|
514
|
+
// Anthropic's streaming protocol issues a `content_block_start`
|
|
515
|
+
// carrying the tool's id + name, then a sequence of
|
|
516
|
+
// `input_json_delta`s with `partial_json` chunks, then a
|
|
517
|
+
// `content_block_stop`.
|
|
518
|
+
const toolBlockIdByIndex = new Map<number, string>()
|
|
519
|
+
for await (const event of stream) {
|
|
520
|
+
if (
|
|
521
|
+
event.type === 'content_block_start' &&
|
|
522
|
+
event.content_block.type === 'tool_use'
|
|
523
|
+
) {
|
|
524
|
+
toolBlockIdByIndex.set(event.index, event.content_block.id)
|
|
525
|
+
yield {
|
|
526
|
+
type: 'tool_use_start',
|
|
527
|
+
id: event.content_block.id,
|
|
528
|
+
name: event.content_block.name,
|
|
529
|
+
}
|
|
530
|
+
} else if (event.type === 'content_block_delta') {
|
|
531
|
+
if (event.delta.type === 'text_delta' && event.delta.text.length > 0) {
|
|
532
|
+
yield { type: 'text', delta: event.delta.text }
|
|
533
|
+
} else if (event.delta.type === 'input_json_delta') {
|
|
534
|
+
const id = toolBlockIdByIndex.get(event.index)
|
|
535
|
+
if (id !== undefined && event.delta.partial_json.length > 0) {
|
|
536
|
+
yield { type: 'tool_use_delta', id, argsDelta: event.delta.partial_json }
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const final = (await stream.finalMessage()) as unknown as Anthropic.Message
|
|
542
|
+
addUsage(aggregated, final.usage)
|
|
543
|
+
const finishReason: string | null = final.stop_reason ?? null
|
|
544
|
+
|
|
545
|
+
yield { type: 'iteration_end', iteration: iterations, stopReason: finishReason }
|
|
546
|
+
|
|
547
|
+
workingMessages.push({
|
|
548
|
+
role: 'assistant',
|
|
549
|
+
content: fromAnthropicContent(final.content),
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
if (final.stop_reason !== 'tool_use') {
|
|
553
|
+
yield {
|
|
554
|
+
type: 'stop',
|
|
555
|
+
stopReason: finishReason ?? 'end_turn',
|
|
556
|
+
iterations,
|
|
557
|
+
usage: aggregated,
|
|
558
|
+
messages: workingMessages,
|
|
559
|
+
}
|
|
560
|
+
return
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const toolUseBlocks = final.content.filter(
|
|
564
|
+
(b): b is Anthropic.ToolUseBlock => b.type === 'tool_use',
|
|
565
|
+
)
|
|
566
|
+
const resultBlocks: ContentBlock[] = []
|
|
567
|
+
for (const block of toolUseBlocks) {
|
|
568
|
+
yield { type: 'tool_use', id: block.id, name: block.name, input: block.input }
|
|
569
|
+
const { content, isError } = await runToolWithRecovery(
|
|
570
|
+
toolMap.get(block.name),
|
|
571
|
+
block.name,
|
|
572
|
+
block.id,
|
|
573
|
+
block.input,
|
|
574
|
+
options,
|
|
575
|
+
)
|
|
576
|
+
resultBlocks.push({
|
|
577
|
+
type: 'tool_result',
|
|
578
|
+
toolUseId: block.id,
|
|
579
|
+
content,
|
|
580
|
+
...(isError ? { isError: true } : {}),
|
|
581
|
+
} satisfies ToolResultBlock)
|
|
582
|
+
yield {
|
|
583
|
+
type: 'tool_result',
|
|
584
|
+
id: block.id,
|
|
585
|
+
name: block.name,
|
|
586
|
+
content,
|
|
587
|
+
isError,
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
workingMessages.push({ role: 'user', content: resultBlocks })
|
|
591
|
+
|
|
592
|
+
iterations++
|
|
593
|
+
if (iterations >= maxIterations) {
|
|
594
|
+
yield {
|
|
595
|
+
type: 'stop',
|
|
596
|
+
stopReason: 'max_iterations',
|
|
597
|
+
iterations,
|
|
598
|
+
usage: aggregated,
|
|
599
|
+
messages: workingMessages,
|
|
600
|
+
}
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async *streamWithToolsAndSchema<T>(
|
|
607
|
+
messages: readonly Message[],
|
|
608
|
+
tools: readonly Tool[],
|
|
609
|
+
schema: OutputSchema<T>,
|
|
610
|
+
options: RunWithToolsOptions = {},
|
|
611
|
+
): AsyncIterable<AgentStreamEvent<T>> {
|
|
612
|
+
const maxIterations = options.maxIterations ?? 10
|
|
613
|
+
const toolMap = new Map<string, Tool>(tools.map((t) => [t.name, t]))
|
|
614
|
+
const workingMessages: Message[] = [...messages]
|
|
615
|
+
const aggregated: ChatUsage = {
|
|
616
|
+
inputTokens: 0,
|
|
617
|
+
outputTokens: 0,
|
|
618
|
+
cacheReadTokens: 0,
|
|
619
|
+
cacheCreationTokens: 0,
|
|
620
|
+
}
|
|
621
|
+
let iterations = 0
|
|
622
|
+
|
|
623
|
+
const mcpServers = options.mcpServers ?? []
|
|
624
|
+
const useMcpBeta = mcpServers.length > 0
|
|
625
|
+
|
|
626
|
+
while (true) {
|
|
627
|
+
checkAborted(options.signal)
|
|
628
|
+
yield { type: 'iteration_start', iteration: iterations }
|
|
629
|
+
|
|
630
|
+
const params = this.buildParams(workingMessages, options) as Anthropic.MessageCreateParamsNonStreaming & {
|
|
631
|
+
mcp_servers?: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition[]
|
|
632
|
+
}
|
|
633
|
+
params.tools = [
|
|
634
|
+
// Server tools placed first when present (from buildParams).
|
|
635
|
+
...((params.tools ?? []) as Anthropic.ToolUnion[]),
|
|
636
|
+
...tools.map((t) => ({
|
|
637
|
+
name: t.name,
|
|
638
|
+
description: t.description,
|
|
639
|
+
input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
|
|
640
|
+
})),
|
|
641
|
+
...mcpServers
|
|
642
|
+
.filter((s) => s.tools?.enabled !== false)
|
|
643
|
+
.map((s) => ({
|
|
644
|
+
type: 'mcp_toolset' as const,
|
|
645
|
+
mcp_server_name: s.name,
|
|
646
|
+
...(s.tools?.allowedTools ? { allowed_tools: [...s.tools.allowedTools] } : {}),
|
|
647
|
+
})),
|
|
648
|
+
] as unknown as Anthropic.MessageCreateParams['tools']
|
|
649
|
+
params.output_config = {
|
|
650
|
+
...(params.output_config ?? {}),
|
|
651
|
+
format: { type: 'json_schema', schema: schema.jsonSchema },
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (useMcpBeta) {
|
|
655
|
+
params.mcp_servers = mcpServers.map((s) => {
|
|
656
|
+
const def: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition = {
|
|
657
|
+
type: 'url',
|
|
658
|
+
name: s.name,
|
|
659
|
+
url: s.url,
|
|
660
|
+
}
|
|
661
|
+
if (s.authorizationToken !== undefined) def.authorization_token = s.authorizationToken
|
|
662
|
+
return def
|
|
663
|
+
})
|
|
664
|
+
const baseBetas = (params as { betas?: readonly string[] }).betas ?? []
|
|
665
|
+
;(params as { betas?: string[] }).betas = baseBetas.includes('mcp-client-2025-11-20')
|
|
666
|
+
? [...baseBetas]
|
|
667
|
+
: [...baseBetas, 'mcp-client-2025-11-20']
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const stream = needsBetaRouting(params)
|
|
671
|
+
? this.client.beta.messages.stream(
|
|
672
|
+
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsStreaming,
|
|
673
|
+
reqOpts(options),
|
|
674
|
+
)
|
|
675
|
+
: this.client.messages.stream(params, reqOpts(options))
|
|
676
|
+
|
|
677
|
+
// Track tool_use content blocks by their stream index so
|
|
678
|
+
// `input_json_delta` events can be paired with the correct id.
|
|
679
|
+
// Anthropic's streaming protocol issues a `content_block_start`
|
|
680
|
+
// carrying the tool's id + name, then a sequence of
|
|
681
|
+
// `input_json_delta`s with `partial_json` chunks, then a
|
|
682
|
+
// `content_block_stop`.
|
|
683
|
+
const toolBlockIdByIndex = new Map<number, string>()
|
|
684
|
+
for await (const event of stream) {
|
|
685
|
+
if (
|
|
686
|
+
event.type === 'content_block_start' &&
|
|
687
|
+
event.content_block.type === 'tool_use'
|
|
688
|
+
) {
|
|
689
|
+
toolBlockIdByIndex.set(event.index, event.content_block.id)
|
|
690
|
+
yield {
|
|
691
|
+
type: 'tool_use_start',
|
|
692
|
+
id: event.content_block.id,
|
|
693
|
+
name: event.content_block.name,
|
|
694
|
+
}
|
|
695
|
+
} else if (event.type === 'content_block_delta') {
|
|
696
|
+
if (event.delta.type === 'text_delta' && event.delta.text.length > 0) {
|
|
697
|
+
yield { type: 'text', delta: event.delta.text }
|
|
698
|
+
} else if (event.delta.type === 'input_json_delta') {
|
|
699
|
+
const id = toolBlockIdByIndex.get(event.index)
|
|
700
|
+
if (id !== undefined && event.delta.partial_json.length > 0) {
|
|
701
|
+
yield { type: 'tool_use_delta', id, argsDelta: event.delta.partial_json }
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const final = (await stream.finalMessage()) as unknown as Anthropic.Message
|
|
707
|
+
addUsage(aggregated, final.usage)
|
|
708
|
+
const finishReason: string | null = final.stop_reason ?? null
|
|
709
|
+
yield { type: 'iteration_end', iteration: iterations, stopReason: finishReason }
|
|
710
|
+
|
|
711
|
+
workingMessages.push({
|
|
712
|
+
role: 'assistant',
|
|
713
|
+
content: fromAnthropicContent(final.content),
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
if (final.stop_reason !== 'tool_use') {
|
|
717
|
+
const text = collectText(final.content)
|
|
718
|
+
const value = parseGenerated(text, schema)
|
|
719
|
+
yield {
|
|
720
|
+
type: 'stop',
|
|
721
|
+
stopReason: finishReason ?? 'end_turn',
|
|
722
|
+
iterations,
|
|
723
|
+
usage: aggregated,
|
|
724
|
+
messages: workingMessages,
|
|
725
|
+
value,
|
|
726
|
+
text,
|
|
727
|
+
} as AgentStreamEvent<T>
|
|
728
|
+
return
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const toolUseBlocks = final.content.filter(
|
|
732
|
+
(b): b is Anthropic.ToolUseBlock => b.type === 'tool_use',
|
|
733
|
+
)
|
|
734
|
+
const resultBlocks: ContentBlock[] = []
|
|
735
|
+
for (const block of toolUseBlocks) {
|
|
736
|
+
yield { type: 'tool_use', id: block.id, name: block.name, input: block.input }
|
|
737
|
+
const { content, isError } = await runToolWithRecovery(
|
|
738
|
+
toolMap.get(block.name),
|
|
739
|
+
block.name,
|
|
740
|
+
block.id,
|
|
741
|
+
block.input,
|
|
742
|
+
options,
|
|
743
|
+
)
|
|
744
|
+
resultBlocks.push({
|
|
745
|
+
type: 'tool_result',
|
|
746
|
+
toolUseId: block.id,
|
|
747
|
+
content,
|
|
748
|
+
...(isError ? { isError: true } : {}),
|
|
749
|
+
} satisfies ToolResultBlock)
|
|
750
|
+
yield {
|
|
751
|
+
type: 'tool_result',
|
|
752
|
+
id: block.id,
|
|
753
|
+
name: block.name,
|
|
754
|
+
content,
|
|
755
|
+
isError,
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
workingMessages.push({ role: 'user', content: resultBlocks })
|
|
759
|
+
|
|
760
|
+
iterations++
|
|
761
|
+
if (iterations >= maxIterations) {
|
|
762
|
+
const text = collectText(final.content)
|
|
763
|
+
const value = parseGenerated(text, schema)
|
|
764
|
+
yield {
|
|
765
|
+
type: 'stop',
|
|
766
|
+
stopReason: 'max_iterations',
|
|
767
|
+
iterations,
|
|
768
|
+
usage: aggregated,
|
|
769
|
+
messages: workingMessages,
|
|
770
|
+
value,
|
|
771
|
+
text,
|
|
772
|
+
} as AgentStreamEvent<T>
|
|
773
|
+
return
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
268
778
|
async generate<T>(
|
|
269
779
|
messages: readonly Message[],
|
|
270
780
|
schema: OutputSchema<T>,
|
|
@@ -275,7 +785,7 @@ export class AnthropicProvider implements Provider {
|
|
|
275
785
|
...(params.output_config ?? {}),
|
|
276
786
|
format: { type: 'json_schema', schema: schema.jsonSchema },
|
|
277
787
|
}
|
|
278
|
-
const response = await this.client.messages.create(params)
|
|
788
|
+
const response = await this.client.messages.create(params, reqOpts(options))
|
|
279
789
|
const text = collectText(response.content)
|
|
280
790
|
const value = parseGenerated(text, schema)
|
|
281
791
|
return {
|
|
@@ -320,11 +830,34 @@ export class AnthropicProvider implements Provider {
|
|
|
320
830
|
;(params as { cache_control?: { type: 'ephemeral' } }).cache_control = EPHEMERAL_CACHE
|
|
321
831
|
}
|
|
322
832
|
|
|
323
|
-
|
|
833
|
+
// Compaction — emits the beta `edits` entry + flips the
|
|
834
|
+
// `compact-2026-01-12` beta header so the request goes through
|
|
835
|
+
// the SDK's beta surface (same routing as MCP).
|
|
836
|
+
const baseBetas = mergeBetas(this.betas, options.betas)
|
|
837
|
+
const betas = options.compact !== undefined
|
|
838
|
+
? mergeBetas(baseBetas, [COMPACT_BETA])
|
|
839
|
+
: baseBetas
|
|
840
|
+
if (options.compact !== undefined) {
|
|
841
|
+
const edit: Record<string, unknown> = { type: COMPACT_EDIT_TYPE }
|
|
842
|
+
if (options.compact.trigger !== undefined) {
|
|
843
|
+
edit.trigger = { type: 'input_tokens', value: options.compact.trigger }
|
|
844
|
+
}
|
|
845
|
+
if (options.compact.instructions !== undefined) {
|
|
846
|
+
edit.instructions = options.compact.instructions
|
|
847
|
+
}
|
|
848
|
+
if (options.compact.pauseAfterCompaction !== undefined) {
|
|
849
|
+
edit.pause_after_compaction = options.compact.pauseAfterCompaction
|
|
850
|
+
}
|
|
851
|
+
;(params as { edits?: unknown[] }).edits = [edit]
|
|
852
|
+
}
|
|
324
853
|
if (betas.length > 0) {
|
|
325
854
|
;(params as { betas?: readonly string[] }).betas = betas
|
|
326
855
|
}
|
|
327
856
|
|
|
857
|
+
if (options.serverTools && options.serverTools.length > 0) {
|
|
858
|
+
params.tools = anthropicServerTools(options.serverTools)
|
|
859
|
+
}
|
|
860
|
+
|
|
328
861
|
return params
|
|
329
862
|
}
|
|
330
863
|
|
|
@@ -333,18 +866,60 @@ export class AnthropicProvider implements Provider {
|
|
|
333
866
|
.filter((b): b is Anthropic.TextBlock => b.type === 'text')
|
|
334
867
|
.map((b) => b.text)
|
|
335
868
|
.join('')
|
|
336
|
-
|
|
869
|
+
const result: ChatResult<Anthropic.Message> = {
|
|
337
870
|
text,
|
|
338
871
|
model: message.model,
|
|
339
872
|
stopReason: message.stop_reason,
|
|
340
873
|
usage: toUsage(message.usage),
|
|
341
874
|
raw: message,
|
|
342
875
|
}
|
|
876
|
+
// Surface structured content when the turn carries blocks
|
|
877
|
+
// beyond plain text (compaction today; reasoning blocks in a
|
|
878
|
+
// future slice). Apps that persist conversations push this
|
|
879
|
+
// onto the message history so round-trippable blocks survive
|
|
880
|
+
// subsequent requests.
|
|
881
|
+
const blocks = fromAnthropicContent(message.content)
|
|
882
|
+
if (blocks.some((b) => b.type !== 'text')) {
|
|
883
|
+
result.content = blocks
|
|
884
|
+
}
|
|
885
|
+
return result
|
|
343
886
|
}
|
|
344
887
|
}
|
|
345
888
|
|
|
346
889
|
// ─── Shape converters ─────────────────────────────────────────────────────
|
|
347
890
|
|
|
891
|
+
/** Compaction beta — required header + `edits[].type` for `compact-2026-01-12`. */
|
|
892
|
+
const COMPACT_BETA = 'compact-2026-01-12'
|
|
893
|
+
const COMPACT_EDIT_TYPE = 'compact_20260112'
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Whether the request needs to flow through `client.beta.messages.create`
|
|
897
|
+
* instead of the stable surface. Triggered by:
|
|
898
|
+
*
|
|
899
|
+
* - `edits[]` (compaction).
|
|
900
|
+
* - `mcp_servers[]` (server-side MCP).
|
|
901
|
+
*
|
|
902
|
+
* Tests typically stub `client.messages.create`; the beta path uses the
|
|
903
|
+
* stub that lives at `client.beta.messages.create`.
|
|
904
|
+
*/
|
|
905
|
+
function needsBetaRouting(params: Anthropic.MessageCreateParamsNonStreaming): boolean {
|
|
906
|
+
const p = params as { edits?: unknown[]; mcp_servers?: unknown[] }
|
|
907
|
+
return (p.edits !== undefined && p.edits.length > 0)
|
|
908
|
+
|| (p.mcp_servers !== undefined && p.mcp_servers.length > 0)
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/** Build the request-options bag forwarded to the SDK. Only `signal` for now. */
|
|
912
|
+
function reqOpts(options: { signal?: AbortSignal }): { signal?: AbortSignal } | undefined {
|
|
913
|
+
return options.signal !== undefined ? { signal: options.signal } : undefined
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/** Throw a DOMException-shaped abort error if the signal has fired. */
|
|
917
|
+
function checkAborted(signal: AbortSignal | undefined): void {
|
|
918
|
+
if (signal?.aborted) {
|
|
919
|
+
throw signal.reason ?? new DOMException('Aborted', 'AbortError')
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
348
923
|
function toUsage(u: Anthropic.Usage): ChatUsage {
|
|
349
924
|
return {
|
|
350
925
|
inputTokens: u.input_tokens,
|
|
@@ -392,6 +967,54 @@ function toMessageParam(message: Message): Anthropic.MessageParam {
|
|
|
392
967
|
if (block.isError) param.is_error = true
|
|
393
968
|
return param
|
|
394
969
|
}
|
|
970
|
+
if (block.type === 'image') {
|
|
971
|
+
return {
|
|
972
|
+
type: 'image',
|
|
973
|
+
source:
|
|
974
|
+
block.source.type === 'base64'
|
|
975
|
+
? {
|
|
976
|
+
type: 'base64',
|
|
977
|
+
media_type:
|
|
978
|
+
block.source.mediaType as Anthropic.Base64ImageSource['media_type'],
|
|
979
|
+
data: block.source.data,
|
|
980
|
+
}
|
|
981
|
+
: { type: 'url', url: block.source.url },
|
|
982
|
+
} satisfies Anthropic.ImageBlockParam
|
|
983
|
+
}
|
|
984
|
+
if (block.type === 'document') {
|
|
985
|
+
const documentParam: Anthropic.DocumentBlockParam = {
|
|
986
|
+
type: 'document',
|
|
987
|
+
source:
|
|
988
|
+
block.source.type === 'base64'
|
|
989
|
+
? {
|
|
990
|
+
type: 'base64',
|
|
991
|
+
media_type: 'application/pdf',
|
|
992
|
+
data: block.source.data,
|
|
993
|
+
}
|
|
994
|
+
: { type: 'url', url: block.source.url },
|
|
995
|
+
}
|
|
996
|
+
if (block.title !== undefined) documentParam.title = block.title
|
|
997
|
+
return documentParam
|
|
998
|
+
}
|
|
999
|
+
if (block.type === 'audio') {
|
|
1000
|
+
throw new BrainError(
|
|
1001
|
+
"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.",
|
|
1002
|
+
{ context: { provider: 'anthropic' } },
|
|
1003
|
+
)
|
|
1004
|
+
}
|
|
1005
|
+
if (block.type === 'compaction') {
|
|
1006
|
+
// Round-trip the compaction block verbatim — the server uses
|
|
1007
|
+
// the opaque `encrypted_content` to stitch prior compactions
|
|
1008
|
+
// together; mutating either field would invalidate the
|
|
1009
|
+
// history. Untyped on the stable SDK surface; cast through
|
|
1010
|
+
// the beta type shape.
|
|
1011
|
+
const param: Record<string, unknown> = { type: 'compaction' }
|
|
1012
|
+
if (block.content !== null) param.content = block.content
|
|
1013
|
+
if (block.encryptedContent !== null) {
|
|
1014
|
+
param.encrypted_content = block.encryptedContent
|
|
1015
|
+
}
|
|
1016
|
+
return param as unknown as Anthropic.ContentBlockParam
|
|
1017
|
+
}
|
|
395
1018
|
const text: Anthropic.TextBlockParam = { type: 'text', text: block.text }
|
|
396
1019
|
if (block.cache) text.cache_control = EPHEMERAL_CACHE
|
|
397
1020
|
return text
|
|
@@ -416,6 +1039,61 @@ function toSystemParam(
|
|
|
416
1039
|
return [param]
|
|
417
1040
|
}
|
|
418
1041
|
|
|
1042
|
+
/**
|
|
1043
|
+
* Translate framework `ServerTool[]` into Anthropic's typed
|
|
1044
|
+
* server-tool entries. Uses the latest SDK-known versions; the
|
|
1045
|
+
* Anthropic backend is backward-compatible to older clients
|
|
1046
|
+
* pinning earlier dates, but we standardize on current. Web fetch
|
|
1047
|
+
* is Anthropic-only; `url_context` is rejected (Gemini-only).
|
|
1048
|
+
*/
|
|
1049
|
+
function anthropicServerTools(serverTools: readonly ServerTool[]): Anthropic.ToolUnion[] {
|
|
1050
|
+
const out: Anthropic.ToolUnion[] = []
|
|
1051
|
+
for (const t of serverTools) {
|
|
1052
|
+
if (t.type === 'web_search') {
|
|
1053
|
+
const tool: Anthropic.WebSearchTool20260209 = {
|
|
1054
|
+
type: 'web_search_20260209',
|
|
1055
|
+
name: 'web_search',
|
|
1056
|
+
}
|
|
1057
|
+
if (t.maxUses !== undefined) {
|
|
1058
|
+
;(tool as { max_uses?: number }).max_uses = t.maxUses
|
|
1059
|
+
}
|
|
1060
|
+
if (t.allowedDomains !== undefined) {
|
|
1061
|
+
tool.allowed_domains = [...t.allowedDomains]
|
|
1062
|
+
}
|
|
1063
|
+
if (t.blockedDomains !== undefined) {
|
|
1064
|
+
tool.blocked_domains = [...t.blockedDomains]
|
|
1065
|
+
}
|
|
1066
|
+
out.push(tool)
|
|
1067
|
+
} else if (t.type === 'code_execution') {
|
|
1068
|
+
out.push({
|
|
1069
|
+
type: 'code_execution_20260120',
|
|
1070
|
+
name: 'code_execution',
|
|
1071
|
+
} satisfies Anthropic.CodeExecutionTool20260120)
|
|
1072
|
+
} else if (t.type === 'web_fetch') {
|
|
1073
|
+
const tool: Anthropic.WebFetchTool20260309 = {
|
|
1074
|
+
type: 'web_fetch_20260309',
|
|
1075
|
+
name: 'web_fetch',
|
|
1076
|
+
}
|
|
1077
|
+
if (t.maxUses !== undefined) {
|
|
1078
|
+
;(tool as { max_uses?: number }).max_uses = t.maxUses
|
|
1079
|
+
}
|
|
1080
|
+
if (t.allowedDomains !== undefined) {
|
|
1081
|
+
tool.allowed_domains = [...t.allowedDomains]
|
|
1082
|
+
}
|
|
1083
|
+
if (t.blockedDomains !== undefined) {
|
|
1084
|
+
tool.blocked_domains = [...t.blockedDomains]
|
|
1085
|
+
}
|
|
1086
|
+
out.push(tool)
|
|
1087
|
+
} else if (t.type === 'url_context') {
|
|
1088
|
+
throw new BrainError(
|
|
1089
|
+
'AnthropicProvider: server tool `url_context` is Gemini-only. Use `web_fetch` for Anthropic or route the call to Gemini.',
|
|
1090
|
+
{ context: { provider: 'anthropic' } },
|
|
1091
|
+
)
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return out
|
|
1095
|
+
}
|
|
1096
|
+
|
|
419
1097
|
function mergeBetas(
|
|
420
1098
|
providerBetas: readonly string[],
|
|
421
1099
|
callBetas: readonly string[] | undefined,
|
|
@@ -503,6 +1181,13 @@ function fromAnthropicContent(
|
|
|
503
1181
|
}
|
|
504
1182
|
if (r.is_error) result.isError = true
|
|
505
1183
|
out.push(result)
|
|
1184
|
+
} else if (block.type === 'compaction') {
|
|
1185
|
+
const c = block as { content?: string | null; encrypted_content?: string | null }
|
|
1186
|
+
out.push({
|
|
1187
|
+
type: 'compaction',
|
|
1188
|
+
content: c.content ?? null,
|
|
1189
|
+
encryptedContent: c.encrypted_content ?? null,
|
|
1190
|
+
} satisfies CompactionBlock)
|
|
506
1191
|
}
|
|
507
1192
|
}
|
|
508
1193
|
return out
|