@librechat/agents 3.1.78-dev.0 → 3.1.79

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 (53) hide show
  1. package/dist/cjs/graphs/Graph.cjs +7 -0
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/llm/anthropic/index.cjs +44 -55
  4. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  5. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +33 -21
  6. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  7. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +0 -4
  8. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  9. package/dist/cjs/messages/anthropicToolCache.cjs +48 -15
  10. package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -1
  11. package/dist/cjs/messages/format.cjs +97 -14
  12. package/dist/cjs/messages/format.cjs.map +1 -1
  13. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +14 -16
  14. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  15. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +30 -0
  16. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  17. package/dist/esm/graphs/Graph.mjs +7 -0
  18. package/dist/esm/graphs/Graph.mjs.map +1 -1
  19. package/dist/esm/llm/anthropic/index.mjs +43 -54
  20. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  21. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +33 -21
  22. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  23. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +0 -4
  24. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  25. package/dist/esm/messages/anthropicToolCache.mjs +48 -15
  26. package/dist/esm/messages/anthropicToolCache.mjs.map +1 -1
  27. package/dist/esm/messages/format.mjs +97 -14
  28. package/dist/esm/messages/format.mjs.map +1 -1
  29. package/dist/esm/tools/local/LocalExecutionEngine.mjs +14 -16
  30. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  31. package/dist/esm/tools/subagent/SubagentExecutor.mjs +30 -0
  32. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  33. package/dist/types/llm/anthropic/index.d.ts +1 -9
  34. package/dist/types/messages/anthropicToolCache.d.ts +5 -5
  35. package/dist/types/tools/subagent/SubagentExecutor.d.ts +29 -0
  36. package/package.json +1 -1
  37. package/src/graphs/Graph.ts +9 -0
  38. package/src/llm/anthropic/index.ts +55 -64
  39. package/src/llm/anthropic/llm.spec.ts +585 -0
  40. package/src/llm/anthropic/utils/message_inputs.ts +36 -21
  41. package/src/llm/anthropic/utils/message_outputs.ts +0 -4
  42. package/src/llm/anthropic/utils/server-tool-inputs.test.ts +95 -13
  43. package/src/messages/__tests__/anthropicToolCache.test.ts +46 -0
  44. package/src/messages/anthropicToolCache.ts +70 -25
  45. package/src/messages/format.ts +117 -18
  46. package/src/messages/formatAgentMessages.test.ts +202 -1
  47. package/src/scripts/subagent-configurable-inheritance.ts +252 -0
  48. package/src/specs/summarization.test.ts +3 -3
  49. package/src/tools/__tests__/LocalExecutionRoots.test.ts +8 -0
  50. package/src/tools/__tests__/SubagentExecutor.test.ts +148 -0
  51. package/src/tools/local/LocalExecutionEngine.ts +55 -54
  52. package/src/tools/subagent/SubagentExecutor.ts +60 -0
  53. package/src/types/diff.d.ts +15 -0
@@ -47,6 +47,8 @@ type GoogleFunctionCallBlock = MessageContentComplex & {
47
47
  };
48
48
  };
49
49
 
