@strands-agents/sdk 1.18.0 → 1.19.0

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 (57) hide show
  1. package/README.md +4 -1
  2. package/dist/src/agent/agent.d.ts +3 -1
  3. package/dist/src/agent/agent.d.ts.map +1 -1
  4. package/dist/src/agent/agent.js +19 -9
  5. package/dist/src/agent/agent.js.map +1 -1
  6. package/dist/src/background-tasks/background-tasks.d.ts.map +1 -1
  7. package/dist/src/background-tasks/background-tasks.js +13 -10
  8. package/dist/src/background-tasks/background-tasks.js.map +1 -1
  9. package/dist/src/context-manager/context-manager.d.ts +7 -5
  10. package/dist/src/context-manager/context-manager.d.ts.map +1 -1
  11. package/dist/src/context-manager/context-manager.js +6 -4
  12. package/dist/src/context-manager/context-manager.js.map +1 -1
  13. package/dist/src/conversation-manager/compression/context-compression.d.ts +13 -1
  14. package/dist/src/conversation-manager/compression/context-compression.d.ts.map +1 -1
  15. package/dist/src/conversation-manager/compression/context-compression.js +32 -5
  16. package/dist/src/conversation-manager/compression/context-compression.js.map +1 -1
  17. package/dist/src/interventions/registry.d.ts.map +1 -1
  18. package/dist/src/interventions/registry.js +5 -0
  19. package/dist/src/interventions/registry.js.map +1 -1
  20. package/dist/src/models/anthropic.d.ts +30 -0
  21. package/dist/src/models/anthropic.d.ts.map +1 -1
  22. package/dist/src/models/anthropic.js +338 -126
  23. package/dist/src/models/anthropic.js.map +1 -1
  24. package/dist/src/models/bedrock.d.ts +10 -0
  25. package/dist/src/models/bedrock.d.ts.map +1 -1
  26. package/dist/src/models/bedrock.js +25 -15
  27. package/dist/src/models/bedrock.js.map +1 -1
  28. package/dist/src/models/model.d.ts.map +1 -1
  29. package/dist/src/models/model.js +6 -1
  30. package/dist/src/models/model.js.map +1 -1
  31. package/dist/src/retry/model-retry-strategy.js +16 -3
  32. package/dist/src/retry/model-retry-strategy.js.map +1 -1
  33. package/dist/src/sandbox/stream-process.d.ts.map +1 -1
  34. package/dist/src/sandbox/stream-process.js +5 -0
  35. package/dist/src/sandbox/stream-process.js.map +1 -1
  36. package/dist/src/tsconfig.tsbuildinfo +1 -1
  37. package/dist/src/types/agent.d.ts +7 -1
  38. package/dist/src/types/agent.d.ts.map +1 -1
  39. package/dist/src/types/agent.js +6 -0
  40. package/dist/src/types/agent.js.map +1 -1
  41. package/dist/src/vended-tools/handoff-to-user/handoff-to-user.d.ts +28 -0
  42. package/dist/src/vended-tools/handoff-to-user/handoff-to-user.d.ts.map +1 -0
  43. package/dist/src/vended-tools/handoff-to-user/handoff-to-user.js +45 -0
  44. package/dist/src/vended-tools/handoff-to-user/handoff-to-user.js.map +1 -0
  45. package/dist/src/vended-tools/handoff-to-user/index.d.ts +8 -0
  46. package/dist/src/vended-tools/handoff-to-user/index.d.ts.map +1 -0
  47. package/dist/src/vended-tools/handoff-to-user/index.js +6 -0
  48. package/dist/src/vended-tools/handoff-to-user/index.js.map +1 -0
  49. package/dist/src/vended-tools/handoff-to-user/types.d.ts +18 -0
  50. package/dist/src/vended-tools/handoff-to-user/types.d.ts.map +1 -0
  51. package/dist/src/vended-tools/handoff-to-user/types.js +11 -0
  52. package/dist/src/vended-tools/handoff-to-user/types.js.map +1 -0
  53. package/dist/src/vended-tools/index.d.ts +10 -1
  54. package/dist/src/vended-tools/index.d.ts.map +1 -1
  55. package/dist/src/vended-tools/index.js +10 -1
  56. package/dist/src/vended-tools/index.js.map +1 -1
  57. package/package.json +8 -4
