@librechat/agents 3.2.38 → 3.2.41

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 (105) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +25 -8
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +7 -4
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +4 -3
  6. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -1
  7. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +20 -4
  8. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  9. package/dist/cjs/llm/bedrock/index.cjs +7 -1
  10. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/toolCache.cjs +5 -4
  12. package/dist/cjs/llm/bedrock/toolCache.cjs.map +1 -1
  13. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +34 -17
  14. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  15. package/dist/cjs/llm/openrouter/index.cjs +1 -0
  16. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  17. package/dist/cjs/llm/openrouter/toolCache.cjs +18 -5
  18. package/dist/cjs/llm/openrouter/toolCache.cjs.map +1 -1
  19. package/dist/cjs/main.cjs +4 -0
  20. package/dist/cjs/messages/anthropicToolCache.cjs +75 -13
  21. package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -1
  22. package/dist/cjs/messages/cache.cjs +91 -35
  23. package/dist/cjs/messages/cache.cjs.map +1 -1
  24. package/dist/cjs/summarization/node.cjs +3 -2
  25. package/dist/cjs/summarization/node.cjs.map +1 -1
  26. package/dist/cjs/tools/ReadFile.cjs +2 -2
  27. package/dist/cjs/tools/ReadFile.cjs.map +1 -1
  28. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +11 -11
  29. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -1
  30. package/dist/cjs/tools/local/LocalCodingTools.cjs +11 -11
  31. package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
  32. package/dist/esm/agents/AgentContext.mjs +26 -9
  33. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  34. package/dist/esm/graphs/Graph.mjs +8 -5
  35. package/dist/esm/graphs/Graph.mjs.map +1 -1
  36. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +4 -3
  37. package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -1
  38. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +20 -4
  39. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  40. package/dist/esm/llm/bedrock/index.mjs +7 -1
  41. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  42. package/dist/esm/llm/bedrock/toolCache.mjs +5 -4
  43. package/dist/esm/llm/bedrock/toolCache.mjs.map +1 -1
  44. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +34 -17
  45. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  46. package/dist/esm/llm/openrouter/index.mjs +1 -0
  47. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  48. package/dist/esm/llm/openrouter/toolCache.mjs +18 -5
  49. package/dist/esm/llm/openrouter/toolCache.mjs.map +1 -1
  50. package/dist/esm/main.mjs +2 -2
  51. package/dist/esm/messages/anthropicToolCache.mjs +75 -13
  52. package/dist/esm/messages/anthropicToolCache.mjs.map +1 -1
  53. package/dist/esm/messages/cache.mjs +88 -36
  54. package/dist/esm/messages/cache.mjs.map +1 -1
  55. package/dist/esm/summarization/node.mjs +4 -3
  56. package/dist/esm/summarization/node.mjs.map +1 -1
  57. package/dist/esm/tools/ReadFile.mjs +2 -2
  58. package/dist/esm/tools/ReadFile.mjs.map +1 -1
  59. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +11 -11
  60. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -1
  61. package/dist/esm/tools/local/LocalCodingTools.mjs +11 -11
  62. package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
  63. package/dist/types/agents/AgentContext.d.ts +11 -0
  64. package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +2 -0
  65. package/dist/types/llm/bedrock/index.d.ts +13 -0
  66. package/dist/types/llm/bedrock/toolCache.d.ts +2 -1
  67. package/dist/types/llm/openrouter/index.d.ts +8 -0
  68. package/dist/types/llm/openrouter/toolCache.d.ts +2 -1
  69. package/dist/types/messages/anthropicToolCache.d.ts +2 -1
  70. package/dist/types/messages/cache.d.ts +49 -5
  71. package/dist/types/tools/ReadFile.d.ts +4 -4
  72. package/dist/types/types/llm.d.ts +14 -0
  73. package/package.json +1 -1
  74. package/src/agents/AgentContext.ts +64 -17
  75. package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +6 -2
  76. package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +7 -5
  77. package/src/agents/__tests__/AgentContext.openrouter.live.test.ts +1 -1
  78. package/src/agents/__tests__/AgentContext.test.ts +31 -19
  79. package/src/agents/__tests__/promptCacheLiveHelpers.ts +6 -2
  80. package/src/graphs/Graph.ts +40 -4
  81. package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +12 -12
  82. package/src/hooks/createWorkspacePolicyHook.ts +7 -6
  83. package/src/llm/anthropic/utils/message_inputs.ts +33 -6
  84. package/src/llm/bedrock/index.ts +21 -1
  85. package/src/llm/bedrock/llm.spec.ts +61 -0
  86. package/src/llm/bedrock/toolCache.test.ts +24 -0
  87. package/src/llm/bedrock/toolCache.ts +12 -7
  88. package/src/llm/bedrock/utils/message_inputs.ts +57 -40
  89. package/src/llm/openrouter/index.ts +9 -0
  90. package/src/llm/openrouter/toolCache.test.ts +52 -1
  91. package/src/llm/openrouter/toolCache.ts +40 -6
  92. package/src/messages/__tests__/anthropicToolCache.test.ts +168 -0
  93. package/src/messages/anthropicToolCache.ts +118 -15
  94. package/src/messages/cache.test.ts +175 -0
  95. package/src/messages/cache.ts +133 -48
  96. package/src/summarization/node.ts +21 -2
  97. package/src/tools/ReadFile.ts +2 -2
  98. package/src/tools/__tests__/LocalExecutionTools.test.ts +25 -25
  99. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +5 -5
  100. package/src/tools/__tests__/ReadFile.test.ts +3 -3
  101. package/src/tools/__tests__/ToolNode.session.test.ts +2 -2
  102. package/src/tools/__tests__/workspaceSeam.test.ts +2 -2
  103. package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +11 -11
  104. package/src/tools/local/LocalCodingTools.ts +14 -14
  105. package/src/types/llm.ts +14 -0
