@librechat/agents 3.3.3 → 3.3.5

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 (274) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +2 -1
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/constants.cjs +21 -0
  4. package/dist/cjs/common/constants.cjs.map +1 -1
  5. package/dist/cjs/events.cjs +10 -1
  6. package/dist/cjs/events.cjs.map +1 -1
  7. package/dist/cjs/graphs/Graph.cjs +793 -111
  8. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
  10. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  11. package/dist/cjs/hooks/HookRegistry.cjs +25 -0
  12. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  13. package/dist/cjs/hooks/index.cjs +12 -0
  14. package/dist/cjs/hooks/index.cjs.map +1 -1
  15. package/dist/cjs/hooks/types.cjs +1 -0
  16. package/dist/cjs/hooks/types.cjs.map +1 -1
  17. package/dist/cjs/langfuse.cjs +8 -0
  18. package/dist/cjs/langfuse.cjs.map +1 -1
  19. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
  20. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  21. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +38 -13
  22. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  23. package/dist/cjs/llm/google/utils/common.cjs +19 -7
  24. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  25. package/dist/cjs/llm/invoke.cjs +331 -5
  26. package/dist/cjs/llm/invoke.cjs.map +1 -1
  27. package/dist/cjs/llm/openai/index.cjs +41 -5
  28. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  29. package/dist/cjs/llm/openai/utils/index.cjs +25 -4
  30. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  31. package/dist/cjs/llm/openrouter/index.cjs +4 -1
  32. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  33. package/dist/cjs/llm/preempt.cjs +132 -0
  34. package/dist/cjs/llm/preempt.cjs.map +1 -0
  35. package/dist/cjs/main.cjs +36 -5
  36. package/dist/cjs/messages/alternation.cjs +92 -0
  37. package/dist/cjs/messages/alternation.cjs.map +1 -0
  38. package/dist/cjs/messages/cache.cjs +54 -33
  39. package/dist/cjs/messages/cache.cjs.map +1 -1
  40. package/dist/cjs/messages/contextPruning.cjs +17 -43
  41. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  42. package/dist/cjs/messages/core.cjs +315 -23
  43. package/dist/cjs/messages/core.cjs.map +1 -1
  44. package/dist/cjs/messages/format.cjs +290 -60
  45. package/dist/cjs/messages/format.cjs.map +1 -1
  46. package/dist/cjs/messages/handoffCue.cjs +63 -0
  47. package/dist/cjs/messages/handoffCue.cjs.map +1 -0
  48. package/dist/cjs/messages/index.cjs +4 -1
  49. package/dist/cjs/messages/injected.cjs +60 -0
  50. package/dist/cjs/messages/injected.cjs.map +1 -0
  51. package/dist/cjs/messages/prune.cjs +503 -127
  52. package/dist/cjs/messages/prune.cjs.map +1 -1
  53. package/dist/cjs/run.cjs +80 -7
  54. package/dist/cjs/run.cjs.map +1 -1
  55. package/dist/cjs/session/handlers.cjs +18 -9
  56. package/dist/cjs/session/handlers.cjs.map +1 -1
  57. package/dist/cjs/stream.cjs +35 -10
  58. package/dist/cjs/stream.cjs.map +1 -1
  59. package/dist/cjs/summarization/node.cjs +28 -14
  60. package/dist/cjs/summarization/node.cjs.map +1 -1
  61. package/dist/cjs/tools/ToolNode.cjs +56 -63
  62. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  63. package/dist/cjs/tools/search/keenable-scraper.cjs +90 -0
  64. package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -0
  65. package/dist/cjs/tools/search/tool.cjs +9 -1
  66. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  67. package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
  68. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  69. package/dist/cjs/utils/index.cjs +1 -1
  70. package/dist/cjs/utils/llm.cjs +1 -1
  71. package/dist/cjs/utils/llm.cjs.map +1 -1
  72. package/dist/cjs/utils/tokens.cjs +307 -20
  73. package/dist/cjs/utils/tokens.cjs.map +1 -1
  74. package/dist/cjs/utils/toolContent.cjs +1514 -0
  75. package/dist/cjs/utils/toolContent.cjs.map +1 -0
  76. package/dist/esm/agents/AgentContext.mjs +2 -1
  77. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  78. package/dist/esm/common/constants.mjs +19 -1
  79. package/dist/esm/common/constants.mjs.map +1 -1
  80. package/dist/esm/events.mjs +10 -1
  81. package/dist/esm/events.mjs.map +1 -1
  82. package/dist/esm/graphs/Graph.mjs +798 -116
  83. package/dist/esm/graphs/Graph.mjs.map +1 -1
  84. package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
  85. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  86. package/dist/esm/hooks/HookRegistry.mjs +25 -0
  87. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  88. package/dist/esm/hooks/index.mjs +12 -1
  89. package/dist/esm/hooks/index.mjs.map +1 -1
  90. package/dist/esm/hooks/types.mjs +1 -0
  91. package/dist/esm/hooks/types.mjs.map +1 -1
  92. package/dist/esm/langfuse.mjs +9 -1
  93. package/dist/esm/langfuse.mjs.map +1 -1
  94. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
  95. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  96. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +38 -13
  97. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  98. package/dist/esm/llm/google/utils/common.mjs +19 -7
  99. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  100. package/dist/esm/llm/invoke.mjs +332 -8
  101. package/dist/esm/llm/invoke.mjs.map +1 -1
  102. package/dist/esm/llm/openai/index.mjs +41 -5
  103. package/dist/esm/llm/openai/index.mjs.map +1 -1
  104. package/dist/esm/llm/openai/utils/index.mjs +25 -4
  105. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  106. package/dist/esm/llm/openrouter/index.mjs +4 -1
  107. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  108. package/dist/esm/llm/preempt.mjs +131 -0
  109. package/dist/esm/llm/preempt.mjs.map +1 -0
  110. package/dist/esm/main.mjs +14 -11
  111. package/dist/esm/messages/alternation.mjs +91 -0
  112. package/dist/esm/messages/alternation.mjs.map +1 -0
  113. package/dist/esm/messages/cache.mjs +55 -34
  114. package/dist/esm/messages/cache.mjs.map +1 -1
  115. package/dist/esm/messages/contextPruning.mjs +17 -43
  116. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  117. package/dist/esm/messages/core.mjs +305 -24
  118. package/dist/esm/messages/core.mjs.map +1 -1
  119. package/dist/esm/messages/format.mjs +290 -61
  120. package/dist/esm/messages/format.mjs.map +1 -1
  121. package/dist/esm/messages/handoffCue.mjs +61 -0
  122. package/dist/esm/messages/handoffCue.mjs.map +1 -0
  123. package/dist/esm/messages/index.mjs +4 -1
  124. package/dist/esm/messages/injected.mjs +60 -0
  125. package/dist/esm/messages/injected.mjs.map +1 -0
  126. package/dist/esm/messages/prune.mjs +500 -128
  127. package/dist/esm/messages/prune.mjs.map +1 -1
  128. package/dist/esm/run.mjs +80 -7
  129. package/dist/esm/run.mjs.map +1 -1
  130. package/dist/esm/session/handlers.mjs +19 -10
  131. package/dist/esm/session/handlers.mjs.map +1 -1
  132. package/dist/esm/stream.mjs +34 -11
  133. package/dist/esm/stream.mjs.map +1 -1
  134. package/dist/esm/summarization/node.mjs +28 -14
  135. package/dist/esm/summarization/node.mjs.map +1 -1
  136. package/dist/esm/tools/ToolNode.mjs +56 -63
  137. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  138. package/dist/esm/tools/search/keenable-scraper.mjs +88 -0
  139. package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -0
  140. package/dist/esm/tools/search/tool.mjs +9 -1
  141. package/dist/esm/tools/search/tool.mjs.map +1 -1
  142. package/dist/esm/tools/toolOutputReferences.mjs +6 -0
  143. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  144. package/dist/esm/utils/index.mjs +1 -1
  145. package/dist/esm/utils/llm.mjs +1 -1
  146. package/dist/esm/utils/llm.mjs.map +1 -1
  147. package/dist/esm/utils/tokens.mjs +307 -21
  148. package/dist/esm/utils/tokens.mjs.map +1 -1
  149. package/dist/esm/utils/toolContent.mjs +1503 -0
  150. package/dist/esm/utils/toolContent.mjs.map +1 -0
  151. package/dist/types/common/constants.d.ts +18 -0
  152. package/dist/types/graphs/Graph.d.ts +139 -2
  153. package/dist/types/hooks/HookRegistry.d.ts +15 -0
  154. package/dist/types/hooks/index.d.ts +12 -1
  155. package/dist/types/hooks/types.d.ts +45 -6
  156. package/dist/types/index.d.ts +1 -1
  157. package/dist/types/llm/invoke.d.ts +34 -1
  158. package/dist/types/llm/openai/index.d.ts +4 -0
  159. package/dist/types/llm/openai/utils/index.d.ts +2 -0
  160. package/dist/types/llm/openrouter/index.d.ts +5 -0
  161. package/dist/types/llm/preempt.d.ts +40 -0
  162. package/dist/types/messages/alternation.d.ts +23 -0
  163. package/dist/types/messages/contextPruning.d.ts +2 -1
  164. package/dist/types/messages/core.d.ts +36 -0
  165. package/dist/types/messages/format.d.ts +6 -0
  166. package/dist/types/messages/handoffCue.d.ts +40 -0
  167. package/dist/types/messages/index.d.ts +3 -0
  168. package/dist/types/messages/injected.d.ts +3 -0
  169. package/dist/types/messages/prune.d.ts +16 -12
  170. package/dist/types/run.d.ts +7 -0
  171. package/dist/types/stream.d.ts +14 -0
  172. package/dist/types/tools/ToolNode.d.ts +0 -7
  173. package/dist/types/tools/search/keenable-scraper.d.ts +15 -0
  174. package/dist/types/tools/search/types.d.ts +31 -2
  175. package/dist/types/types/graph.d.ts +8 -1
  176. package/dist/types/types/run.d.ts +65 -0
  177. package/dist/types/types/stream.d.ts +1 -26
  178. package/dist/types/utils/tokens.d.ts +7 -0
  179. package/dist/types/utils/toolContent.d.ts +107 -0
  180. package/package.json +1 -1
  181. package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
  182. package/src/agents/AgentContext.ts +1 -0
  183. package/src/{splitStream.test.ts → aggregator.test.ts} +59 -666
  184. package/src/common/constants.ts +21 -0
  185. package/src/events.ts +15 -1
  186. package/src/graphs/Graph.ts +1261 -198
  187. package/src/graphs/MultiAgentGraph.ts +6 -1
  188. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1139 -4
  189. package/src/graphs/__tests__/Graph.preemptSignal.test.ts +126 -0
  190. package/src/hooks/HookRegistry.ts +40 -0
  191. package/src/hooks/__tests__/preemptBoundary.test.ts +152 -0
  192. package/src/hooks/index.ts +16 -2
  193. package/src/hooks/types.ts +47 -3
  194. package/src/index.ts +1 -1
  195. package/src/langfuse.ts +26 -1
  196. package/src/llm/anthropic/utils/message_inputs.ts +158 -12
  197. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
  198. package/src/llm/bedrock/utils/message_inputs.test.ts +131 -1
  199. package/src/llm/bedrock/utils/message_inputs.ts +100 -24
  200. package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
  201. package/src/llm/google/utils/common.test.ts +75 -9
  202. package/src/llm/google/utils/common.ts +34 -3
  203. package/src/llm/invoke.alternation.test.ts +87 -0
  204. package/src/llm/invoke.handoffCue.test.ts +112 -0
  205. package/src/llm/invoke.test.ts +488 -0
  206. package/src/llm/invoke.ts +611 -8
  207. package/src/llm/openai/index.ts +106 -6
  208. package/src/llm/openai/utils/index.ts +109 -53
  209. package/src/llm/openai/utils/messages.test.ts +330 -1
  210. package/src/llm/openrouter/index.ts +12 -2
  211. package/src/llm/openrouter/reasoning.test.ts +314 -0
  212. package/src/llm/preempt.test.ts +323 -0
  213. package/src/llm/preempt.ts +178 -0
  214. package/src/messages/__tests__/observationMasking.test.ts +93 -2
  215. package/src/messages/alternation.test.ts +212 -0
  216. package/src/messages/alternation.ts +112 -0
  217. package/src/messages/cache.tail.test.ts +193 -0
  218. package/src/messages/cache.test.ts +113 -0
  219. package/src/messages/cache.ts +92 -60
  220. package/src/messages/contextPruning.test.ts +184 -0
  221. package/src/messages/contextPruning.ts +49 -42
  222. package/src/messages/core.ts +653 -40
  223. package/src/messages/ensureThinkingBlock.test.ts +49 -1
  224. package/src/messages/foldToollessToolBlocks.test.ts +163 -5
  225. package/src/messages/format.ts +454 -91
  226. package/src/messages/formatAgentMessages.steer.test.ts +267 -0
  227. package/src/messages/formatAgentMessages.test.ts +604 -14
  228. package/src/messages/formatAgentMessages.tools.test.ts +103 -0
  229. package/src/messages/handoffCue.test.ts +96 -0
  230. package/src/messages/handoffCue.ts +78 -0
  231. package/src/messages/index.ts +3 -0
  232. package/src/messages/injected.test.ts +90 -0
  233. package/src/messages/injected.ts +74 -0
  234. package/src/messages/prune.ts +996 -183
  235. package/src/run.ts +91 -6
  236. package/src/scripts/preempt-probe.ts +330 -0
  237. package/src/scripts/preempt-scenarios.ts +388 -0
  238. package/src/session/handlers.ts +32 -12
  239. package/src/specs/handoffCue.test.ts +165 -0
  240. package/src/specs/langfuse-callbacks.test.ts +352 -2
  241. package/src/specs/preemptSeal.test.ts +309 -0
  242. package/src/specs/prune.test.ts +1083 -6
  243. package/src/specs/summarization-unit.test.ts +105 -0
  244. package/src/specs/tokens.test.ts +609 -32
  245. package/src/stream.dispatch.test.ts +63 -0
  246. package/src/stream.ts +59 -23
  247. package/src/summarization/__tests__/node.test.ts +77 -0
  248. package/src/summarization/node.ts +46 -18
  249. package/src/tools/ToolNode.ts +150 -99
  250. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
  251. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
  252. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
  253. package/src/tools/__tests__/directToolHooks.test.ts +255 -7
  254. package/src/tools/search/keenable-scraper.test.ts +153 -0
  255. package/src/tools/search/keenable-scraper.ts +137 -0
  256. package/src/tools/search/tool.ts +13 -2
  257. package/src/tools/search/types.ts +50 -3
  258. package/src/tools/toolOutputReferences.ts +6 -0
  259. package/src/types/graph.ts +12 -1
  260. package/src/types/run.ts +67 -0
  261. package/src/types/stream.ts +1 -42
  262. package/src/utils/llm.test.ts +18 -0
  263. package/src/utils/llm.ts +4 -1
  264. package/src/utils/tokens.ts +586 -31
  265. package/src/utils/toolContent.test.ts +1432 -0
  266. package/src/utils/toolContent.ts +2707 -0
  267. package/dist/cjs/splitStream.cjs +0 -151
  268. package/dist/cjs/splitStream.cjs.map +0 -1
  269. package/dist/esm/splitStream.mjs +0 -150
  270. package/dist/esm/splitStream.mjs.map +0 -1
  271. package/dist/types/mockStream.d.ts +0 -32
  272. package/dist/types/splitStream.d.ts +0 -37
  273. package/src/mockStream.ts +0 -99
  274. package/src/splitStream.ts +0 -234
