@strav/brain 1.0.0-alpha.22 → 1.0.0-alpha.25

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 (45) hide show
  1. package/package.json +3 -3
  2. package/src/agent_runner.ts +1 -1
  3. package/src/{provider.ts → brain_driver.ts} +11 -10
  4. package/src/brain_error.ts +86 -10
  5. package/src/brain_manager.ts +30 -7
  6. package/src/brain_provider.ts +16 -16
  7. package/src/drivers/anthropic/anthropic_brain_driver.ts +641 -0
  8. package/src/drivers/anthropic/anthropic_helpers.ts +65 -0
  9. package/src/drivers/anthropic/anthropic_message_builder.ts +258 -0
  10. package/src/drivers/anthropic/anthropic_response_mapper.ts +123 -0
  11. package/src/drivers/anthropic/anthropic_tool_loop.ts +246 -0
  12. package/src/drivers/anthropic/index.ts +1 -0
  13. package/src/{providers/deepseek_provider.ts → drivers/deepseek/deepseek_brain_driver.ts} +10 -10
  14. package/src/drivers/deepseek/index.ts +1 -0
  15. package/src/{providers/gemini_provider.ts → drivers/gemini/gemini_brain_driver.ts} +21 -21
  16. package/src/drivers/gemini/index.ts +1 -0
  17. package/src/drivers/ollama/index.ts +1 -0
  18. package/src/{providers/ollama_provider.ts → drivers/ollama/ollama_brain_driver.ts} +5 -5
  19. package/src/drivers/openai/index.ts +1 -0
  20. package/src/{providers/openai_provider.ts → drivers/openai/openai_brain_driver.ts} +152 -591
  21. package/src/drivers/openai/openai_helpers.ts +58 -0
  22. package/src/drivers/openai/openai_message_builder.ts +187 -0
  23. package/src/drivers/openai/openai_response_mapper.ts +70 -0
  24. package/src/drivers/openai/openai_tool_dispatch.ts +127 -0
  25. package/src/drivers/openai/openai_tool_loop.ts +191 -0
  26. package/src/drivers/openai_compat/index.ts +1 -0
  27. package/src/{providers/openai_compat_provider.ts → drivers/openai_compat/openai_compat_brain_driver.ts} +16 -16
  28. package/src/drivers/openai_responses/index.ts +1 -0
  29. package/src/{providers/openai_responses_provider.ts → drivers/openai_responses/openai_responses_brain_driver.ts} +24 -24
  30. package/src/index.ts +18 -12
  31. package/src/mcp/pool.ts +1 -1
  32. package/src/persistence/brain_message.ts +1 -1
  33. package/src/persistence/brain_message_repository.ts +3 -11
  34. package/src/persistence/brain_suspended_run.ts +1 -1
  35. package/src/persistence/brain_suspended_run_repository.ts +2 -11
  36. package/src/persistence/brain_thread.ts +1 -1
  37. package/src/persistence/brain_thread_repository.ts +2 -11
  38. package/src/persistence/index.ts +1 -1
  39. package/src/tool_runner.ts +1 -1
  40. package/src/types.ts +2 -2
  41. package/src/providers/anthropic_provider.ts +0 -1194
  42. /package/src/persistence/{schema → schemas}/brain_message_schema.ts +0 -0
  43. /package/src/persistence/{schema → schemas}/brain_suspended_run_schema.ts +0 -0
  44. /package/src/persistence/{schema → schemas}/brain_thread_schema.ts +0 -0
  45. /package/src/persistence/{schema → schemas}/index.ts +0 -0