@@ -1,7 +1,7 @@
1
1
  import Anthropic, {} from '@anthropic-ai/sdk';
2
2
  import { Model, resolveConfigMetadata, resolveCacheSection, } from '../models/model.js';
3
3
  import { createEmptyUsage } from '../models/streaming.js';
4
- import { ContextWindowOverflowError, ModelThrottledError, normalizeError } from '../errors.js';
4
+ import { ContextWindowOverflowError, ModelError, ModelThrottledError, normalizeError } from '../errors.js';
5
5
  import { encodeBase64 } from '../types/media.js';
6
6
  import { logger } from '../logging/logger.js';
7
7
  import { warnOnce } from '../logging/warn-once.js';
@@ -17,11 +17,52 @@ const CONTEXT_WINDOW_OVERFLOW_ERRORS = [
17
17
  'input length exceeds context window',
18
18
  'input and output tokens exceed your context limit',
19
19
  ];
20
+ // Content blocks for tools Anthropic executes server side. They have no toolUse/toolResult equivalent
21
+ // (the agent never runs them), so they are not streamed.
22
+ const SERVER_TOOL_BLOCK_TYPES = new Set([
23
+ 'bash_code_execution_tool_result',
24
+ 'code_execution_tool_result',
25
+ 'container_upload',
26
+ 'mcp_tool_result',
27
+ 'mcp_tool_use',
28
+ 'server_tool_use',
29
+ 'text_editor_code_execution_tool_result',
30
+ 'tool_search_tool_result',
31
+ 'web_fetch_tool_result',
32
+ 'web_search_tool_result',
33
+ ]);
34
+ // Anthropic pauses a long server-side tool turn with stop_reason=pause_turn and expects the paused
35
+ // assistant message to be sent back as-is to resume it. Bounds how many times stream() does so.
36
+ const MAX_PAUSE_TURN_CONTINUATIONS = 10;
20
37
  /**
21
38
  * `ephemeral` is the only cache type the Anthropic API supports.
22
39
  */
23
40
  const ANTHROPIC_CACHE_TYPE = 'ephemeral';
24
41
  const TEXT_FILE_FORMATS = ['txt', 'md', 'markdown', 'csv', 'json', 'xml', 'html', 'yml', 'yaml', 'js', 'ts', 'py'];
42
+ /**
43
+ * Validates that `anthropicTools` does not contain function tool definitions.
44
+ *
45
+ * @param anthropicTools - The configured server-side tools
46
+ * @throws Error - When an entry carries an `input_schema`
47
+ */
48
+ function validateAnthropicTools(anthropicTools) {
49
+ for (const tool of anthropicTools) {
50
+ if ('input_schema' in tool) {
51
+ throw new Error('anthropicTools should not contain function tool definitions. Use the standard tools interface for ' +
52
+ 'function calling tools. anthropicTools is reserved for Anthropic server-side tools like web_search, ' +
53
+ 'web_fetch, and code_execution.');
54
+ }
55
+ }
56
+ }
57
+ /**
58
+ * Copies the numeric fields of an Anthropic usage object; `null` fields leave the existing value in place.
59
+ */
60
+ function mergeUsage(target, source) {
61
+ for (const [key, value] of Object.entries(source)) {
62
+ if (typeof value === 'number')
63
+ target[key] = value;
64
+ }
65
+ }
25
66
  export class AnthropicModel extends Model {
26
67
  _config;
27
68
  _client;
@@ -33,6 +74,8 @@ export class AnthropicModel extends Model {
33
74
  maxTokens: MODEL_DEFAULTS.anthropic.maxTokens,
34
75
  ...modelConfig,
35
76
  };
77
+ if (modelConfig.anthropicTools)
78
+ validateAnthropicTools(modelConfig.anthropicTools);
36
79
  if (modelConfig.modelId === undefined) {
37
80
  warnOnce(logger, defaultModelWarningMessage(MODEL_DEFAULTS.anthropic.modelId));
38
81
  }
@@ -54,6 +97,8 @@ export class AnthropicModel extends Model {
54
97
  }
55
98
  }