@@ -1,16 +1,10 @@
1
- import {
2
- AIMessage,
3
- BaseMessage,
4
- ToolMessage,
5
- HumanMessage,
6
- SystemMessage,
7
- MessageContentComplex,
8
- } from '@langchain/core/messages';
1
+ import { isProxy } from 'node:util/types';
2
+ import { BaseMessage, MessageContentComplex } from '@langchain/core/messages';
9
3
  import type Anthropic from '@anthropic-ai/sdk';
10
4
  import type { AnthropicMessage } from '@/types/messages';
5
+ import { hasComputerCallOutputMarker } from '@/utils/toolContent';
11
6
  import { toLangChainContent } from './langchain';
12
7
  import { ContentTypes } from '@/common/enum';
13
- import { withMessageRole } from './format';
14
8
 
15
9
  type MessageWithContent = {
16
10
  content?: string | MessageContentComplex[];
@@ -134,13 +128,30 @@ export function resolveBedrockPromptCacheTtl(
134
128
  * Deep clones a message's content to prevent mutation of the original.
135
129
  */
136
130
  function deepCloneContent<T extends string | MessageContentComplex[]>(
137
- content: T
131
+ content: T,
132
+ omittedProperty?: string
138
133
  ): T {
139
134
  if (typeof content === 'string') {
140
135
  return content;
141
136
  }
142
137
  if (Array.isArray(content)) {
143
- return content.map((block) => ({ ...block })) as T;
138
+ return content.map((block) => {
139
+ if (typeof block !== 'object' || block == null || isProxy(block)) {
140
+ return block;
141
+ }
142
+ try {
143
+ const descriptors = Object.getOwnPropertyDescriptors(block);
144
+ if (omittedProperty != null) {
145
+ delete descriptors[omittedProperty];
146
+ }
147
+ return Object.create(
148
+ Object.getPrototypeOf(block),
149
+ descriptors
150
+ ) as MessageContentComplex;
151
+ } catch {
152
+ return block;
153
+ }
154
+ }) as T;
144
155
  }
145
156
  return content;
146
157
  }
@@ -156,45 +167,36 @@ export function cloneMessage<T extends MessageWithContent>(
156
167
  content: string | MessageContentComplex[]
157
168
  ): T {
158
169
  if (message instanceof BaseMessage) {
159
- const baseParams = {
160
- content: toLangChainContent(content),
161
- additional_kwargs: { ...message.additional_kwargs },
162
- response_metadata: { ...message.response_metadata },
163
- id: message.id,
164
- name: message.name,
170
+ const descriptors = Object.getOwnPropertyDescriptors(message) as Record<
171
+ PropertyKey,
172
+ PropertyDescriptor | undefined
173
+ >;
174
+ const contentDescriptor = descriptors.content;
175
+ descriptors.content = {
176
+ configurable: contentDescriptor?.configurable ?? true,
177
+ enumerable: contentDescriptor?.enumerable ?? true,
178
+ value: toLangChainContent(content),
179
+ writable: contentDescriptor?.writable ?? true,
165
180
  };
166
-
167
- const msgType = message.getType();
168
- switch (msgType) {
169
- case 'ai':
170
- return withMessageRole(
171
- new AIMessage({
172
- ...baseParams,
173
- tool_calls: (message as unknown as AIMessage).tool_calls,
174
- }),
175
- 'assistant'
176
- ) as unknown as T;
177
- case 'human':
178
- return withMessageRole(
179
- new HumanMessage(baseParams),
180
- 'user'
181
- ) as unknown as T;
182
- case 'system':
183
- return withMessageRole(
184
- new SystemMessage(baseParams),
185
- 'system'
186
- ) as unknown as T;
187
- case 'tool':
188
- return withMessageRole(
189
- new ToolMessage({
190
- ...baseParams,
191
- tool_call_id: (message as unknown as ToolMessage).tool_call_id,
192
- }),
193
- 'tool'
194
- ) as unknown as T;
195
- default:
196
- break;
181
+ const lcKwargs = descriptors.lc_kwargs;
182
+ if (
183
+ lcKwargs != null &&
184
+ 'value' in lcKwargs &&
185
+ typeof lcKwargs.value === 'object' &&
186
+ lcKwargs.value != null
187
+ ) {
188
+ descriptors.lc_kwargs = {
189
+ ...lcKwargs,
190
+ value: {
191
+ ...(lcKwargs.value as Record<string, unknown>),
192
+ content: toLangChainContent(content),
193
+ },
194
+ };
197
195
  }
196
+ return Object.create(
197
+ Object.getPrototypeOf(message),
198
+ descriptors as PropertyDescriptorMap
199
+ ) as T;
198
200
  }
199
201
 
200
202
  const {
@@ -251,7 +253,11 @@ function sanitizeBedrockSystemMessage<T extends MessageWithContent>(
251
253
  let modified = false;
252
254
  for (const block of content) {
253
255
  if (isCachePoint(block)) {
254
- const existing = (block as { cachePoint?: { ttl?: unknown } }).cachePoint;
256
+ const descriptor = Object.getOwnPropertyDescriptor(block, 'cachePoint');
257
+ const existing =
258
+ descriptor != null && 'value' in descriptor
259
+ ? (descriptor.value as { ttl?: unknown } | undefined)
260
+ : undefined;
255
261
  const desired = buildBedrockCachePoint(ttl);
256
262
  if (existing?.ttl !== desired.ttl) {
257
263
  modified = true;
@@ -378,8 +384,17 @@ export function addCacheControl<T extends AnthropicMessage | BaseMessage>(
378
384
  /**
379
385
  * Checks if a content block is a cache point
380
386
  */
381
- function isCachePoint(block: MessageContentComplex): boolean {
382
- return 'cachePoint' in block && !('type' in block);
387
+ function isCachePoint(block: unknown): boolean {
388
+ if (typeof block !== 'object' || block == null || isProxy(block)) {
389
+ return false;
390
+ }
391
+ try {
392
+ const cachePoint = Object.getOwnPropertyDescriptor(block, 'cachePoint');
393
+ const type = Object.getOwnPropertyDescriptor(block, 'type');
394
+ return cachePoint?.enumerable === true && type == null;
395
+ } catch {
396
+ return false;
397
+ }
383
398
  }
384
399
 
385
400
  /**
@@ -460,8 +475,13 @@ export function addTailCacheControl<T extends AnthropicMessage | BaseMessage>(
460
475
  const originalMessage = updatedMessages[i];
461
476
  const content = originalMessage.content;
462
477
  const hasArrayContent = Array.isArray(content);
478
+ const isComputerOutput =
479
+ originalMessage instanceof BaseMessage &&
480
+ hasComputerCallOutputMarker(originalMessage);
463
481
  const canPlaceMarker =
464
- !markerPlaced && !isSyntheticMetaMessage(originalMessage);
482
+ !markerPlaced &&
483
+ !isSyntheticMetaMessage(originalMessage) &&
484
+ !isComputerOutput;
465
485
 
466
486
  // Earlier string-content messages carry no markers to strip.
467
487
  if (!canPlaceMarker && !hasArrayContent) {
@@ -706,7 +726,21 @@ export function addCacheControlToStablePrefixMessages<
706
726
  */
707
727
  function hasAnthropicCacheControl(content: MessageContentComplex[]): boolean {
708
728
  for (let i = 0; i < content.length; i++) {
709
- if ('cache_control' in content[i]) return true;
729
+ const block = content[i];
730
+ if (typeof block !== 'object' || block == null || isProxy(block)) {
731
+ continue;
732
+ }
733
+ try {
734
+ const cacheControl = Object.getOwnPropertyDescriptor(
735
+ block,
736
+ 'cache_control'
737
+ );
738
+ if (cacheControl?.enumerable === true) {
739
+ return true;
740
+ }
741
+ } catch {
742
+ continue;
743
+ }
710
744
  }
711
745
  return false;
712
746
  }
@@ -733,13 +767,7 @@ export function stripAnthropicCacheControl<T extends MessageWithContent>(
733
767
  continue;
734
768
  }
735
769
 
736
- const clonedContent = deepCloneContent(content);
737
- for (let j = 0; j < clonedContent.length; j++) {
738
- const block = clonedContent[j] as Record<string, unknown>;
739
- if ('cache_control' in block) {
740
- delete block.cache_control;
741
- }
742
- }
770
+ const clonedContent = deepCloneContent(content, 'cache_control');
743
771
  updatedMessages[i] = cloneMessage(originalMessage, clonedContent);
744
772
  }
745
773
 
@@ -961,10 +989,14 @@ export function addBedrockTailCacheControl<
961
989
  'lc_namespace' in originalMessage;
962
990
  const hasArrayContent = Array.isArray(content);
963
991
  const isEmptyString = typeof content === 'string' && content === '';
992
+ const isComputerOutput =
993
+ originalMessage instanceof BaseMessage &&
994
+ hasComputerCallOutputMarker(originalMessage);
964
995
  const canPlaceCachePoint =
965
996
  !cachePointPlaced &&
966
997
  !isEmptyString &&
967
998
  !isSyntheticMetaMessage(originalMessage) &&
999
+ !isComputerOutput &&
968
1000
  (typeof content === 'string' || hasArrayContent);
969
1001
 
970
1002
  if (!canPlaceCachePoint && !hasArrayContent && !hasSerializationProps) {
@@ -0,0 +1,184 @@
1
+ import {
2
+ AIMessage,
3
+ HumanMessage,
4
+ ToolMessage,
5
+ type BaseMessage,
6
+ } from '@langchain/core/messages';
7
+ import { applyContextPruning } from './contextPruning';
8
+
9
+ function charCounter(message: BaseMessage): number {
10
+ return (
11
+ typeof message.content === 'string'
12
+ ? message.content
13
+ : JSON.stringify(message.content)
14
+ ).length;
15
+ }
16
+
17
+ describe('applyContextPruning', () => {
18
+ it('soft-trims old structured tool results and preserves metadata', () => {
19
+ const toolCallId = 'tc-old';
20
+ const messages: BaseMessage[] = [
21
+ new HumanMessage('query the table'),
22
+ new AIMessage({
23
+ content: '',
24
+ tool_calls: [
25
+ {
26
+ id: toolCallId,
27
+ name: 'run_select_query',
28
+ args: {},
29
+ type: 'tool_call',
30
+ },
31
+ ],
32
+ }),
33
+ new ToolMessage({
34
+ content: [
35
+ {
36
+ type: 'json',
37
+ rows: Array.from({ length: 20 }, (_, index) => ({
38
+ id: index,
39
+ value: `${'x'.repeat(100)}-${index}`,
40
+ })),
41
+ },
42
+ ],
43
+ tool_call_id: toolCallId,
44
+ name: 'run_select_query',
45
+ status: 'success',
46
+ artifact: { source: 'clickhouse' },
47
+ }),
48
+ new AIMessage('The query returned 20 rows.'),
49
+ new HumanMessage('continue'),
50
+ new AIMessage('Ready.'),
51
+ new HumanMessage('next question'),
52
+ ];
53
+ const indexTokenCountMap: Record<string, number | undefined> = {};
54
+ for (let i = 0; i < messages.length; i++) {
55
+ indexTokenCountMap[i] = charCounter(messages[i]);
56
+ }
57
+
58
+ const result = applyContextPruning({
59
+ messages,
60
+ indexTokenCountMap,
61
+ tokenCounter: charCounter,
62
+ config: {
63
+ enabled: true,
64
+ keepLastAssistants: 1,
65
+ softTrimRatio: 0,
66
+ minPrunableToolChars: 1,
67
+ softTrim: { maxChars: 200, headChars: 80, tailChars: 40 },
68
+ hardClear: { enabled: false },
69
+ },
70
+ });
71
+
72
+ expect(result.softTrimmed).toBe(1);
73
+ expect(typeof messages[2].content).toBe('string');
74
+ expect(messages[2].content).toContain('soft-trimmed');
75
+ const trimmed = messages[2] as ToolMessage;
76
+ expect(trimmed.tool_call_id).toBe(toolCallId);
77
+ expect(trimmed.name).toBe('run_select_query');
78
+ expect(trimmed.status).toBe('success');
79
+ expect(trimmed.artifact).toEqual({ source: 'clickhouse' });
80
+ expect(indexTokenCountMap[2]).toBe(charCounter(trimmed));
81
+ });
82
+
83
+ it('preserves small non-image media blocks while trimming adjacent text', () => {
84
+ const document = {
85
+ type: 'document',
86
+ source: { type: 'url', url: 'https://example.com/report.pdf' },
87
+ };
88
+ const messages: BaseMessage[] = [
89
+ new HumanMessage('read the report'),
90
+ new AIMessage({
91
+ content: '',
92
+ tool_calls: [
93
+ {
94
+ id: 'tc-document',
95
+ name: 'read_document',
96
+ args: {},
97
+ type: 'tool_call',
98
+ },
99
+ ],
100
+ }),
101
+ new ToolMessage({
102
+ content: [{ type: 'text', text: 'x'.repeat(2_000) }, document],
103
+ tool_call_id: 'tc-document',
104
+ }),
105
+ new AIMessage('I read the report.'),
106
+ new HumanMessage('continue'),
107
+ new AIMessage('Ready.'),
108
+ new HumanMessage('next question'),
109
+ ];
110
+ const indexTokenCountMap: Record<string, number | undefined> = {};
111
+ for (let i = 0; i < messages.length; i++) {
112
+ indexTokenCountMap[i] = charCounter(messages[i]);
113
+ }
114
+
115
+ const result = applyContextPruning({
116
+ messages,
117
+ indexTokenCountMap,
118
+ tokenCounter: charCounter,
119
+ config: {
120
+ enabled: true,
121
+ keepLastAssistants: 1,
122
+ softTrimRatio: 0,
123
+ minPrunableToolChars: 1,
124
+ softTrim: { maxChars: 400, headChars: 80, tailChars: 40 },
125
+ hardClear: { enabled: false },
126
+ },
127
+ });
128
+
129
+ expect(result.softTrimmed).toBe(1);
130
+ expect(Array.isArray(messages[2].content)).toBe(true);
131
+ expect(messages[2].content).toContain(document);
132
+ });
133
+
134
+ it('does not hard-clear native computer screenshots', () => {
135
+ const screenshot = `data:image/png;base64,${'A'.repeat(2_000)}`;
136
+ const computerOutput = new ToolMessage({
137
+ content: screenshot,
138
+ tool_call_id: 'tc-computer',
139
+ additional_kwargs: { type: 'computer_call_output' },
140
+ });
141
+ const messages: BaseMessage[] = [
142
+ new HumanMessage('take a screenshot'),
143
+ new AIMessage({
144
+ content: '',
145
+ tool_calls: [
146
+ {
147
+ id: 'tc-computer',
148
+ name: 'computer',
149
+ args: {},
150
+ type: 'tool_call',
151
+ },
152
+ ],
153
+ }),
154
+ computerOutput,
155
+ new AIMessage('Done.'),
156
+ new HumanMessage('continue'),
157
+ new AIMessage('Ready.'),
158
+ new HumanMessage('next question'),
159
+ ];
160
+ const indexTokenCountMap: Record<string, number | undefined> = {};
161
+ for (let i = 0; i < messages.length; i++) {
162
+ indexTokenCountMap[i] = charCounter(messages[i]);
163
+ }
164
+
165
+ const result = applyContextPruning({
166
+ messages,
167
+ indexTokenCountMap,
168
+ tokenCounter: charCounter,
169
+ config: {
170
+ enabled: true,
171
+ keepLastAssistants: 1,
172
+ softTrimRatio: 0,
173
+ minPrunableToolChars: 1,
174
+ softTrim: { maxChars: 80, headChars: 20, tailChars: 20 },
175
+ hardClear: { enabled: true },
176
+ },
177
+ });
178
+
179
+ expect(result.softTrimmed).toBe(0);
180
+ expect(result.hardCleared).toBe(0);
181
+ expect(messages[2]).toBe(computerOutput);
182
+ expect(messages[2].content).toBe(screenshot);
183
+ });
184
+ });
@@ -9,32 +9,24 @@
9
9
  * - Hard-clear: Replace entire content with a placeholder.
10
10
  *
11
11
  * Messages in the "protected zone" (recent assistant turns, system/pre-first-human
12
- * messages, and messages with image content) are never pruned.
12
+ * messages) are never pruned. Atomic media/resource blocks are preserved when
13
+ * they fit; oversized inline payloads are replaced with bounded placeholders.
13
14
  */
14
15
 
15
16
  import { ToolMessage, type BaseMessage } from '@langchain/core/messages';
16
17
  import type { ContextPruningSettings } from './contextPruningSettings';
17
18
  import type { ContextPruningConfig } from '@/types/graph';
18
19
  import type { TokenCounter } from '@/types/run';
20
+ import {
21
+ cloneToolMessageWithContent,
22
+ compactToolContent,
23
+ getToolContentCharLength,
24
+ isAtomicToolContentBlock,
25
+ isComputerCallOutputMessage,
26
+ serializeToolContentBounded,
27
+ } from '@/utils/toolContent';
19
28
  import { resolveContextPruningSettings } from './contextPruningSettings';
20
29
 
21
- /**
22
- * Checks if a message contains image content blocks.
23
- * Messages with images are skipped by position-based content degradation
24
- * because images cannot be meaningfully soft-trimmed or replaced with placeholders.
25
- */
26
- function hasImageContent(message: BaseMessage): boolean {
27
- if (!Array.isArray(message.content)) {
28
- return false;
29
- }
30
- return message.content.some(
31
- (block) =>
32
- typeof block === 'object' &&
33
- 'type' in block &&
34
- (block.type === 'image_url' || block.type === 'image')
35
- );
36
- }
37
-
38
30
  /**
39
31
  * Applies head+tail soft-trim to tool result content.
40
32
  */
@@ -138,48 +130,63 @@ export function applyContextPruning(params: {
138
130
  if (message.getType() !== 'tool') {
139
131
  continue;
140
132
  }
141
- if (protectedIndices.has(i)) {
133
+ if (isComputerCallOutputMessage(message)) {
142
134
  continue;
143
135
  }
144
- if (hasImageContent(message)) {
136
+ if (protectedIndices.has(i)) {
145
137
  continue;
146
138
  }
147
-
148
139
  const content = message.content;
149
- if (typeof content !== 'string') {
150
- continue;
151
- }
152
- if (content.length < settings.minPrunableToolChars) {
140
+ const contentLength = getToolContentCharLength(content);
141
+ if (contentLength < settings.minPrunableToolChars) {
153
142
  continue;
154
143
  }
155
144
 
156
145
  // Compute age ratio: how far back from the end (0 = latest, 1 = oldest)
157
146
  const ageRatio = (totalMessages - i) / totalMessages;
158
147
 
148
+ if (
149
+ Array.isArray(content) &&
150
+ content.some(isAtomicToolContentBlock) &&
151
+ ageRatio >= settings.softTrimRatio
152
+ ) {
153
+ const compacted = compactToolContent(content, settings.softTrim.maxChars);
154
+ if (compacted.changed) {
155
+ const cloned = cloneToolMessageWithContent(
156
+ message as ToolMessage,
157
+ compacted.content
158
+ );
159
+ messages[i] = cloned;
160
+ indexTokenCountMap[i] = tokenCounter(cloned);
161
+ softTrimmed++;
162
+ }
163
+ continue;
164
+ }
165
+
159
166
  if (ageRatio >= settings.hardClearRatio && settings.hardClear.enabled) {
160
167
  // Hard-clear: replace with placeholder
161
- const cloned = new ToolMessage({
162
- content: settings.hardClear.placeholder,
163
- tool_call_id: (message as ToolMessage).tool_call_id,
164
- name: message.name,
165
- id: message.id,
166
- additional_kwargs: message.additional_kwargs,
167
- response_metadata: message.response_metadata,
168
- });
168
+ const cloned = cloneToolMessageWithContent(
169
+ message as ToolMessage,
170
+ settings.hardClear.placeholder
171
+ );
169
172
  messages[i] = cloned;
170
173
  indexTokenCountMap[i] = tokenCounter(cloned);
171
174
  hardCleared++;
172
175
  } else if (ageRatio >= settings.softTrimRatio) {
173
176
  // Soft-trim: keep head + tail
174
- if (content.length > settings.softTrim.maxChars) {
175
- const cloned = new ToolMessage({
176
- content: softTrimContent(content, settings.softTrim),
177
- tool_call_id: (message as ToolMessage).tool_call_id,
178
- name: message.name,
179
- id: message.id,
180
- additional_kwargs: message.additional_kwargs,
181
- response_metadata: message.response_metadata,
182
- });
177
+ if (contentLength > settings.softTrim.maxChars) {
178
+ const cloned = cloneToolMessageWithContent(
179
+ message as ToolMessage,
180
+ softTrimContent(
181
+ typeof content === 'string'
182
+ ? content
183
+ : serializeToolContentBounded(
184
+ content,
185
+ settings.softTrim.maxChars
186
+ ),
187
+ settings.softTrim
188
+ )
189
+ );
183
190
  messages[i] = cloned;
184
191
  indexTokenCountMap[i] = tokenCounter(cloned);
185
192
  softTrimmed++;