@@ -20,6 +20,65 @@ type MessageContentWithCacheControl = MessageContentComplex & {
20
20
  cache_control?: unknown;
21
21
  };
22
22
 
23
+ /**
24
+ * Prompt-cache breakpoint TTL.
25
+ *
26
+ * Both Anthropic (`cache_control.ttl`) and Bedrock (`cachePoint.ttl`) accept
27
+ * `'5m'` (the legacy provider default) and `'1h'` (the extended cache). When
28
+ * prompt caching is enabled the SDK now defaults to the 1-hour extended cache
29
+ * (see {@link DEFAULT_PROMPT_CACHE_TTL}); pass `'5m'` to opt back into the
30
+ * legacy 5-minute behavior.
31
+ */
32
+ export type PromptCacheTtl = '5m' | '1h';
33
+
34
+ /**
35
+ * Default TTL applied wherever a prompt-cache breakpoint is added. The 1-hour
36
+ * extended cache keeps prefixes warm across longer gaps between turns, at the
37
+ * cost of a higher one-time cache-write multiplier (2x vs 1.25x for 5m).
38
+ */
39
+ export const DEFAULT_PROMPT_CACHE_TTL: PromptCacheTtl = '1h';
40
+
41
+ /**
42
+ * Resolve an optionally-configured TTL to a concrete value, defaulting to the
43
+ * 1-hour extended cache. Used at the Anthropic/Bedrock prompt-cache call sites.
44
+ */
45
+ export function resolvePromptCacheTtl(
46
+ ttl: PromptCacheTtl | undefined
47
+ ): PromptCacheTtl {
48
+ return ttl ?? DEFAULT_PROMPT_CACHE_TTL;
49
+ }
50
+
51
+ /** Anthropic `cache_control` shape (the SDK accepts an optional `ttl`). */
52
+ type AnthropicCacheControl = { type: 'ephemeral'; ttl?: '1h' };
53
+
54
+ /**
55
+ * Build an Anthropic `cache_control` breakpoint for the given TTL. `'5m'` (or
56
+ * `undefined`) omits the `ttl` field — that is the provider default, so the
57
+ * payload stays byte-identical to the legacy 5-minute marker. `'1h'` adds the
58
+ * explicit extended-cache `ttl`.
59
+ */
60
+ export function buildAnthropicCacheControl(
61
+ ttl?: PromptCacheTtl
62
+ ): AnthropicCacheControl {
63
+ return ttl === '1h'
64
+ ? { type: 'ephemeral', ttl: '1h' }
65
+ : { type: 'ephemeral' };
66
+ }
67
+
68
+ /** Bedrock `cachePoint` shape (the SDK accepts an optional `ttl`). */
69
+ type BedrockCachePoint = { type: 'default'; ttl?: '1h' };
70
+
71
+ /**
72
+ * Build a Bedrock `cachePoint` for the given TTL. Mirrors
73
+ * {@link buildAnthropicCacheControl}: `'5m'`/`undefined` omits `ttl` (the
74
+ * legacy default), `'1h'` adds the extended-cache `ttl`.
75
+ */
76
+ export function buildBedrockCachePoint(
77
+ ttl?: PromptCacheTtl
78
+ ): BedrockCachePoint {
79
+ return ttl === '1h' ? { type: 'default', ttl: '1h' } : { type: 'default' };
80
+ }
81
+
23
82
  /**
24
83
  * Deep clones a message's content to prevent mutation of the original.
25
84
  */
@@ -119,38 +178,53 @@ export function cloneMessage<T extends MessageWithContent>(
119
178
  return cloned;
120
179
  }
121
180
 