@@ -1,1194 +0,0 @@
1
- /**
2
- * `AnthropicProvider` — implementation of `Provider` backed by the
3
- * official `@anthropic-ai/sdk`.
4
- *
5
- * Responsibilities:
6
- * 1. Hold a singleton `Anthropic` client instance for the
7
- * configured API key + base URL.
8
- * 2. Translate the framework's `ChatOptions` / `Message` shapes
9
- * into Anthropic's `MessageCreateParams` (system as `TextBlock[]`
10
- * with `cache_control` when requested; messages with per-block
11
- * cache flags translated likewise; `thinking` mapped to
12
- * `ThinkingConfigParam`; `effort` placed under `output_config`).
13
- * 3. Translate the response back to `ChatResult` — flatten the
14
- * content blocks into a single `text` string, surface usage with
15
- * cache-hit counters, and pass the raw `Message` through on `.raw`.
16
- * 4. Stream via `client.messages.stream()` and yield the framework
17
- * `StreamEvent` union — `text` deltas plus a terminal `stop`
18
- * event with usage + stop reason.
19
- *
20
- * Errors from the SDK propagate; apps that want provider-specific
21
- * recovery can `instanceof Anthropic.RateLimitError` etc. The brain
22
- * facade wraps the call site in `BrainError` only for invariants the
23
- * facade owns (e.g. "no provider configured").
24
- */
25
-
26
- import Anthropic from '@anthropic-ai/sdk'
27
- import type { AgentResult } from '../agent_result.ts'
28
- import type { AnthropicProviderConfig } from '../brain_config.ts'
29
- import { DEFAULT_MODEL } from '../brain_config.ts'
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'
37
- import type { Tool } from '../tool.ts'
38
- import type {
39
- ChatOptions,
40
- ChatResult,
41
- ChatUsage,
42
- CompactionBlock,
43
- ContentBlock,
44
- GenerateResult,
45
- MCPToolResultBlock,
46
- MCPToolUseBlock,
47
- Message,
48
- ServerTool,
49
- StreamEvent,
50
- SystemPrompt,
51
- TextBlock,
52
- ToolResultBlock,
53
- ToolUseBlock,
54
- } from '../types.ts'
55
- import type { AgentGenerateResult } from '../agent_generate_result.ts'
56
- import type { AgentStreamEvent } from '../agent_stream_event.ts'
57
- import { parseGenerated, type OutputSchema } from '../output_schema.ts'
58
- import { runToolWithRecovery } from '../tool_runner.ts'
59
-
60
- const EPHEMERAL_CACHE = { type: 'ephemeral' } as const
61
-
62
- export class AnthropicProvider implements Provider {
63
- readonly name: string
64
- private readonly client: Anthropic
65
- private readonly defaultModel: string
66
- private readonly defaultMaxTokens: number
67
- private readonly betas: readonly string[]
68
-
69
- constructor(
70
- name: string,
71
- config: AnthropicProviderConfig,
72
- options: { client?: Anthropic } = {},
73
- ) {
74
- this.name = name
75
- this.defaultModel = config.defaultModel ?? DEFAULT_MODEL
76
- this.defaultMaxTokens = config.defaultMaxTokens ?? 4096
77
- this.betas = config.betas ?? []
78
- // `client` injection point — tests pass a stub; apps that want a
79
- // pre-configured SDK instance (custom retry, fetch transport, etc.)
80
- // build their own and hand it over here.
81
- this.client =
82
- options.client ??
83
- new Anthropic({
84
- apiKey: config.apiKey,
85
- ...(config.baseUrl !== undefined ? { baseURL: config.baseUrl } : {}),
86
- })
87
- }
88
-
89
- async chat(messages: readonly Message[], options: ChatOptions = {}): Promise<ChatResult> {
90
- const params = this.buildParams(messages, options)
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))
98
- return this.toChatResult(response)
99
- }
100
-
101
- async *stream(
102
- messages: readonly Message[],
103
- options: ChatOptions = {},
104
- ): AsyncIterable<StreamEvent> {
105
- const params = this.buildParams(messages, options)
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))
112
- for await (const event of stream) {
113
- if (
114
- event.type === 'content_block_delta' &&
115
- event.delta.type === 'text_delta'
116
- ) {
117
- yield { type: 'text', delta: event.delta.text }
118
- }
119
- }
120
- const final = await stream.finalMessage()
121
- yield {
122
- type: 'stop',
123
- stopReason: final.stop_reason,
124
- usage: toUsage(final.usage),
125
- }
126
- }
127
-
128
- async countTokens(
129
- messages: readonly Message[],
130
- options: ChatOptions = {},
131
- ): Promise<number> {
132
- const base = this.buildParams(messages, options)
133
- // count_tokens only accepts a subset of MessageCreateParams; build
134
- // a focused payload that matches what apps actually need to budget.
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
- )
144
- return result.input_tokens
145
- }
146
-
147
- /**
148
- * Agentic loop. Send → detect tool_use blocks → execute → append
149
- * tool_result → re-send, until the model returns `end_turn` or
150
- * the iteration ceiling is hit.
151
- *
152
- * Tools are passed once on every call — Anthropic doesn't carry
153
- * tool state across requests; the model rediscovers them from the
154
- * `tools` array each turn. Apps that care about cache hits keep
155
- * the tool list stable across runs.
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>
167
- async runWithTools(
168
- messages: readonly Message[],
169
- tools: readonly Tool[],
170
- options: RunWithToolsOptions = {},
171
- ): Promise<AgentResult | SuspendedRun> {
172
- const maxIterations = options.maxIterations ?? 10
173
- const toolMap = new Map<string, Tool>(tools.map((t) => [t.name, t]))
174
- const workingMessages: Message[] = [...messages]
175
- const aggregated: ChatUsage = {
176
- inputTokens: 0,
177
- outputTokens: 0,
178
- cacheReadTokens: 0,
179
- cacheCreationTokens: 0,
180
- }
181
- let iterations = 0
182
- let lastStopReason: string | null = null
183
-
184
- const mcpServers = options.mcpServers ?? []
185
- const useMcpBeta = mcpServers.length > 0
186
-
187
- while (true) {
188
- checkAborted(options.signal)
189
- const params = this.buildParams(workingMessages, options) as Anthropic.MessageCreateParamsNonStreaming & {
190
- mcp_servers?: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition[]
191
- }
192
- params.tools = [
193
- // Server tools placed first when present (from buildParams).
194
- ...((params.tools ?? []) as Anthropic.ToolUnion[]),
195
- ...tools.map((t) => ({
196
- name: t.name,
197
- description: t.description,
198
- input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
199
- })),
200
- // MCP toolsets — one per declared server. The model sees the
201
- // server's tools via Anthropic's connector, not via our local
202
- // `tools` list.
203
- ...mcpServers
204
- .filter((s) => s.tools?.enabled !== false)
205
- .map((s) => ({
206
- type: 'mcp_toolset' as const,
207
- mcp_server_name: s.name,
208
- ...(s.tools?.allowedTools
209
- ? { allowed_tools: [...s.tools.allowedTools] }
210
- : {}),
211
- })),
212
- ] as unknown as Anthropic.MessageCreateParams['tools']
213
-
214
- // Declare MCP servers + flip to the beta surface when in use.
215
- // Anthropic's MCP connector requires `mcp-client-2025-11-20`.
216
- if (useMcpBeta) {
217
- params.mcp_servers = mcpServers.map((s) => {
218
- const def: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition = {
219
- type: 'url',
220
- name: s.name,
221
- url: s.url,
222
- }
223
- if (s.authorizationToken !== undefined) def.authorization_token = s.authorizationToken
224
- return def
225
- })
226
- const baseBetas = (params as { betas?: readonly string[] }).betas ?? []
227
- ;(params as { betas?: string[] }).betas = baseBetas.includes('mcp-client-2025-11-20')
228
- ? [...baseBetas]
229
- : [...baseBetas, 'mcp-client-2025-11-20']
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))
239
- addUsage(aggregated, response.usage)
240
- lastStopReason = response.stop_reason ?? null
241
-
242
- // Append the assistant turn verbatim from the SDK shape so
243
- // tool_use blocks survive to the next request unchanged.
244
- workingMessages.push({
245
- role: 'assistant',
246
- content: fromAnthropicContent(response.content),
247
- })
248
-
249
- if (response.stop_reason !== 'tool_use') {
250
- return {
251
- text: collectText(response.content),
252
- messages: workingMessages,
253
- iterations,
254
- stopReason: lastStopReason ?? 'end_turn',
255
- usage: aggregated,
256
- }
257
- }
258
-
259
- // Execute every tool_use block in the response and append the
260
- // results in a single user-role turn. The SDK's API expects all
261
- // tool_result blocks for a given assistant turn to land in the
262
- // same user message.
263
- const toolUseBlocks = response.content.filter(
264
- (b): b is Anthropic.ToolUseBlock => b.type === 'tool_use',
265
- )
266
- const resultBlocks: ContentBlock[] = []
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
- }
288
- }
289
- const { content, isError } = await runToolWithRecovery(
290
- toolMap.get(block.name),
291
- block.name,
292
- block.id,
293
- block.input,
294
- options,
295
- )
296
- const resultBlock: ToolResultBlock = {
297
- type: 'tool_result',
298
- toolUseId: block.id,
299
- content,
300
- ...(isError ? { isError: true } : {}),
301
- }
302
- resultBlocks.push(resultBlock)
303
- }
304
- workingMessages.push({ role: 'user', content: resultBlocks })
305
-
306
- iterations++
307
- if (iterations >= maxIterations) {
308
- return {
309
- text: collectText(response.content),
310
- messages: workingMessages,
311
- iterations,
312
- stopReason: 'max_iterations',
313
- usage: aggregated,
314
- }
315
- }
316
- }
317
- }
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
-
778
- async generate<T>(
779
- messages: readonly Message[],
780
- schema: OutputSchema<T>,
781
- options: ChatOptions = {},
782
- ): Promise<GenerateResult<T>> {
783
- const params = this.buildParams(messages, options) as Anthropic.MessageCreateParamsNonStreaming
784
- params.output_config = {
785
- ...(params.output_config ?? {}),
786
- format: { type: 'json_schema', schema: schema.jsonSchema },
787
- }
788
- const response = await this.client.messages.create(params, reqOpts(options))
789
- const text = collectText(response.content)
790
- const value = parseGenerated(text, schema)
791
- return {
792
- value,
793
- text,
794
- model: response.model,
795
- stopReason: response.stop_reason,
796
- usage: toUsage(response.usage),
797
- raw: response,
798
- }
799
- }
800
-
801
- // ─── Param translation ──────────────────────────────────────────────────
802
-
803
- private buildParams(
804
- messages: readonly Message[],
805
- options: ChatOptions,
806
- ): Anthropic.MessageCreateParamsNonStreaming {
807
- const model = options.model ?? this.defaultModel
808
- const params: Anthropic.MessageCreateParamsNonStreaming = {
809
- model,
810
- max_tokens: options.maxTokens ?? this.defaultMaxTokens,
811
- messages: messages.map(toMessageParam),
812
- }
813
-
814
- const system = toSystemParam(options.system)
815
- if (system !== undefined) params.system = system
816
-
817
- if (options.thinking === 'adaptive') {
818
- params.thinking = { type: 'adaptive' }
819
- } else if (options.thinking === 'disabled') {
820
- params.thinking = { type: 'disabled' }
821
- }
822
-
823
- if (options.effort !== undefined) {
824
- params.output_config = { effort: options.effort }
825
- }
826
-
827
- if (options.cache === true) {
828
- // Top-level auto-cache the last cacheable block. Maps to the
829
- // SDK's `cache_control` shorthand on the request body.
830
- ;(params as { cache_control?: { type: 'ephemeral' } }).cache_control = EPHEMERAL_CACHE
831
- }
832
-
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
- }
853
- if (betas.length > 0) {
854
- ;(params as { betas?: readonly string[] }).betas = betas
855
- }
856
-
857
- if (options.serverTools && options.serverTools.length > 0) {
858
- params.tools = anthropicServerTools(options.serverTools)
859
- }
860
-
861
- return params
862
- }
863
-
864
- private toChatResult(message: Anthropic.Message): ChatResult<Anthropic.Message> {
865
- const text = message.content
866
- .filter((b): b is Anthropic.TextBlock => b.type === 'text')
867
- .map((b) => b.text)
868
- .join('')
869
- const result: ChatResult<Anthropic.Message> = {
870
- text,
871
- model: message.model,
872
- stopReason: message.stop_reason,
873
- usage: toUsage(message.usage),
874
- raw: message,
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
886
- }
887
- }
888
-
889
- // ─── Shape converters ─────────────────────────────────────────────────────
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
-
923
- function toUsage(u: Anthropic.Usage): ChatUsage {
924
- return {
925
- inputTokens: u.input_tokens,
926
- outputTokens: u.output_tokens,
927
- cacheReadTokens: u.cache_read_input_tokens ?? 0,
928
- cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
929
- }
930
- }
931
-
932
- function toMessageParam(message: Message): Anthropic.MessageParam {
933
- if (typeof message.content === 'string') {
934
- return { role: message.role, content: message.content }
935
- }
936
- return {
937
- role: message.role,
938
- content: message.content
939
- // MCP blocks are inbound-only — Anthropic produces them, we
940
- // surface them on `result.messages` for observability, but we
941
- // never echo them back to the model. The backend tracks MCP
942
- // tool state on its side.
943
- .filter(
944
- (b): b is Exclude<ContentBlock, MCPToolUseBlock | MCPToolResultBlock> =>
945
- b.type !== 'mcp_tool_use' && b.type !== 'mcp_tool_result',
946
- )
947
- .map((block): Anthropic.ContentBlockParam => {
948
- if (block.type === 'tool_use') {
949
- return {
950
- type: 'tool_use',
951
- id: block.id,
952
- name: block.name,
953
- input: block.input as Record<string, unknown>,
954
- }
955
- }
956
- if (block.type === 'tool_result') {
957
- const param: Anthropic.ToolResultBlockParam = {
958
- type: 'tool_result',
959
- tool_use_id: block.toolUseId,
960
- content:
961
- typeof block.content === 'string'
962
- ? block.content
963
- : block.content.map(
964
- (b) => ({ type: 'text', text: b.text }) as Anthropic.TextBlockParam,
965
- ),
966
- }
967
- if (block.isError) param.is_error = true
968
- return param
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
- }
1018
- const text: Anthropic.TextBlockParam = { type: 'text', text: block.text }
1019
- if (block.cache) text.cache_control = EPHEMERAL_CACHE
1020
- return text
1021
- }),
1022
- }
1023
- }
1024
-
1025
- function toSystemParam(
1026
- system: SystemPrompt | undefined,
1027
- ): string | Anthropic.TextBlockParam[] | undefined {
1028
- if (system === undefined) return undefined
1029
- if (typeof system === 'string') return system
1030
- if (Array.isArray(system)) {
1031
- return system.map((block) => {
1032
- const param: Anthropic.TextBlockParam = { type: 'text', text: block.text }
1033
- if (block.cache) param.cache_control = EPHEMERAL_CACHE
1034
- return param
1035
- })
1036
- }
1037
- const param: Anthropic.TextBlockParam = { type: 'text', text: system.text }
1038
- if (system.cache) param.cache_control = EPHEMERAL_CACHE
1039
- return [param]
1040
- }
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
-
1097
- function mergeBetas(
1098
- providerBetas: readonly string[],
1099
- callBetas: readonly string[] | undefined,
1100
- ): readonly string[] {
1101
- if (!callBetas || callBetas.length === 0) return providerBetas
1102
- const seen = new Set<string>()
1103
- const out: string[] = []
1104
- for (const b of providerBetas) {
1105
- if (seen.has(b)) continue
1106
- seen.add(b)
1107
- out.push(b)
1108
- }
1109
- for (const b of callBetas) {
1110
- if (seen.has(b)) continue
1111
- seen.add(b)
1112
- out.push(b)
1113
- }
1114
- return out
1115
- }
1116
-
1117
- function addUsage(acc: ChatUsage, u: Anthropic.Usage): void {
1118
- acc.inputTokens += u.input_tokens
1119
- acc.outputTokens += u.output_tokens
1120
- acc.cacheReadTokens += u.cache_read_input_tokens ?? 0
1121
- acc.cacheCreationTokens += u.cache_creation_input_tokens ?? 0
1122
- }
1123
-
1124
- function collectText(content: Anthropic.ContentBlock[]): string {
1125
- return content
1126
- .filter((b): b is Anthropic.TextBlock => b.type === 'text')
1127
- .map((b) => b.text)
1128
- .join('')
1129
- }
1130
-
1131
- /**
1132
- * Translate the SDK's response content blocks back into framework
1133
- * `ContentBlock`s for storage in `workingMessages`. We preserve
1134
- * `text` and `tool_use` blocks verbatim; other server-side block
1135
- * types (thinking, server tool blocks) are dropped — V1 doesn't
1136
- * surface them, and re-sending them as part of the assistant turn
1137
- * could confuse the model.
1138
- */
1139
- function fromAnthropicContent(
1140
- content: ReadonlyArray<Anthropic.ContentBlock | { type: string; [k: string]: unknown }>,
1141
- ): ContentBlock[] {
1142
- const out: ContentBlock[] = []
1143
- for (const block of content) {
1144
- if (block.type === 'text') {
1145
- out.push({ type: 'text', text: (block as { text: string }).text } satisfies TextBlock)
1146
- } else if (block.type === 'tool_use') {
1147
- const u = block as { id: string; name: string; input: unknown }
1148
- out.push({
1149
- type: 'tool_use',
1150
- id: u.id,
1151
- name: u.name,
1152
- input: u.input,
1153
- } satisfies ToolUseBlock)
1154
- } else if (block.type === 'mcp_tool_use') {
1155
- const m = block as unknown as {
1156
- id: string
1157
- server_name: string
1158
- name: string
1159
- input: unknown
1160
- }
1161
- out.push({
1162
- type: 'mcp_tool_use',
1163
- id: m.id,
1164
- serverName: m.server_name,
1165
- name: m.name,
1166
- input: m.input,
1167
- } satisfies MCPToolUseBlock)
1168
- } else if (block.type === 'mcp_tool_result') {
1169
- const r = block as unknown as {
1170
- tool_use_id: string
1171
- content: string | Array<{ type: 'text'; text: string }>
1172
- is_error?: boolean
1173
- }
1174
- const result: MCPToolResultBlock = {
1175
- type: 'mcp_tool_result',
1176
- toolUseId: r.tool_use_id,
1177
- content:
1178
- typeof r.content === 'string'
1179
- ? r.content
1180
- : r.content.map((c) => ({ type: 'text', text: c.text }) satisfies TextBlock),
1181
- }
1182
- if (r.is_error) result.isError = true
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)
1191
- }
1192
- }
1193
- return out
1194
- }