50
+ const ANTHROPIC_EMPTY_TEXT_PLACEHOLDER = '_';
51
+
50
52
  function _formatImage(imageUrl: string) {
51
53
  const parsed = parseBase64DataUrl({ dataUrl: imageUrl });
52
54
  if (parsed) {
@@ -126,13 +128,15 @@ function _ensureMessageContents(
126
128
  );
127
129
  }
128
130
  } else {
131
+ const toolMessageContent = (
132
+ message as { content?: BaseMessage['content'] | null }
133
+ ).content;
129
134
  updatedMsgs.push(
130
135
  new HumanMessage({
131
136
  content: [
132
137
  {
133
138
  type: 'tool_result',
134
- // rare case: message.content could be undefined
135
- ...(message.content != null
139
+ ...(toolMessageContent != null
136
140
  ? { content: _formatContent(message) }
137
141
  : {}),
138
142
  tool_use_id: (message as ToolMessage).tool_call_id,
@@ -486,7 +490,7 @@ function _formatContent(message: BaseMessage) {
486
490
  return {
487
491
  type: 'image' as const, // Explicitly setting the type as "image"
488
492
  source,
489
- ...(cacheControl ? { cache_control: cacheControl } : {}),
493
+ ...(cacheControl != null ? { cache_control: cacheControl } : {}),
490
494
  };
491
495
  } else if (isAnthropicImageBlockParam(contentPart)) {
492
496
  return contentPart;
@@ -494,7 +498,7 @@ function _formatContent(message: BaseMessage) {
494
498
  // PDF
495
499
  return {
496
500
  ...contentPart,
497
- ...(cacheControl ? { cache_control: cacheControl } : {}),
501
+ ...(cacheControl != null ? { cache_control: cacheControl } : {}),
498
502
  };
499
503
  } else if (contentPart.type === 'thinking') {
500
504
  const thinkingPart = contentPart as AnthropicThinkingBlockParam;
@@ -502,7 +506,7 @@ function _formatContent(message: BaseMessage) {
502
506
  type: 'thinking' as const, // Explicitly setting the type as "thinking"
503
507
  thinking: thinkingPart.thinking,
504
508
  signature: thinkingPart.signature,
505
- ...(cacheControl ? { cache_control: cacheControl } : {}),
509
+ ...(cacheControl != null ? { cache_control: cacheControl } : {}),
506
510
  };
507
511
  return block;
508
512
  } else if (contentPart.type === 'redacted_thinking') {
@@ -510,7 +514,7 @@ function _formatContent(message: BaseMessage) {
510
514
  const block: AnthropicRedactedThinkingBlockParam = {
511
515
  type: 'redacted_thinking' as const, // Explicitly setting the type as "redacted_thinking"
512
516
  data: redactedPart.data,
513
- ...(cacheControl ? { cache_control: cacheControl } : {}),
517
+ ...(cacheControl != null ? { cache_control: cacheControl } : {}),
514
518
  };
515
519
  return block;
516
520
  } else if (contentPart.type === 'search_result') {
@@ -519,10 +523,11 @@ function _formatContent(message: BaseMessage) {
519
523
  type: 'search_result' as const,
520
524
  title: searchResultPart.title,
521
525
  source: searchResultPart.source,
522
- ...('cache_control' in contentPart && contentPart.cache_control
526
+ ...('cache_control' in contentPart &&
527
+ contentPart.cache_control != null
523
528
  ? { cache_control: contentPart.cache_control }
524
529
  : {}),
525
- ...('citations' in contentPart && contentPart.citations
530
+ ...('citations' in contentPart && contentPart.citations != null
526
531
  ? { citations: contentPart.citations }
527
532
  : {}),
528
533
  content: searchResultPart.content,
@@ -533,23 +538,23 @@ function _formatContent(message: BaseMessage) {
533
538
  const block: AnthropicCompactionBlockParam = {
534
539
  type: 'compaction' as const,
535
540
  content: compactionPart.content,
536
- ...(cacheControl ? { cache_control: cacheControl } : {}),
541
+ ...(cacheControl != null ? { cache_control: cacheControl } : {}),
537
542
  };
538
543
  return block;
539
544
  } else if (
540
- textTypes.find((t) => t === contentPart.type) &&
545
+ textTypes.some((t) => t === contentPart.type) &&
541
546
  'text' in contentPart
542
547
  ) {
543
548
  // Assuming contentPart is of type MessageContentText here
544
549
  return {
545
550
  type: 'text' as const, // Explicitly setting the type as "text"
546
551
  text: contentPart.text,
547
- ...(cacheControl ? { cache_control: cacheControl } : {}),
548
- ...('citations' in contentPart && contentPart.citations
552
+ ...(cacheControl != null ? { cache_control: cacheControl } : {}),
553
+ ...('citations' in contentPart && contentPart.citations != null
549
554
  ? { citations: contentPart.citations }
550
555
  : {}),
551
556
  };
552
- } else if (toolTypes.find((t) => t === contentPart.type)) {
557
+ } else if (toolTypes.some((t) => t === contentPart.type)) {
553
558
  const contentPartCopy = { ...contentPart };
554
559
  if ('index' in contentPartCopy) {
555
560
  // Anthropic does not support passing the index field here, so we remove it.
@@ -593,12 +598,12 @@ function _formatContent(message: BaseMessage) {
593
598
  // TODO: Fix when SDK types are fixed
594
599
  return {
595
600
  ...contentPartCopy,
596
- ...(cacheControl ? { cache_control: cacheControl } : {}),
601
+ ...(cacheControl != null ? { cache_control: cacheControl } : {}),
597
602
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
598
603
  } as any;
599
604
  } else if (
600
605
  'functionCall' in contentPart &&
601
- contentPart.functionCall &&
606
+ contentPart.functionCall != null &&
602
607
  typeof contentPart.functionCall === 'object' &&
603
608
  isAIMessage(message)
604
609
  ) {
@@ -626,7 +631,7 @@ function _formatContent(message: BaseMessage) {
626
631
  throw new Error('Unsupported message content format');
627
632
  }
628
633
  });
629
- return contentBlocks.filter(
634
+ const filteredContentBlocks = contentBlocks.filter(
630
635
  (block) =>
631
636
  block !== null &&
632
637
  !(
@@ -636,6 +641,9 @@ function _formatContent(message: BaseMessage) {
636
641
  block.text.trim() === ''
637
642
  )
638
643
  );
644
+ return filteredContentBlocks.length > 0
645
+ ? filteredContentBlocks
646
+ : [{ type: 'text' as const, text: ANTHROPIC_EMPTY_TEXT_PLACEHOLDER }];
639
647
  }
640
648
  }
641
649
 
@@ -670,9 +678,11 @@ export function _convertMessagesToAnthropicPayload(
670
678
  } else {
671
679
  throw new Error(`Message type "${message._getType()}" is not supported.`);
672
680
  }
673
- if (isAIMessage(message) && !!message.tool_calls?.length) {
681
+ const isAI = isAIMessage(message);
682
+ const toolCalls = isAI ? (message.tool_calls ?? []) : [];
683
+ if (isAI && toolCalls.length > 0) {
674
684
  if (typeof message.content === 'string') {
675
- const clientToolCalls = message.tool_calls.filter(
685
+ const clientToolCalls = toolCalls.filter(
676
686
  (tc) =>
677
687
  !(
678
688
  tc.id?.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX) ?? false
@@ -684,7 +694,12 @@ export function _convertMessagesToAnthropicPayload(
684
694
  content:
685
695
  clientToolCalls.length > 0
686
696
  ? clientToolCalls.map(_convertLangChainToolCallToAnthropic)
687
- : [{ type: 'text' as const, text: ' ' }],
697
+ : [
698
+ {
699
+ type: 'text' as const,
700
+ text: ANTHROPIC_EMPTY_TEXT_PLACEHOLDER,
701
+ },
702
+ ],
688
703
  };
689
704
  } else {
690
705
  return {
@@ -697,7 +712,7 @@ export function _convertMessagesToAnthropicPayload(
697
712
  }
698
713
  } else {
699
714
  const { content } = message;
700
- const hasMismatchedToolCalls = !message.tool_calls.every(
715
+ const hasMismatchedToolCalls = !toolCalls.every(
701
716
  (toolCall) =>
702
717
  !!content.find(
703
718
  (contentPart) =>
@@ -731,7 +746,7 @@ export function _convertMessagesToAnthropicPayload(
731
746
  }
732
747
 
733
748
  function mergeMessages(messages: AnthropicMessageCreateParams['messages']) {
734
- if (!messages || messages.length <= 1) {
749
+ if (messages.length <= 1) {
735
750
  return messages;
736
751
  }
737
752
 
@@ -98,10 +98,6 @@ export function _makeMessageChunkFromAnthropicEvent(
98
98
  input_tokens: 0,
99
99
  output_tokens: data.usage.output_tokens,
100
100
  total_tokens: data.usage.output_tokens,
101
- input_token_details: {
102
- cache_creation: data.usage.cache_creation_input_tokens ?? undefined,
103
- cache_read: data.usage.cache_read_input_tokens ?? undefined,
104
- },
105
101
  };
106
102
  return {
107
103
  chunk: new AIMessageChunk({
@@ -3,6 +3,14 @@ import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
3
3
  import type { BaseMessage } from '@langchain/core/messages';
4
4
  import { _convertMessagesToAnthropicPayload } from './message_inputs';
5
5
 
6
+ type AnthropicTestBlock = {
7
+ id?: unknown;
8
+ type?: unknown;
9
+ };
10
+
11
+ const isServerToolId = (id: unknown): id is string =>
12
+ typeof id === 'string' && id.startsWith('srvtoolu_');
13
+
6
14
  describe('_convertMessagesToAnthropicPayload — server tool use (web search) multi-turn', () => {
7
15
  it('corrects tool_use blocks with srvtoolu_ IDs to server_tool_use', () => {
8
16
  const messageHistory: BaseMessage[] = [
@@ -78,9 +86,10 @@ describe('_convertMessagesToAnthropicPayload — server tool use (web search) mu
78
86
  const searchResultBlocks = assistantContent.filter(
79
87
  (b: any) => b.type === 'web_search_tool_result'
80
88
  );
81
- const regularToolUseBlocks = assistantContent.filter(
82
- (b: any) => b.type === 'tool_use' && b.id?.startsWith('srvtoolu_')
83
- );
89
+ const regularToolUseBlocks = assistantContent.filter((block: unknown) => {
90
+ const b = block as AnthropicTestBlock;
91
+ return b.type === 'tool_use' && isServerToolId(b.id);
92
+ });
84
93
 
85
94
  expect(serverToolBlocks).toHaveLength(2);
86
95
  expect(searchResultBlocks).toHaveLength(2);
@@ -190,9 +199,10 @@ describe('_convertMessagesToAnthropicPayload — server tool use (web search) mu
190
199
  expect(toolUseBlocks).toHaveLength(1);
191
200
  expect(toolUseBlocks[0].id).toBe('toolu_regular');
192
201
 
193
- const serverToolBlocks = assistantContent.filter((b: any) =>
194
- b.id?.startsWith('srvtoolu_')
195
- );
202
+ const serverToolBlocks = assistantContent.filter((block: unknown) => {
203
+ const b = block as AnthropicTestBlock;
204
+ return isServerToolId(b.id);
205
+ });
196
206
  expect(serverToolBlocks).toHaveLength(0);
197
207
  });
198
208
 
@@ -217,7 +227,80 @@ describe('_convertMessagesToAnthropicPayload — server tool use (web search) mu
217
227
  const assistantContent = messages[1].content as any[];
218
228
  expect(assistantContent).toHaveLength(1);
219
229
  expect(assistantContent[0].type).toBe('text');
220
- expect(assistantContent[0].text).toBe(' ');
230
+ expect(assistantContent[0].text).toBe('_');
231
+ expect(assistantContent[0].text.trim()).toBe('_');
232
+ });
233
+
234
+ it('uses non-whitespace fallback content after filtering empty array text', () => {
235
+ const messageHistory: BaseMessage[] = [
236
+ new HumanMessage({
237
+ content: [
238
+ { type: 'text', text: ' ' },
239
+ { type: 'text', text: '\n' },
240
+ ],
241
+ }),
242
+ ];
243
+
244
+ const { messages } = _convertMessagesToAnthropicPayload(messageHistory);
245
+ expect(messages[0].content).toEqual([{ type: 'text', text: '_' }]);
246
+ });
247
+
248
+ it('uses non-whitespace fallback content for empty server tool result artifacts', () => {
249
+ const messageHistory: BaseMessage[] = [
250
+ new HumanMessage('search for X'),
251
+ new AIMessage({
252
+ content: '',
253
+ tool_calls: [
254
+ {
255
+ id: 'srvtoolu_1',
256
+ name: 'web_search',
257
+ args: { query: 'X' },
258
+ type: 'tool_call',
259
+ },
260
+ ],
261
+ }),
262
+ new ToolMessage({
263
+ content: '',
264
+ tool_call_id: 'srvtoolu_1',
265
+ }),
266
+ new HumanMessage('follow up'),
267
+ ];
268
+
269
+ const { messages } = _convertMessagesToAnthropicPayload(messageHistory);
270
+
271
+ expect(messages[1].content).toEqual([{ type: 'text', text: '_' }]);
272
+ expect(messages[2].content).toEqual([{ type: 'text', text: '_' }]);
273
+ });
274
+
275
+ it('does not throw when a non-string ToolMessage has undefined content', () => {
276
+ const undefinedContentToolMessage = {
277
+ _getType: (): 'tool' => 'tool',
278
+ content: undefined,
279
+ tool_call_id: 'toolu_calc',
280
+ } as unknown as ToolMessage;
281
+ const messageHistory: BaseMessage[] = [
282
+ new HumanMessage('call the calculator'),
283
+ new AIMessage({
284
+ content: '',
285
+ tool_calls: [
286
+ {
287
+ id: 'toolu_calc',
288
+ name: 'calculator',
289
+ args: { expr: '2+2' },
290
+ type: 'tool_call',
291
+ },
292
+ ],
293
+ }),
294
+ undefinedContentToolMessage,
295
+ ];
296
+
297
+ const { messages } = _convertMessagesToAnthropicPayload(messageHistory);
298
+ expect(messages[2].content).toEqual([
299
+ {
300
+ type: 'tool_result',
301
+ tool_use_id: 'toolu_calc',
302
+ },
303
+ ]);
221
304
  });
222
305
 
223
306
  it('preserves regular tool_use blocks alongside corrected server tool blocks', () => {
@@ -283,9 +366,10 @@ describe('_convertMessagesToAnthropicPayload — server tool use (web search) mu
283
366
  const webSearchResult = assistantContent.filter(
284
367
  (b: any) => b.type === 'web_search_tool_result'
285
368
  );
286
- const regularToolUse = assistantContent.filter(
287
- (b: any) => b.type === 'tool_use' && !b.id?.startsWith('srvtoolu_')
288
- );
369
+ const regularToolUse = assistantContent.filter((block: unknown) => {
370
+ const b = block as AnthropicTestBlock;
371
+ return b.type === 'tool_use' && !isServerToolId(b.id);
372
+ });
289
373
 
290
374
  expect(serverToolUse).toHaveLength(1);
291
375
  expect(serverToolUse[0].id).toBe('srvtoolu_1');
@@ -416,9 +500,7 @@ describe('_convertMessagesToAnthropicPayload — server tool use (web search) mu
416
500
  );
417
501
  expect(whitespaceTextBlocks).toHaveLength(0);
418
502
 
419
- const textBlocks = assistantContent.filter(
420
- (b: any) => b.type === 'text'
421
- );
503
+ const textBlocks = assistantContent.filter((b: any) => b.type === 'text');
422
504
  expect(textBlocks).toHaveLength(1);
423
505
  expect(textBlocks[0].text).toBe('Here are the results.');
424
506
  }
@@ -5,6 +5,7 @@ import {
5
5
  makeIsDeferred,
6
6
  partitionAndMarkAnthropicToolCache,
7
7
  } from '../anthropicToolCache';
8
+ import { CustomAnthropic } from '@/llm/anthropic';
8
9
 
9
10
  function fakeTool(name: string): unknown {
10
11
  return tool(async () => 'ok', {
@@ -86,6 +87,51 @@ describe('partitionAndMarkAnthropicToolCache', () => {
86
87
  expect(out[0].extras?.cache_control).toEqual({ type: 'ephemeral' });
87
88
  });
88
89
 
90
+ it('stamps Anthropic built-in tools with direct cache_control', () => {
91
+ const webSearch = {
92
+ type: 'web_search_20250305',
93
+ name: 'web_search',
94
+ max_uses: 3,
95
+ };
96
+ const out = partitionAndMarkAnthropicToolCache(
97
+ [webSearch] as never,
98
+ () => false
99
+ ) as Array<{
100
+ cache_control?: { type: string };
101
+ extras?: { cache_control?: { type: string } };
102
+ }>;
103
+
104
+ expect(out[0]).not.toBe(webSearch);
105
+ expect(out[0].cache_control).toEqual({ type: 'ephemeral' });
106
+ expect(out[0].extras).toBeUndefined();
107
+ });
108
+
109
+ it('does not serialize extras on Anthropic built-in tools', () => {
110
+ const model = new CustomAnthropic({
111
+ model: 'claude-haiku-4-5',
112
+ apiKey: 'testing',
113
+ });
114
+ const webSearch = {
115
+ type: 'web_search_20250305',
116
+ name: 'web_search',
117
+ max_uses: 3,
118
+ };
119
+ const tools = partitionAndMarkAnthropicToolCache(
120
+ [webSearch] as never,
121
+ () => false
122
+ );
123
+ const formattedTools = model.formatStructuredToolToAnthropic(tools);
124
+ const formatted = formattedTools?.[0];
125
+
126
+ expect(formatted).toEqual({
127
+ type: 'web_search_20250305',
128
+ name: 'web_search',
129
+ max_uses: 3,
130
+ cache_control: { type: 'ephemeral' },
131
+ });
132
+ expect(formatted).not.toHaveProperty('extras');
133
+ });
134
+
89
135
  it('is idempotent when re-marking a tool that already has the marker', () => {
90
136
  const a = fakeTool('a-static') as { extras?: Record<string, unknown> };
91
137
  a.extras = { cache_control: { type: 'ephemeral' } };
@@ -19,14 +19,74 @@
19
19
  * the breakpoint.
20
20
  *
21
21
  * LangChain's Anthropic adapter passes the marker through via
22
- * `tool.extras.cache_control` (`AnthropicToolExtrasSchema`), so we set
23
- * it as an `extras` field on a fresh wrapper around the tool never
24
- * mutating the original tool instance, since callers may share them
22
+ * `tool.extras.cache_control` for custom tools, while Anthropic built-ins
23
+ * require direct `cache_control`. Either way, we stamp a fresh wrapper —
24
+ * never mutating the original tool instance, since callers may share them
25
25
  * across runs.
26
26
  */
27
27
 
28
28
  import type { GraphTools } from '@/types';
29
29
 
30
+ const ANTHROPIC_BUILT_IN_TOOL_PREFIXES = [
31
+ 'text_editor_',
32
+ 'computer_',
33
+ 'bash_',
34
+ 'web_search_',
35
+ 'web_fetch_',
36
+ 'str_replace_editor_',
37
+ 'str_replace_based_edit_tool_',
38
+ 'code_execution_',
39
+ 'memory_',
40
+ 'tool_search_',
41
+ 'mcp_toolset',
42
+ ] as const;
43
+
44
+ const CACHE_CONTROL = { type: 'ephemeral' as const };
45
+
46
+ type AnthropicToolCacheCandidate = {
47
+ name?: unknown;
48
+ type?: unknown;
49
+ extras?: Record<string, unknown>;
50
+ cache_control?: unknown;
51
+ };
52
+
53
+ function isAnthropicBuiltInTool(
54
+ tool: AnthropicToolCacheCandidate
55
+ ): tool is AnthropicToolCacheCandidate & { type: string } {
56
+ const { type } = tool;
57
+ return (
58
+ typeof type === 'string' &&
59
+ ANTHROPIC_BUILT_IN_TOOL_PREFIXES.some((prefix) => type.startsWith(prefix))
60
+ );
61
+ }
62
+
63
+ function hasCacheControl(tool: AnthropicToolCacheCandidate): boolean {
64
+ if (isAnthropicBuiltInTool(tool)) {
65
+ return tool.cache_control != null;
66
+ }
67
+ return tool.extras?.cache_control != null;
68
+ }
69
+
70
+ function markCacheControl(
71
+ tool: AnthropicToolCacheCandidate
72
+ ): AnthropicToolCacheCandidate {
73
+ const prototype = Object.getPrototypeOf(tool) ?? Object.prototype;
74
+ if (isAnthropicBuiltInTool(tool)) {
75
+ const wrapped = { ...tool };
76
+ delete wrapped.extras;
77
+ return Object.assign(Object.create(prototype), wrapped, {
78
+ cache_control: CACHE_CONTROL,
79
+ });
80
+ }
81
+
82
+ return Object.assign(Object.create(prototype), tool, {
83
+ extras: {
84
+ ...(tool.extras ?? {}),
85
+ cache_control: CACHE_CONTROL,
86
+ },
87
+ });
88
+ }
89
+
30
90
  /**
31
91
  * Returns a callable that reports whether a given tool *name* is deferred
32
92
  * according to the supplied registry of tool definitions. Tools without
@@ -59,8 +119,8 @@ export function makeIsDeferred(
59
119
  *
60
120
  * The original tool instances are never mutated. The marked entry is a
61
121
  * shallow wrapper that preserves the prototype chain so downstream
62
- * `instanceof` checks still pass. `extras` is merged so any existing
63
- * `providerToolDefinition` / other extras the host attached are kept.
122
+ * `instanceof` checks still pass. For custom tools, `extras` is merged
123
+ * so any existing `providerToolDefinition` / other extras are kept.
64
124
  */
65
125
  export function partitionAndMarkAnthropicToolCache(
66
126
  tools: GraphTools | undefined,
@@ -87,30 +147,15 @@ export function partitionAndMarkAnthropicToolCache(
87
147
  return tools;
88
148
  }
89
149
 
90
- const last = staticTools[staticTools.length - 1] as {
91
- extras?: Record<string, unknown>;
92
- };
150
+ const last = staticTools[
151
+ staticTools.length - 1
152
+ ] as AnthropicToolCacheCandidate;
93
153
  // Already marked? Don't double-clone.
94
- if (
95
- last.extras != null &&
96
- 'cache_control' in last.extras &&
97
- (last.extras as { cache_control?: unknown }).cache_control != null
98
- ) {
154
+ if (hasCacheControl(last)) {
99
155
  if (deferredTools.length === 0) return tools;
100
156
  return [...staticTools, ...deferredTools] as GraphTools;
101
157
  }
102
158
 
103
- const wrapped = Object.assign(
104
- Object.create(Object.getPrototypeOf(last) ?? Object.prototype),
105
- last,
106
- {
107
- extras: {
108
- ...((last.extras as Record<string, unknown> | undefined) ?? {}),
109
- cache_control: { type: 'ephemeral' as const },
110
- },
111
- }
112
- );
113
-
114
- staticTools[staticTools.length - 1] = wrapped;
159
+ staticTools[staticTools.length - 1] = markCacheControl(last);
115
160
  return [...staticTools, ...deferredTools] as GraphTools;
116
161
  }