122
- function stripAnthropicCacheControlFromBlocks(
123
- content: MessageContentComplex[]
124
- ): { content: MessageContentComplex[]; modified: boolean } {
125
- let modified = false;
126
- const strippedContent = content.map((block) => {
127
- if (!('cache_control' in block)) {
128
- return block;
129
- }
130
-
131
- const cloned: MessageContentWithCacheControl = { ...block };
132
- delete cloned.cache_control;
133
- modified = true;
134
- return cloned;
135
- });
136
-
137
- return { content: strippedContent, modified };
138
- }
139
-
181
+ /**
182
+ * Sanitize a Bedrock system message: strip Anthropic `cache_control` (Bedrock
183
+ * conversion can't use it) and normalize any existing `cachePoint` to the
184
+ * resolved TTL. The normalization matters because Bedrock requires longer-TTL
185
+ * checkpoints to appear before shorter ones — a stale 5-minute system cachePoint
186
+ * (host-supplied or carried over from a 5m config) left ahead of a 1-hour
187
+ * message tail would make the request invalid. System messages are never
188
+ * anchored as the tail breakpoint; this only fixes markers already present.
189
+ */
140
190
  function sanitizeBedrockSystemMessage<T extends MessageWithContent>(
141
- message: T
191
+ message: T,
192
+ ttl?: PromptCacheTtl
142
193
  ): T {
143
194
  const content = message.content;
144
195
  if (!Array.isArray(content)) {
145
196
  return message;
146
197
  }
147
198
 
148
- const stripped = stripAnthropicCacheControlFromBlocks(content);
149
- if (!stripped.modified) {
199
+ const sanitized: MessageContentComplex[] = [];
200
+ let modified = false;
201
+ for (const block of content) {
202
+ if (isCachePoint(block)) {
203
+ const existing = (block as { cachePoint?: { ttl?: unknown } }).cachePoint;
204
+ const desired = buildBedrockCachePoint(ttl);
205
+ if (existing?.ttl !== desired.ttl) {
206
+ modified = true;
207
+ sanitized.push({ cachePoint: desired } as MessageContentComplex);
208
+ } else {
209
+ sanitized.push(block);
210
+ }
211
+ continue;
212
+ }
213
+ if ('cache_control' in block) {
214
+ const cloned: MessageContentWithCacheControl = { ...block };
215
+ delete cloned.cache_control;
216
+ modified = true;
217
+ sanitized.push(cloned);
218
+ continue;
219
+ }
220
+ sanitized.push(block);
221
+ }
222
+
223
+ if (!modified) {
150
224
  return message;
151
225
  }
152
226
 
153
- return cloneMessage(message, stripped.content);
227
+ return cloneMessage(message, sanitized);
154
228
  }
155
229
 
156
230
  /**
@@ -163,7 +237,8 @@ function sanitizeBedrockSystemMessage<T extends MessageWithContent>(
163
237
  * @returns - A new array of message objects with cache control added.
164
238
  */
165
239
  export function addCacheControl<T extends AnthropicMessage | BaseMessage>(
166
- messages: T[]
240
+ messages: T[],
241
+ ttl?: PromptCacheTtl
167
242
  ): T[] {
168
243
  if (!Array.isArray(messages) || messages.length < 2) {
169
244
  return messages;
@@ -224,14 +299,16 @@ export function addCacheControl<T extends AnthropicMessage | BaseMessage>(
224
299
  if (needsCacheAdd && lastTextIndex >= 0) {
225
300
  (
226
301
  workingContent[lastTextIndex] as Anthropic.TextBlockParam
227
- ).cache_control = {
228
- type: 'ephemeral',
229
- };
302
+ ).cache_control = buildAnthropicCacheControl(ttl);
230
303
  userMessagesModified++;
231
304
  }
232
305
  } else if (typeof content === 'string' && needsCacheAdd) {
233
306
  workingContent = [
234
- { type: 'text', text: content, cache_control: { type: 'ephemeral' } },
307
+ {
308
+ type: 'text',
309
+ text: content,
310
+ cache_control: buildAnthropicCacheControl(ttl),
311
+ },
235
312
  ] as unknown as MessageContentComplex[];
236
313
  userMessagesModified++;
237
314
  } else {
@@ -318,7 +395,8 @@ function isTailCacheableBlock(block: MessageContentComplex): boolean {
318
395
  * Returns a new array; only messages that require modification are cloned.
319
396
  */
320
397
  export function addTailCacheControl<T extends AnthropicMessage | BaseMessage>(
321
- messages: T[]
398
+ messages: T[],
399
+ ttl?: PromptCacheTtl
322
400
  ): T[] {
323
401
  if (!Array.isArray(messages) || messages.length === 0) {
324
402
  return messages;
@@ -368,9 +446,7 @@ export function addTailCacheControl<T extends AnthropicMessage | BaseMessage>(
368
446
 
369
447
  if (canPlaceMarker && tailIndex >= 0) {
370
448
  (workingContent[tailIndex] as Anthropic.TextBlockParam).cache_control =
371
- {
372
- type: 'ephemeral',
373
- };
449
+ buildAnthropicCacheControl(ttl);
374
450
  markerPlaced = true;
375
451
  modified = true;
376
452
  }
@@ -384,7 +460,11 @@ export function addTailCacheControl<T extends AnthropicMessage | BaseMessage>(
384
460
  content.trim() !== ''
385
461
  ) {
386
462
  workingContent = [
387
- { type: 'text', text: content, cache_control: { type: 'ephemeral' } },
463
+ {
464
+ type: 'text',
465
+ text: content,
466
+ cache_control: buildAnthropicCacheControl(ttl),
467
+ },
388
468
  ] as unknown as MessageContentComplex[];
389
469
  markerPlaced = true;
390
470
  } else {
@@ -454,7 +534,8 @@ function addCacheControlToRecentMessages<
454
534
  >(
455
535
  messages: T[],
456
536
  maxCachePoints: number,
457
- canUseMessage: (message: MessageWithContent) => boolean
537
+ canUseMessage: (message: MessageWithContent) => boolean,
538
+ ttl?: PromptCacheTtl
458
539
  ): T[] {
459
540
  if (
460
541
  !Array.isArray(messages) ||
@@ -513,9 +594,7 @@ function addCacheControlToRecentMessages<
513
594
  if (canAddCache && lastNonEmptyTextIndex >= 0) {
514
595
  (
515
596
  workingContent[lastNonEmptyTextIndex] as Anthropic.TextBlockParam
516
- ).cache_control = {
517
- type: 'ephemeral',
518
- };
597
+ ).cache_control = buildAnthropicCacheControl(ttl);
519
598
  cachePointsAdded++;
520
599
  modified = true;
521
600
  }
@@ -529,7 +608,11 @@ function addCacheControlToRecentMessages<
529
608
  canAddCache
530
609
  ) {
531
610
  workingContent = [
532
- { type: 'text', text: content, cache_control: { type: 'ephemeral' } },
611
+ {
612
+ type: 'text',
613
+ text: content,
614
+ cache_control: buildAnthropicCacheControl(ttl),
615
+ },
533
616
  ] as unknown as MessageContentComplex[];
534
617
  cachePointsAdded++;
535
618
  } else {
@@ -547,11 +630,12 @@ function addCacheControlToRecentMessages<
547
630
 
548
631
  export function addCacheControlToStablePrefixMessages<
549
632
  T extends AnthropicMessage | BaseMessage,
550
- >(messages: T[], maxCachePoints: number): T[] {
633
+ >(messages: T[], maxCachePoints: number, ttl?: PromptCacheTtl): T[] {
551
634
  const assistantMarked = addCacheControlToRecentMessages(
552
635
  messages,
553
636
  maxCachePoints,
554
- isAssistantConversationMessage
637
+ isAssistantConversationMessage,
638
+ ttl
555
639
  );
556
640
 
557
641
  if (assistantMarked.some(hasCacheMarker)) {
@@ -561,7 +645,8 @@ export function addCacheControlToStablePrefixMessages<
561
645
  return addCacheControlToRecentMessages(
562
646
  messages,
563
647
  maxCachePoints,
564
- isCacheableConversationMessage
648
+ isCacheableConversationMessage,
649
+ ttl
565
650
  );
566
651
  }
567
652
 
@@ -664,7 +749,7 @@ export function stripBedrockCacheControl<T extends MessageWithContent>(
664
749
  */
665
750
  export function addBedrockCacheControl<
666
751
  T extends MessageWithContent & { getType?: () => string; role?: string },
667
- >(messages: T[]): T[] {
752
+ >(messages: T[], ttl?: PromptCacheTtl): T[] {
668
753
  if (!Array.isArray(messages) || messages.length === 0) {
669
754
  return messages;
670
755
  }
@@ -687,7 +772,7 @@ export function addBedrockCacheControl<
687
772
  const isSystemMessage =
688
773
  messageType === 'system' || messageRole === 'system';
689
774
  if (isSystemMessage) {
690
- updatedMessages[i] = sanitizeBedrockSystemMessage(originalMessage);
775
+ updatedMessages[i] = sanitizeBedrockSystemMessage(originalMessage, ttl);
691
776
  continue;
692
777
  }
693
778
 
@@ -750,14 +835,14 @@ export function addBedrockCacheControl<
750
835
  // Skip if no cacheable text content exists (whitespace-only messages).
751
836
  if (needsCacheAdd && lastNonEmptyTextIndex >= 0) {
752
837
  workingContent.splice(lastNonEmptyTextIndex + 1, 0, {
753
- cachePoint: { type: 'default' },
838
+ cachePoint: buildBedrockCachePoint(ttl),
754
839
  } as MessageContentComplex);
755
840
  cachePointsAdded++;
756
841
  }
757
842
  } else if (typeof content === 'string' && needsCacheAdd) {
758
843
  workingContent = [
759
844
  { type: ContentTypes.TEXT, text: content },
760
- { cachePoint: { type: 'default' } } as MessageContentComplex,
845
+ { cachePoint: buildBedrockCachePoint(ttl) } as MessageContentComplex,
761
846
  ];
762
847
  cachePointsAdded++;
763
848
  } else if (typeof content === 'string' && hasSerializationProps) {
@@ -791,7 +876,7 @@ export function addBedrockCacheControl<
791
876
  */
792
877
  export function addBedrockTailCacheControl<
793
878
  T extends MessageWithContent & { getType?: () => string; role?: string },
794
- >(messages: T[]): T[] {
879
+ >(messages: T[], ttl?: PromptCacheTtl): T[] {
795
880
  if (!Array.isArray(messages) || messages.length === 0) {
796
881
  return messages;
797
882
  }
@@ -814,7 +899,7 @@ export function addBedrockTailCacheControl<
814
899
  const isSystemMessage =
815
900
  messageType === 'system' || messageRole === 'system';
816
901
  if (isSystemMessage) {
817
- updatedMessages[i] = sanitizeBedrockSystemMessage(originalMessage);
902
+ updatedMessages[i] = sanitizeBedrockSystemMessage(originalMessage, ttl);
818
903
  continue;
819
904
  }
820
905
 
@@ -869,7 +954,7 @@ export function addBedrockTailCacheControl<
869
954
 
870
955
  if (canPlaceCachePoint && lastNonEmptyTextIndex >= 0) {
871
956
  workingContent.splice(lastNonEmptyTextIndex + 1, 0, {
872
- cachePoint: { type: 'default' },
957
+ cachePoint: buildBedrockCachePoint(ttl),
873
958
  } as MessageContentComplex);
874
959
  cachePointPlaced = true;
875
960
  modified = true;
@@ -877,7 +962,7 @@ export function addBedrockTailCacheControl<
877
962
  } else if (typeof content === 'string' && canPlaceCachePoint) {
878
963
  workingContent = [
879
964
  { type: ContentTypes.TEXT, text: content },
880
- { cachePoint: { type: 'default' } } as MessageContentComplex,
965
+ { cachePoint: buildBedrockCachePoint(ttl) } as MessageContentComplex,
881
966
  ];
882
967
  cachePointPlaced = true;
883
968
  } else if (typeof content === 'string' && hasSerializationProps) {
@@ -10,6 +10,11 @@ import type { AgentContext } from '@/agents/AgentContext';
10
10
  import type { HookRegistry } from '@/hooks';
11
11
  import type { OnChunk } from '@/llm/invoke';
12
12
  import type * as t from '@/types';
13
+ import {
14
+ addTailCacheControl,
15
+ resolvePromptCacheTtl,
16
+ type PromptCacheTtl,
17
+ } from '@/messages/cache';
13
18
  import {
14
19
  Constants,
15
20
  ContentTypes,
@@ -22,7 +27,6 @@ import { attemptInvoke, tryFallbackProviders } from '@/llm/invoke';
22
27
  import { createRemoveAllMessage } from '@/messages/reducer';
23
28
  import { splitAtRecencyBoundary } from '@/messages/recency';
24
29
  import { getMaxOutputTokensKey } from '@/llm/request';
25
- import { addTailCacheControl } from '@/messages/cache';
26
30
  import { initializeModel } from '@/llm/init';
27
31
  import { getChunkContent } from '@/stream';
28
32
  import { executeHooks } from '@/hooks';
@@ -552,6 +556,17 @@ async function executeSummarizationWithFallback(params: {
552
556
  provider: clientConfig.provider as Providers,
553
557
  reasoningKey: agentContext.reasoningKey,
554
558
  usePromptCache,
559
+ promptCacheTtl:
560
+ (clientConfig.provider as Providers) === Providers.ANTHROPIC ||
561
+ (clientConfig.provider as Providers) === Providers.OPENROUTER
562
+ ? resolvePromptCacheTtl(
563
+ (
564
+ clientConfig.clientOptions as {
565
+ promptCacheTtl?: PromptCacheTtl;
566
+ }
567
+ ).promptCacheTtl
568
+ )
569
+ : undefined,
555
570
  log,
556
571
  });
557
572
  summaryText = result.text;
@@ -1205,6 +1220,7 @@ async function summarizeWithCacheHit({
1205
1220
  provider,
1206
1221
  reasoningKey,
1207
1222
  usePromptCache,
1223
+ promptCacheTtl,
1208
1224
  log,
1209
1225
  }: {
1210
1226
  model: t.ChatModel;
@@ -1217,6 +1233,7 @@ async function summarizeWithCacheHit({
1217
1233
  provider: Providers;
1218
1234
  reasoningKey?: 'reasoning_content' | 'reasoning';
1219
1235
  usePromptCache?: boolean;
1236
+ promptCacheTtl?: PromptCacheTtl;
1220
1237
  log?: LogFn;
1221
1238
  }): Promise<{ text: string; usage?: Partial<UsageMetadata> }> {
1222
1239
  const instruction = buildSummarizationInstruction(
@@ -1227,7 +1244,9 @@ async function summarizeWithCacheHit({
1227
1244
 
1228
1245
  const fullMessages = [...messages, new HumanMessage(instruction)];
1229
1246
  const invokeMessages =
1230
- usePromptCache === true ? addTailCacheControl(fullMessages) : fullMessages;
1247
+ usePromptCache === true
1248
+ ? addTailCacheControl(fullMessages, promptCacheTtl)
1249
+ : fullMessages;
1231
1250
 
1232
1251
  const result = await attemptInvoke(
1233
1252
  {
@@ -22,13 +22,13 @@ CONSTRAINTS:
22
22
  export const ReadFileToolSchema = {
23
23
  type: 'object',
24
24
  properties: {
25
- file_path: {
25
+ path: {
26
26
  type: 'string',
27
27
  description:
28
28
  'Path to the file. For skill files: "{skillName}/{path}" (e.g. "pdf-analyzer/src/utils.py"). For code execution output: the path as returned by the execution tool.',
29
29
  },
30
30
  },
31
- required: ['file_path'],
31
+ required: ['path'],
32
32
  } as const;
33
33
 
34
34
  export const ReadFileToolDefinition = {
@@ -175,8 +175,8 @@ describe('local execution tools', () => {
175
175
  aiMessageWithToolCall(Constants.PROGRAMMATIC_TOOL_CALLING, {
176
176
  lang: 'py',
177
177
  code: [
178
- 'await write_file(file_path="ptc.txt", content="from local ptc")',
179
- 'contents = await read_file(file_path="ptc.txt")',
178
+ 'await write_file(path="ptc.txt", content="from local ptc")',
179
+ 'contents = await read_file(path="ptc.txt")',
180
180
  'print(contents)',
181
181
  ].join('\n'),
182
182
  }),
@@ -205,8 +205,8 @@ describe('local execution tools', () => {
205
205
  messages: [
206
206
  aiMessageWithToolCall(Constants.PROGRAMMATIC_TOOL_CALLING, {
207
207
  code: [
208
- 'write_file \'{"file_path":"bash-ptc.txt","content":"from bash ptc"}\'',
209
- 'read_file \'{"file_path":"bash-ptc.txt"}\'',
208
+ 'write_file \'{"path":"bash-ptc.txt","content":"from bash ptc"}\'',
209
+ 'read_file \'{"path":"bash-ptc.txt"}\'',
210
210
  ].join('\n'),
211
211
  }),
212
212
  ],
@@ -335,8 +335,8 @@ describe('LocalFileCheckpointer', () => {
335
335
 
336
336
  const writeTool = bundle.tools.find((tool_) => tool_.name === 'write_file');
337
337
  expect(writeTool).toBeDefined();
338
- await writeTool!.invoke({ file_path: 'cp.txt', content: 'first' });
339
- await writeTool!.invoke({ file_path: 'cp.txt', content: 'second' });
338
+ await writeTool!.invoke({ path: 'cp.txt', content: 'first' });
339
+ await writeTool!.invoke({ path: 'cp.txt', content: 'second' });
340
340
 
341
341
  const restored = await bundle.checkpointer!.rewind();
342
342
  expect(restored).toBe(1);
@@ -352,7 +352,7 @@ describe('local read tool guards', () => {
352
352
 
353
353
  const bundle = createLocalCodingToolBundle({ cwd });
354
354
  const readTool = bundle.tools.find((t_) => t_.name === Constants.READ_FILE);
355
- const result = await readTool!.invoke({ file_path: 'binary.bin' });
355
+ const result = await readTool!.invoke({ path: 'binary.bin' });
356
356
  expect(String(result)).toContain('binary file');
357
357
  });
358
358
 
@@ -366,7 +366,7 @@ describe('local read tool guards', () => {
366
366
  maxReadBytes: 1024,
367
367
  });
368
368
  const readTool = bundle.tools.find((t_) => t_.name === Constants.READ_FILE);
369
- const result = await readTool!.invoke({ file_path: 'big.txt' });
369
+ const result = await readTool!.invoke({ path: 'big.txt' });
370
370
  expect(String(result)).toContain('exceeds the 1024-byte read cap');
371
371
  });
372
372
 
@@ -380,7 +380,7 @@ describe('local read tool guards', () => {
380
380
  const bundle = createLocalCodingToolBundle({ cwd });
381
381
  const readTool = bundle.tools.find((t_) => t_.name === Constants.READ_FILE);
382
382
  await expect(
383
- readTool!.invoke({ file_path: 'escape/secret.txt' })
383
+ readTool!.invoke({ path: 'escape/secret.txt' })
384
384
  ).rejects.toThrow(/symlink escape/);
385
385
  });
386
386
  });
@@ -406,7 +406,7 @@ describe('local programmatic bridge auth', () => {
406
406
  code: [
407
407
  'import os, json, urllib.request, urllib.error',
408
408
  'url = os.environ["BRIDGE_PROBE_URL"] if "BRIDGE_PROBE_URL" in os.environ else __LIBRECHAT_TOOL_BRIDGE',
409
- 'body = json.dumps({"name":"read_file","input":{"file_path":"x"}}).encode("utf-8")',
409
+ 'body = json.dumps({"name":"read_file","input":{"path":"x"}}).encode("utf-8")',
410
410
  'try:',
411
411
  ' req = urllib.request.Request(url, data=body, headers={"Content-Type":"application/json"}, method="POST")',
412
412
  ' urllib.request.urlopen(req, timeout=5)',
@@ -438,7 +438,7 @@ describe('local edit fuzzy matching', () => {
438
438
  const bundle = createLocalCodingToolBundle({ cwd });
439
439
  const editTool = bundle.tools.find((tt) => tt.name === 'edit_file');
440
440
  const result = await editTool!.invoke({
441
- file_path: 'a.ts',
441
+ path: 'a.ts',
442
442
  // LLM emits a trailing-whitespace-stripped version.
443
443
  old_text:
444
444
  'function greet(name: string) {\n return `Hello, ${name}!`;\n}',
@@ -461,7 +461,7 @@ describe('local edit fuzzy matching', () => {
461
461
  const bundle = createLocalCodingToolBundle({ cwd });
462
462
  const editTool = bundle.tools.find((tt) => tt.name === 'edit_file');
463
463
  const result = await editTool!.invoke({
464
- file_path: 'a.ts',
464
+ path: 'a.ts',
465
465
  // LLM stripped the 4-space indent
466
466
  old_text: 'method() {\n return 1;\n}',
467
467
  new_text: 'method() {\n return 42;\n}',
@@ -480,7 +480,7 @@ describe('local edit fuzzy matching', () => {
480
480
  const bundle = createLocalCodingToolBundle({ cwd });
481
481
  const editTool = bundle.tools.find((tt) => tt.name === 'edit_file');
482
482
  const result = await editTool!.invoke({
483
- file_path: 'a.txt',
483
+ path: 'a.txt',
484
484
  old_text: 'second',
485
485
  new_text: 'SECOND',
486
486
  });
@@ -497,7 +497,7 @@ describe('local edit fuzzy matching', () => {
497
497
  const bundle = createLocalCodingToolBundle({ cwd });
498
498
  const editTool = bundle.tools.find((tt) => tt.name === 'edit_file');
499
499
  await editTool!.invoke({
500
- file_path: 'a.txt',
500
+ path: 'a.txt',
501
501
  old_text: 'two',
502
502
  new_text: 'TWO',
503
503
  });
@@ -512,7 +512,7 @@ describe('local edit fuzzy matching', () => {
512
512
  await fsWriteFile(file, BOM + 'hello\n', 'utf8');
513
513
  const bundle = createLocalCodingToolBundle({ cwd });
514
514
  const writeTool = bundle.tools.find((tt) => tt.name === 'write_file');
515
- await writeTool!.invoke({ file_path: 'a.txt', content: 'goodbye\n' });
515
+ await writeTool!.invoke({ path: 'a.txt', content: 'goodbye\n' });
516
516
  const raw = await fsReadFile(file, 'utf8');
517
517
  expect(raw.startsWith(BOM)).toBe(true);
518
518
  expect(raw.slice(1)).toBe('goodbye\n');
@@ -532,7 +532,7 @@ describe('local read attachments', () => {
532
532
  await fsWriteFile(file, tinyPng);
533
533
  const bundle = createLocalCodingToolBundle({ cwd });
534
534
  const readTool = bundle.tools.find((tt) => tt.name === Constants.READ_FILE);
535
- const result = await readTool!.invoke({ file_path: 'tiny.png' });
535
+ const result = await readTool!.invoke({ path: 'tiny.png' });
536
536
  expect(String(result)).toContain('binary file');
537
537
  });
538
538
 
@@ -552,7 +552,7 @@ describe('local read attachments', () => {
552
552
  const message = (await readTool!.invoke({
553
553
  id: 'call_image',
554
554
  name: Constants.READ_FILE,
555
- args: { file_path: 'tiny.png' },
555
+ args: { path: 'tiny.png' },
556
556
  type: 'tool_call',
557
557
  })) as { content: unknown; artifact: unknown };
558
558
  expect(Array.isArray(message.content)).toBe(true);
@@ -589,7 +589,7 @@ describe('local read attachments', () => {
589
589
  maxAttachmentBytes: 100,
590
590
  });
591
591
  const readTool = bundle.tools.find((tt) => tt.name === Constants.READ_FILE);
592
- const result = await readTool!.invoke({ file_path: 'big.png' });
592
+ const result = await readTool!.invoke({ path: 'big.png' });
593
593
  expect(String(result)).toMatch(/Refusing to embed/);
594
594
  });
595
595
 
@@ -602,7 +602,7 @@ describe('local read attachments', () => {
602
602
  attachReadAttachments: 'images-only',
603
603
  });
604
604
  const readTool = bundle.tools.find((tt) => tt.name === Constants.READ_FILE);
605
- const result = await readTool!.invoke({ file_path: 'a.txt' });
605
+ const result = await readTool!.invoke({ path: 'a.txt' });
606
606
  expect(String(result)).toContain('hello world');
607
607
  });
608
608
  });
@@ -662,7 +662,7 @@ describe('post-edit syntax check', () => {
662
662
  const message = (await writeTool!.invoke({
663
663
  id: 'call_w',
664
664
  name: 'write_file',
665
- args: { file_path: 'broken.js', content: 'function (\n' },
665
+ args: { path: 'broken.js', content: 'function (\n' },
666
666
  type: 'tool_call',
667
667
  })) as { content: string; artifact: { syntax_error?: string } };
668
668
  expect(message.content).toContain('[syntax-check warning');
@@ -680,7 +680,7 @@ describe('post-edit syntax check', () => {
680
680
  writeTool!.invoke({
681
681
  id: 'call_w',
682
682
  name: 'write_file',
683
- args: { file_path: 'broken.js', content: 'function (\n' },
683
+ args: { path: 'broken.js', content: 'function (\n' },
684
684
  type: 'tool_call',
685
685
  })
686
686
  ).rejects.toThrow(/syntax check failed/);
@@ -850,7 +850,7 @@ describe('codex review fixes', () => {
850
850
  const result = await readTool!.invoke({
851
851
  id: 'c',
852
852
  name: Constants.READ_FILE,
853
- args: { file_path: join(parent, 'shared/lib.ts') },
853
+ args: { path: join(parent, 'shared/lib.ts') },
854
854
  type: 'tool_call',
855
855
  });
856
856
  expect(JSON.stringify(result)).toContain('X');
@@ -2253,7 +2253,7 @@ describe('comprehensive review (round 14) — Codex P1 #37 + P2 #38/#40/#41', ()
2253
2253
  writeTool!.invoke({
2254
2254
  id: 'wf-strict',
2255
2255
  name: Constants.WRITE_FILE,
2256
- args: { file_path: file, content: 'function broken( {\n' },
2256
+ args: { path: file, content: 'function broken( {\n' },
2257
2257
  type: 'tool_call',
2258
2258
  })
2259
2259
  ).rejects.toThrow(/syntax check failed.*reverted/i);
@@ -2281,7 +2281,7 @@ describe('comprehensive review (round 14) — Codex P1 #37 + P2 #38/#40/#41', ()
2281
2281
  writeTool!.invoke({
2282
2282
  id: 'wf-strict-new',
2283
2283
  name: Constants.WRITE_FILE,
2284
- args: { file_path: file, content: 'function broken( {\n' },
2284
+ args: { path: file, content: 'function broken( {\n' },
2285
2285
  type: 'tool_call',
2286
2286
  })
2287
2287
  ).rejects.toThrow(/syntax check failed.*reverted/i);
@@ -2308,7 +2308,7 @@ describe('comprehensive review (round 14) — Codex P1 #37 + P2 #38/#40/#41', ()
2308
2308
  id: 'ef-strict',
2309
2309
  name: Constants.EDIT_FILE,
2310
2310
  args: {
2311
- file_path: file,
2311
+ path: file,
2312
2312
  old_text: 'return 1;',
2313
2313
  new_text: 'return broken(',
2314
2314
  },
@@ -1503,10 +1503,10 @@ for member in team:
1503
1503
  { registry, runId: 'r1' },
1504
1504
  'read_file',
1505
1505
  'call_1',
1506
- { file_path: '/tmp/x' }
1506
+ { path: '/tmp/x' }
1507
1507
  );
1508
1508
  expect(gate.denyReason).toBeUndefined();
1509
- expect(gate.input).toEqual({ file_path: '/tmp/x' });
1509
+ expect(gate.input).toEqual({ path: '/tmp/x' });
1510
1510
  });
1511
1511
 
1512
1512
  it('applies updatedInput to the inner tool args', async () => {
@@ -1520,7 +1520,7 @@ for member in team:
1520
1520
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
1521
1521
  async () => ({
1522
1522
  decision: 'allow',
1523
- updatedInput: { file_path: '/tmp/rewritten' },
1523
+ updatedInput: { path: '/tmp/rewritten' },
1524
1524
  }),
1525
1525
  ],
1526
1526
  });
@@ -1529,10 +1529,10 @@ for member in team:
1529
1529
  { registry, runId: 'r1' },
1530
1530
  'read_file',
1531
1531
  'call_1',
1532
- { file_path: '/tmp/original' }
1532
+ { path: '/tmp/original' }
1533
1533
  );
1534
1534
  expect(gate.denyReason).toBeUndefined();
1535
- expect(gate.input).toEqual({ file_path: '/tmp/rewritten' });
1535
+ expect(gate.input).toEqual({ path: '/tmp/rewritten' });
1536
1536
  });
1537
1537
 
1538
1538
  it('treats `ask` as fail-closed deny (HITL not reachable from bridge)', async () => {
@@ -9,9 +9,9 @@ import { Constants } from '@/common';
9
9
 
10
10
  describe('ReadFile', () => {
11
11
  describe('schema structure', () => {
12
- it('has file_path as required string property', () => {
13
- expect(ReadFileToolSchema.properties.file_path.type).toBe('string');
14
- expect(ReadFileToolSchema.required).toContain('file_path');
12
+ it('has path as required string property', () => {
13
+ expect(ReadFileToolSchema.properties.path.type).toBe('string');
14
+ expect(ReadFileToolSchema.required).toContain('path');
15
15
  });
16
16
 
17
17
  it('is an object type schema', () => {
@@ -1021,7 +1021,7 @@ describe('ToolNode code execution session management', () => {
1021
1021
  {
1022
1022
  id: 'call_rf',
1023
1023
  name: Constants.READ_FILE,
1024
- args: { file_path: '/mnt/data/data.csv' },
1024
+ args: { path: '/mnt/data/data.csv' },
1025
1025
  },
1026
1026
  ],
1027
1027
  });
@@ -1062,7 +1062,7 @@ describe('ToolNode code execution session management', () => {
1062
1062
  {
1063
1063
  id: 'call_rf2',
1064
1064
  name: Constants.READ_FILE,
1065
- args: { file_path: 'some-skill/notes.md' },
1065
+ args: { path: 'some-skill/notes.md' },
1066
1066
  },
1067
1067
  ],
1068
1068
  });