56
99
  updateConfig(modelConfig) {
100
+ if (modelConfig.anthropicTools)
101
+ validateAnthropicTools(modelConfig.anthropicTools);
57
102
  this._config = { ...this._config, ...modelConfig };
58
103
  }
59
104
  getConfig() {
@@ -93,125 +138,181 @@ export class AnthropicModel extends Model {
93
138
  return super.countTokens(messages, options);
94
139
  }
95
140
  }
141
+ /**
142
+ * Streams a conversation with the Anthropic model.
143
+ *
144
+ * A server-side tool turn that Anthropic pauses is resumed with follow-up requests inside this call, so
145
+ * an error thrown by one of those requests can surface after earlier events were already yielded.
146
+ *
147
+ * @param messages - Array of conversation messages
148
+ * @param options - Optional streaming configuration
149
+ * @returns Async iterable of streaming events
150
+ */
96
151
  async *stream(messages, options) {
97
152
  try {
98
- const request = this._formatRequest(messages, options);
153
+ let request = this._formatRequest(messages, options);
99
154
  const requestOptions = this._buildRequestOptions();
100
- const stream = requestOptions
101
- ? this._client.messages.stream(request, requestOptions)
102
- : this._client.messages.stream(request);
103
155
  const usage = createEmptyUsage();
104
- let stopReason = 'endTurn';
105
- for await (const event of stream) {
106
- switch (event.type) {
107
- case 'message_start': {
108
- usage.inputTokens = event.message.usage.input_tokens;
109
- const rawUsage = event.message.usage;
110
- if (rawUsage.cache_creation_input_tokens !== undefined) {
111
- usage.cacheWriteInputTokens = rawUsage.cache_creation_input_tokens;
112
- }
113
- if (rawUsage.cache_read_input_tokens !== undefined) {
114
- usage.cacheReadInputTokens = rawUsage.cache_read_input_tokens;
115
- }
116
- yield {
117
- type: 'modelMessageStartEvent',
118
- role: event.message.role,
119
- };
120
- break;
121
- }
122
- case 'content_block_start':
123
- if (event.content_block.type === 'tool_use') {
124
- yield {
125
- type: 'modelContentBlockStartEvent',
126
- start: {
127
- type: 'toolUseStart',
128
- name: event.content_block.name,
129
- toolUseId: event.content_block.id,
130
- },
131
- };
156
+ let continuations = 0;
157
+ while (true) {
158
+ const stream = requestOptions
159
+ ? this._client.messages.stream(request, requestOptions)
160
+ : this._client.messages.stream(request);
161
+ let stopReason = 'endTurn';
162
+ let messageStopped = false;
163
+ const responseUsage = {};
164
+ const serverToolBlockIndexes = new Set();
165
+ for await (const event of stream) {
166
+ switch (event.type) {
167
+ case 'message_start': {
168
+ mergeUsage(responseUsage, event.message.usage);
169
+ if (continuations === 0) {
170
+ yield {
171
+ type: 'modelMessageStartEvent',
172
+ role: event.message.role,
173
+ };
174
+ }
175
+ break;
132
176
  }
133
- else if (event.content_block.type === 'thinking') {
134
- yield { type: 'modelContentBlockStartEvent' };
135
- if (event.content_block.thinking) {
177
+ case 'content_block_start':
178
+ if (SERVER_TOOL_BLOCK_TYPES.has(event.content_block.type)) {
179
+ serverToolBlockIndexes.add(event.index);
180
+ this._logServerToolBlock(event.content_block);
181
+ break;
182
+ }
183
+ if (event.content_block.type === 'tool_use') {
184
+ yield {
185
+ type: 'modelContentBlockStartEvent',
186
+ start: {
187
+ type: 'toolUseStart',
188
+ name: event.content_block.name,
189
+ toolUseId: event.content_block.id,
190
+ },
191
+ };
192
+ }
193
+ else if (event.content_block.type === 'thinking') {
194
+ yield { type: 'modelContentBlockStartEvent' };
195
+ if (event.content_block.thinking) {
196
+ yield {
197
+ type: 'modelContentBlockDeltaEvent',
198
+ delta: {
199
+ type: 'reasoningContentDelta',
200
+ text: event.content_block.thinking,
201
+ signature: event.content_block.signature,
202
+ },
203
+ };
204
+ }
205
+ }
206
+ else if (event.content_block.type === 'redacted_thinking') {
207
+ yield { type: 'modelContentBlockStartEvent' };
136
208
  yield {
137
209
  type: 'modelContentBlockDeltaEvent',
138
210
  delta: {
139
211
  type: 'reasoningContentDelta',
140
- text: event.content_block.thinking,
141
- signature: event.content_block.signature,
212
+ redactedContent: event.content_block.data,
142
213
  },
143
214
  };
144
215
  }
145
- }
146
- else if (event.content_block.type === 'redacted_thinking') {
147
- yield { type: 'modelContentBlockStartEvent' };
148
- yield {
149
- type: 'modelContentBlockDeltaEvent',
150
- delta: {
151
- type: 'reasoningContentDelta',
152
- redactedContent: event.content_block.data,
153
- },
154
- };
155
- }
156
- else {
157
- yield { type: 'modelContentBlockStartEvent' };
158
- if (event.content_block.type === 'text' && event.content_block.text) {
216
+ else {
217
+ yield { type: 'modelContentBlockStartEvent' };
218
+ if (event.content_block.type === 'text' && event.content_block.text) {
219
+ yield {
220
+ type: 'modelContentBlockDeltaEvent',
221
+ delta: { type: 'textDelta', text: event.content_block.text },
222
+ };
223
+ }
224
+ }
225
+ break;
226
+ case 'content_block_delta':
227
+ if (serverToolBlockIndexes.has(event.index))
228
+ break;
229
+ if (event.delta.type === 'text_delta') {
159
230
  yield {
160
231
  type: 'modelContentBlockDeltaEvent',
161
- delta: { type: 'textDelta', text: event.content_block.text },
232
+ delta: { type: 'textDelta', text: event.delta.text },
162
233
  };
163
234
  }
164
- }
165
- break;
166
- case 'content_block_delta':
167
- if (event.delta.type === 'text_delta') {
168
- yield {
169
- type: 'modelContentBlockDeltaEvent',
170
- delta: { type: 'textDelta', text: event.delta.text },
171
- };
172
- }
173
- else if (event.delta.type === 'input_json_delta') {
174
- yield {
175
- type: 'modelContentBlockDeltaEvent',
176
- delta: { type: 'toolUseInputDelta', input: event.delta.partial_json },
177
- };
178
- }
179
- else if (event.delta.type === 'thinking_delta') {
180
- yield {
181
- type: 'modelContentBlockDeltaEvent',
182
- delta: { type: 'reasoningContentDelta', text: event.delta.thinking },
183
- };
184
- }
185
- else if (event.delta.type === 'signature_delta') {
186
- yield {
187
- type: 'modelContentBlockDeltaEvent',
188
- delta: { type: 'reasoningContentDelta', signature: event.delta.signature },
189
- };
190
- }
191
- break;
192
- case 'content_block_stop':
193
- yield { type: 'modelContentBlockStopEvent' };
194
- break;
195
- case 'message_delta':
196
- if (event.usage) {
197
- usage.outputTokens = event.usage.output_tokens;
198
- }
199
- if (event.delta.stop_reason) {
200
- stopReason = this._mapStopReason(event.delta.stop_reason);
201
- }
202
- break;
203
- case 'message_stop':
204
- usage.totalTokens = usage.inputTokens + usage.outputTokens;
205
- yield {
206
- type: 'modelMetadataEvent',
207
- usage,
208
- };
209
- yield {
210
- type: 'modelMessageStopEvent',
211
- stopReason,
212
- };
213
- break;
235
+ else if (event.delta.type === 'citations_delta') {
236
+ const citation = this._formatCitation(event.delta.citation);
237
+ if (citation) {
238
+ yield {
239
+ type: 'modelContentBlockDeltaEvent',
240
+ delta: { type: 'citationsDelta', citations: [citation], content: [] },
241
+ };
242
+ }
243
+ }
244
+ else if (event.delta.type === 'input_json_delta') {
245
+ yield {
246
+ type: 'modelContentBlockDeltaEvent',
247
+ delta: { type: 'toolUseInputDelta', input: event.delta.partial_json },
248
+ };
249
+ }
250
+ else if (event.delta.type === 'thinking_delta') {
251
+ yield {
252
+ type: 'modelContentBlockDeltaEvent',
253
+ delta: { type: 'reasoningContentDelta', text: event.delta.thinking },
254
+ };
255
+ }
256
+ else if (event.delta.type === 'signature_delta') {
257
+ yield {
258
+ type: 'modelContentBlockDeltaEvent',
259
+ delta: { type: 'reasoningContentDelta', signature: event.delta.signature },
260
+ };
261
+ }
262
+ break;
263
+ case 'content_block_stop':
264
+ if (serverToolBlockIndexes.delete(event.index))
265
+ break;
266
+ yield { type: 'modelContentBlockStopEvent' };
267
+ break;
268
+ case 'message_delta':
269
+ if (event.usage) {
270
+ // Cumulative within one response; fields Anthropic omits keep their message_start value.
271
+ mergeUsage(responseUsage, event.usage);
272
+ }
273
+ if (event.delta.stop_reason) {
274
+ stopReason = this._mapStopReason(event.delta.stop_reason);
275
+ }
276
+ break;
277
+ case 'message_stop':
278
+ messageStopped = true;
279
+ break;
280
+ }
281
+ }
282
+ // A stream that ends without message_stop is incomplete; emit no stop event so the caller can tell.
283
+ if (!messageStopped)
284
+ return;
285
+ usage.inputTokens += responseUsage.input_tokens ?? 0;
286
+ usage.outputTokens += responseUsage.output_tokens ?? 0;
287
+ if (responseUsage.cache_creation_input_tokens !== undefined) {
288
+ usage.cacheWriteInputTokens = (usage.cacheWriteInputTokens ?? 0) + responseUsage.cache_creation_input_tokens;
289
+ }
290
+ if (responseUsage.cache_read_input_tokens !== undefined) {
291
+ usage.cacheReadInputTokens = (usage.cacheReadInputTokens ?? 0) + responseUsage.cache_read_input_tokens;
292
+ }
293
+ if (stopReason === 'pauseTurn') {
294
+ if (continuations >= MAX_PAUSE_TURN_CONTINUATIONS) {
295
+ throw new ModelError(`server-side tool turn did not complete after ${continuations} continuations`);
296
+ }
297
+ continuations++;
298
+ const pausedMessage = await stream.finalMessage();
299
+ request = {
300
+ ...request,
301
+ messages: [...request.messages, { role: 'assistant', content: pausedMessage.content }],
302
+ };
303
+ logger.debug(`continuation=<${continuations}> | resuming paused server-side tool turn`);
304
+ continue;
214
305
  }
306
+ usage.totalTokens = usage.inputTokens + usage.outputTokens;
307
+ yield {
308
+ type: 'modelMetadataEvent',
309
+ usage,
310
+ };
311
+ yield {
312
+ type: 'modelMessageStopEvent',
313
+ stopReason,
314
+ };
315
+ return;
215
316
  }
216
317
  }
217
318
  catch (unknownError) {
@@ -342,29 +443,38 @@ export class AnthropicModel extends Model {
342
443
  if (system !== undefined)
343
444
  request.system = system;
344
445
  }
345
- if (options?.toolSpecs?.length) {
346
- const tools = options.toolSpecs.map((tool) => ({
347
- name: tool.name,
348
- description: tool.description,
349
- input_schema: tool.inputSchema,
350
- }));
351
- // A cache_control on the last tool caches all of them, so one cache point suffices.
352
- const toolsCache = this._cacheSection('toolsTTL');
353
- const lastTool = tools[tools.length - 1];
354
- if (toolsCache.enabled && lastTool) {
355
- lastTool.cache_control = this._formatCacheControl(toolsCache.ttl);
446
+ const tools = (options?.toolSpecs ?? []).map((tool) => ({
447
+ name: tool.name,
448
+ description: tool.description,
449
+ input_schema: tool.inputSchema,
450
+ }));
451
+ const serverTools = [
452
+ ...(this._config.anthropicTools ?? []),
453
+ ...(this._config.params?.tools ?? []),
454
+ ];
455
+ // Forcing a tool means this turn must call a function tool, so server tools are left out.
456
+ if (!options?.toolChoice || 'auto' in options.toolChoice) {
457
+ // Copied so the cache_control below never lands on the caller's config.
458
+ tools.push(...serverTools.map((tool) => ({ ...tool })));
459
+ }
460
+ else if (serverTools.length > 0) {
461
+ logger.warn(`tool_choice=<${Object.keys(options.toolChoice)[0]}>, server_tools=<${serverTools.map((tool) => tool.name).join(',')}> | forced tool call, omitting server tools`);
462
+ }
463
+ // A cache_control on the last tool caches all of them, so one cache point suffices.
464
+ const toolsCache = this._cacheSection('toolsTTL');
465
+ const lastTool = tools[tools.length - 1];
466
+ if (toolsCache.enabled && lastTool) {
467
+ lastTool.cache_control = this._formatCacheControl(toolsCache.ttl);
468
+ }
469
+ if (tools.length > 0 && options?.toolChoice) {
470
+ if ('auto' in options.toolChoice) {
471
+ request.tool_choice = { type: 'auto' };
356
472
  }
357
- request.tools = tools;
358
- if (options.toolChoice) {
359
- if ('auto' in options.toolChoice) {
360
- request.tool_choice = { type: 'auto' };
361
- }
362
- else if ('any' in options.toolChoice) {
363
- request.tool_choice = { type: 'any' };
364
- }
365
- else if ('tool' in options.toolChoice) {
366
- request.tool_choice = { type: 'tool', name: options.toolChoice.tool.name };
367
- }
473
+ else if ('any' in options.toolChoice) {
474
+ request.tool_choice = { type: 'any' };
475
+ }
476
+ else if ('tool' in options.toolChoice) {
477
+ request.tool_choice = { type: 'tool', name: options.toolChoice.tool.name };
368
478
  }
369
479
  }
370
480
  if (this._config.temperature !== undefined)
@@ -375,6 +485,11 @@ export class AnthropicModel extends Model {
375
485
  request.stop_sequences = this._config.stopSequences;
376
486
  if (this._config.params)
377
487
  Object.assign(request, this._config.params);
488
+ // params.tools is already merged into tools above; never let it bypass the forced-turn rule.
489
+ if (tools.length > 0)
490
+ request.tools = tools;
491
+ else
492
+ delete request.tools;
378
493
  return request;
379
494
  }
380
495
  _formatMessages(messages, messagesCache = { enabled: false }, cacheTargetMessage = -1, dynamicTrailingBlocks = 0) {
@@ -433,7 +548,8 @@ export class AnthropicModel extends Model {
433
548
  logger.warn(`count=<${strippedCachePoints}> | stripped extra cache points, cacheConfig keeps the first cache ` +
434
549
  `point in the last user message; unset cacheConfig to keep every cache point`);
435
550
  }
436
- return formatted;
551
+ // The API rejects an empty message anywhere but the trailing assistant turn.
552
+ return formatted.filter((msg) => msg.content.length > 0);
437
553
  }
438
554
  _isCacheableBlock(block) {
439
555
  return ['text', 'image', 'tool_use', 'tool_result', 'document'].includes(block.type);
@@ -574,12 +690,108 @@ export class AnthropicModel extends Model {
574
690
  };
575
691
  }
576
692
  return undefined;
693
+ case 'citationsBlock': {
694
+ const text = block.content.map((generated) => generated.text).join('');
695
+ return text ? { type: 'text', text } : undefined;
696
+ }
577
697
  case 'cachePointBlock':
578
698
  return undefined;
579
699
  default:
580
700
  return undefined;
581
701
  }
582
702
  }
703
+ /**
704
+ * Maps an Anthropic citation onto a Strands citation.
705
+ *
706
+ * @param citation - Citation from a `citations_delta` event
707
+ * @returns The mapped citation, or `undefined` when the location type has no Strands equivalent
708
+ */
709
+ _formatCitation(citation) {
710
+ const citedText = 'cited_text' in citation && citation.cited_text ? [{ text: citation.cited_text }] : [];
711
+ switch (citation.type) {
712
+ case 'web_search_result_location': {
713
+ const url = citation.url ?? '';
714
+ let domain;
715
+ try {
716
+ domain = new URL(url).hostname;
717
+ }
718
+ catch {
719
+ domain = undefined;
720
+ }
721
+ return {
722
+ location: { type: 'web', url, ...(domain ? { domain } : {}) },
723
+ source: url,
724
+ sourceContent: citedText,
725
+ title: citation.title ?? '',
726
+ };
727
+ }
728
+ case 'search_result_location':
729
+ return {
730
+ location: {
731
+ type: 'searchResult',
732
+ searchResultIndex: citation.search_result_index,
733
+ start: citation.start_block_index,
734
+ end: citation.end_block_index,
735
+ },
736
+ source: citation.source,
737
+ sourceContent: citedText,
738
+ title: citation.title ?? '',
739
+ };
740
+ case 'char_location':
741
+ return {
742
+ location: {
743
+ type: 'documentChar',
744
+ documentIndex: citation.document_index,
745
+ start: citation.start_char_index,
746
+ end: citation.end_char_index,
747
+ },
748
+ source: citation.file_id ?? '',
749
+ sourceContent: citedText,
750
+ title: citation.document_title ?? '',
751
+ };
752
+ case 'page_location':
753
+ return {
754
+ location: {
755
+ type: 'documentPage',
756
+ documentIndex: citation.document_index,
757
+ start: citation.start_page_number,
758
+ end: citation.end_page_number,
759
+ },
760
+ source: citation.file_id ?? '',
761
+ sourceContent: citedText,
762
+ title: citation.document_title ?? '',
763
+ };
764
+ case 'content_block_location':
765
+ return {
766
+ location: {
767
+ type: 'documentChunk',
768
+ documentIndex: citation.document_index,
769
+ start: citation.start_block_index,
770
+ end: citation.end_block_index,
771
+ },
772
+ source: citation.file_id ?? '',
773
+ sourceContent: citedText,
774
+ title: citation.document_title ?? '',
775
+ };
776
+ default:
777
+ logger.warn(`citation_type=<${citation.type}> | unsupported citation location | skipping`);
778
+ return undefined;
779
+ }
780
+ }
781
+ /**
782
+ * Logs a skipped server-side tool block, at warn level when the tool returned an error.
783
+ *
784
+ * @param contentBlock - The `content_block` of a `content_block_start` event
785
+ */
786
+ _logServerToolBlock(contentBlock) {
787
+ const errorCode = contentBlock.content?.error_code;
788
+ if (errorCode !== undefined) {
789
+ logger.warn(`block_type=<${contentBlock.type}>, error_code=<${errorCode}> | server-side tool failed`);
790
+ }
791
+ else {
792
+ logger.debug(`block_type=<${contentBlock.type}> | skipping server-side tool block`);
793
+ }
794
+ }
583
795
  _mapStopReason(anthropicReason) {
584
796
  switch (anthropicReason) {
585
797
  case 'end_turn':