@librechat/agents 3.7.15 → 3.7.17

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 (125) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +91 -0
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/agents/projection.cjs +2 -1
  4. package/dist/cjs/agents/projection.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +47 -6
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +20 -7
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +1 -1
  10. package/dist/cjs/llm/openai/utils/index.cjs +1 -1
  11. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  12. package/dist/cjs/llm/preempt.cjs +1 -10
  13. package/dist/cjs/llm/preempt.cjs.map +1 -1
  14. package/dist/cjs/main.cjs +23 -3
  15. package/dist/cjs/messages/contextPruning.cjs +1 -1
  16. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  17. package/dist/cjs/messages/core.cjs +29 -1
  18. package/dist/cjs/messages/core.cjs.map +1 -1
  19. package/dist/cjs/messages/fading.cjs +122 -0
  20. package/dist/cjs/messages/fading.cjs.map +1 -0
  21. package/dist/cjs/messages/format.cjs +1 -1
  22. package/dist/cjs/messages/index.cjs +1 -0
  23. package/dist/cjs/messages/prune.cjs +364 -236
  24. package/dist/cjs/messages/prune.cjs.map +1 -1
  25. package/dist/cjs/run.cjs +27 -0
  26. package/dist/cjs/run.cjs.map +1 -1
  27. package/dist/cjs/session/AgentSession.cjs +213 -8
  28. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  29. package/dist/cjs/session/JsonlSessionStore.cjs +41 -0
  30. package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -1
  31. package/dist/cjs/session/index.cjs +1 -1
  32. package/dist/cjs/stream.cjs +1 -1
  33. package/dist/cjs/summarization/node.cjs +1 -1
  34. package/dist/cjs/tools/ToolNode.cjs +1 -1
  35. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +2 -0
  36. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  37. package/dist/cjs/tools/subagent/SubagentReplay.cjs +2 -1
  38. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  39. package/dist/cjs/utils/errors.cjs +3 -1
  40. package/dist/cjs/utils/errors.cjs.map +1 -1
  41. package/dist/cjs/utils/index.cjs +1 -1
  42. package/dist/cjs/utils/truncation.cjs +9 -0
  43. package/dist/cjs/utils/truncation.cjs.map +1 -1
  44. package/dist/esm/agents/AgentContext.mjs +92 -1
  45. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  46. package/dist/esm/agents/projection.mjs +2 -1
  47. package/dist/esm/agents/projection.mjs.map +1 -1
  48. package/dist/esm/graphs/Graph.mjs +47 -6
  49. package/dist/esm/graphs/Graph.mjs.map +1 -1
  50. package/dist/esm/graphs/MultiAgentGraph.mjs +20 -7
  51. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  52. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +1 -1
  53. package/dist/esm/llm/openai/utils/index.mjs +2 -2
  54. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  55. package/dist/esm/llm/preempt.mjs +1 -10
  56. package/dist/esm/llm/preempt.mjs.map +1 -1
  57. package/dist/esm/main.mjs +6 -5
  58. package/dist/esm/messages/contextPruning.mjs +1 -1
  59. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  60. package/dist/esm/messages/core.mjs +29 -2
  61. package/dist/esm/messages/core.mjs.map +1 -1
  62. package/dist/esm/messages/fading.mjs +108 -0
  63. package/dist/esm/messages/fading.mjs.map +1 -0
  64. package/dist/esm/messages/format.mjs +1 -1
  65. package/dist/esm/messages/index.mjs +1 -0
  66. package/dist/esm/messages/prune.mjs +363 -235
  67. package/dist/esm/messages/prune.mjs.map +1 -1
  68. package/dist/esm/run.mjs +27 -0
  69. package/dist/esm/run.mjs.map +1 -1
  70. package/dist/esm/session/AgentSession.mjs +213 -8
  71. package/dist/esm/session/AgentSession.mjs.map +1 -1
  72. package/dist/esm/session/JsonlSessionStore.mjs +41 -0
  73. package/dist/esm/session/JsonlSessionStore.mjs.map +1 -1
  74. package/dist/esm/session/index.mjs +1 -1
  75. package/dist/esm/stream.mjs +1 -1
  76. package/dist/esm/summarization/node.mjs +1 -1
  77. package/dist/esm/tools/ToolNode.mjs +1 -1
  78. package/dist/esm/tools/subagent/SubagentExecutor.mjs +2 -0
  79. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  80. package/dist/esm/tools/subagent/SubagentReplay.mjs +2 -1
  81. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  82. package/dist/esm/utils/errors.mjs +3 -1
  83. package/dist/esm/utils/errors.mjs.map +1 -1
  84. package/dist/esm/utils/index.mjs +1 -1
  85. package/dist/esm/utils/truncation.mjs +7 -1
  86. package/dist/esm/utils/truncation.mjs.map +1 -1
  87. package/dist/types/agents/AgentContext.d.ts +32 -1
  88. package/dist/types/agents/projection.d.ts +3 -1
  89. package/dist/types/graphs/Graph.d.ts +14 -1
  90. package/dist/types/graphs/MultiAgentGraph.d.ts +7 -0
  91. package/dist/types/messages/contextPruning.d.ts +2 -0
  92. package/dist/types/messages/core.d.ts +2 -0
  93. package/dist/types/messages/fading.d.ts +81 -0
  94. package/dist/types/messages/index.d.ts +1 -0
  95. package/dist/types/messages/prune.d.ts +71 -24
  96. package/dist/types/run.d.ts +16 -0
  97. package/dist/types/session/AgentSession.d.ts +15 -0
  98. package/dist/types/session/JsonlSessionStore.d.ts +4 -1
  99. package/dist/types/session/types.d.ts +9 -0
  100. package/dist/types/tools/subagent/SubagentReplay.d.ts +3 -1
  101. package/dist/types/types/graph.d.ts +23 -0
  102. package/dist/types/types/run.d.ts +10 -0
  103. package/dist/types/utils/truncation.d.ts +9 -0
  104. package/package.json +1 -1
  105. package/src/agents/AgentContext.ts +175 -1
  106. package/src/agents/projection.ts +4 -0
  107. package/src/graphs/Graph.ts +97 -11
  108. package/src/graphs/MultiAgentGraph.ts +54 -6
  109. package/src/llm/openai/utils/index.ts +3 -1
  110. package/src/llm/preempt.ts +4 -17
  111. package/src/messages/contextPruning.ts +7 -1
  112. package/src/messages/core.ts +52 -0
  113. package/src/messages/fading.ts +301 -0
  114. package/src/messages/index.ts +1 -0
  115. package/src/messages/prune.ts +719 -507
  116. package/src/run.ts +52 -0
  117. package/src/session/AgentSession.ts +397 -8
  118. package/src/session/JsonlSessionStore.ts +71 -0
  119. package/src/session/types.ts +10 -0
  120. package/src/tools/subagent/SubagentExecutor.ts +6 -0
  121. package/src/tools/subagent/SubagentReplay.ts +15 -2
  122. package/src/types/graph.ts +25 -0
  123. package/src/types/run.ts +10 -0
  124. package/src/utils/errors.ts +17 -1
  125. package/src/utils/truncation.ts +25 -0
@@ -54,7 +54,6 @@ import {
54
54
  CALIBRATION_RATIO_MAX,
55
55
  createPruneMessages,
56
56
  projectToolMessagesForProvider,
57
- calculateMaxToolCallInputChars,
58
57
  syncBudgetDerivedFields,
59
58
  addTailCacheControl,
60
59
  resolvePromptCacheTtl,
@@ -156,6 +155,7 @@ import { createContextPressureMeter } from '@/llm/contextPressureMeter';
156
155
  import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
157
156
  import { prepareProviderRequest } from '@/llm/prepareProviderRequest';
158
157
  import { createCloudflareCodingToolBundle } from '@/tools/cloudflare';
158
+ import { calculateMaxToolCallInputChars } from '@/utils/truncation';
159
159
  import { prepareToolsForPromptCache } from '@/llm/promptCacheTools';
160
160
  import { providerRequiresStrictAlternation } from '@/llm/providers';
161
161
  import { buildSubagentToolParams } from '@/tools/SubagentTool';
@@ -163,6 +163,7 @@ import { initializeLangfuseTracing } from '@/instrumentation';
163
163
  import { shouldTriggerSummarization } from '@/summarization';
164
164
  import { isRunStepResumeState } from '@/tools/runStepResume';
165
165
  import { resolveLocalToolsForBinding } from '@/tools/local';
166
+ import { isFadingTier, isInformativeFadingTier } from '@/messages/fading';
166
167
  import { createSummarizeNode } from '@/summarization/node';
167
168
  import { getTruncationStopReason } from '@/llm/truncation';
168
169
  import { messagesStateReducer } from '@/messages/reducer';
@@ -1070,6 +1071,13 @@ export abstract class Graph<
1070
1071
  return [...threadIds];
1071
1072
  }
1072
1073
 
1074
+ protected createSubagentFadingResumeState(): Pick<
1075
+ SubagentGraphResumeState,
1076
+ 'fadingTier' | 'fadingTiers'
1077
+ > {
1078
+ return {};
1079
+ }
1080
+
1073
1081
  createSubagentResumeState(runId: string): SubagentGraphResumeState {
1074
1082
  return {
1075
1083
  toolCallSteps: [...this.toolCallStepIds].map(([toolCallId, stepId]) => ({
@@ -1107,6 +1115,7 @@ export abstract class Graph<
1107
1115
  ],
1108
1116
  eagerToolSuppressions: [...this.eagerEventToolSuppressions],
1109
1117
  runStepState: this.createRunStepResumeState(),
1118
+ ...this.createSubagentFadingResumeState(),
1110
1119
  ...(this._toolOutputRegistry == null
1111
1120
  ? {}
1112
1121
  : {
@@ -1491,6 +1500,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1491
1500
  tokenCounter,
1492
1501
  indexTokenCountMap,
1493
1502
  calibrationRatio,
1503
+ fadingTier,
1504
+ fadingTiers,
1494
1505
  subagentUsageSink,
1495
1506
  subagentTasks,
1496
1507
  subagentScope,
@@ -1539,6 +1550,21 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1539
1550
  if (calibrationRatio != null && calibrationRatio > 0) {
1540
1551
  agentContext.calibrationRatio = calibrationRatio;
1541
1552
  }
1553
+ let restoredFadingTier: t.FadingTier | null | undefined;
1554
+ if (
1555
+ fadingTiers != null &&
1556
+ Object.prototype.hasOwnProperty.call(fadingTiers, agentConfig.agentId)
1557
+ ) {
1558
+ restoredFadingTier = fadingTiers[agentConfig.agentId];
1559
+ } else if (agentConfig.agentId === agents[0].agentId) {
1560
+ restoredFadingTier = fadingTier;
1561
+ }
1562
+ /** A supplied tier is assumed to describe the supplied history. Hosts
1563
+ * must clear it when they create a new summary; this also permits tiers
1564
+ * learned on turns after a durable summary to survive subsequent Runs. */
1565
+ if (isFadingTier(restoredFadingTier)) {
1566
+ agentContext.fadingTier = { ...restoredFadingTier };
1567
+ }
1542
1568
 
1543
1569
  this.agentContexts.set(agentConfig.agentId, agentContext);
1544
1570
  }
@@ -2551,6 +2577,57 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2551
2577
  return context?.calibrationRatio ?? 1;
2552
2578
  }
2553
2579
 
2580
+ /**
2581
+ * Latched context-fading tier for the default agent, or undefined while it
2582
+ * is still fresh. Hosts persist it beside the calibration ratio.
2583
+ */
2584
+ getFadingTier(): t.FadingTier | undefined {
2585
+ const context = this.agentContexts.get(this.defaultAgentId);
2586
+ const tier = context?.fadingTier;
2587
+ return isInformativeFadingTier(tier, context?.maxContextTokens)
2588
+ ? { ...tier }
2589
+ : undefined;
2590
+ }
2591
+
2592
+ /** Latched context-fading tiers keyed by agent ID. */
2593
+ getFadingTiers(): t.FadingTiers {
2594
+ const tiers: Array<[string, t.FadingTier]> = [];
2595
+ for (const [agentId, context] of this.agentContexts) {
2596
+ if (
2597
+ isInformativeFadingTier(context.fadingTier, context.maxContextTokens)
2598
+ ) {
2599
+ tiers.push([agentId, { ...context.fadingTier }]);
2600
+ }
2601
+ }
2602
+ return Object.fromEntries(tiers);
2603
+ }
2604
+
2605
+ /** Agent IDs whose canonical history was compacted during this run. */
2606
+ getFadingTierResetAgentIds(): string[] {
2607
+ return Array.from(this.agentContexts)
2608
+ .filter(([, context]) => context.fadingTierReset)
2609
+ .map(([agentId]) => agentId);
2610
+ }
2611
+
2612
+ /** Whether the default agent compacted canonical history during this run. */
2613
+ didResetFadingTier(): boolean {
2614
+ return (
2615
+ this.agentContexts.get(this.defaultAgentId)?.fadingTierReset === true
2616
+ );
2617
+ }
2618
+
2619
+ protected override createSubagentFadingResumeState(): Pick<
2620
+ SubagentGraphResumeState,
2621
+ 'fadingTier' | 'fadingTiers'
2622
+ > {
2623
+ const fadingTier = this.getFadingTier();
2624
+ const fadingTiers = this.getFadingTiers();
2625
+ return {
2626
+ ...(fadingTier == null ? {} : { fadingTier }),
2627
+ ...(Object.keys(fadingTiers).length === 0 ? {} : { fadingTiers }),
2628
+ };
2629
+ }
2630
+
2554
2631
  getResolvedInstructionOverhead(): number | undefined {
2555
2632
  const context = this.agentContexts.get(this.defaultAgentId);
2556
2633
  return context?.resolvedInstructionOverhead;
@@ -3059,6 +3136,12 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3059
3136
  * same masking record the configured trigger preserves.
3060
3137
  */
3061
3138
  let prunedOriginalToolContent: Map<number, string> | undefined;
3139
+ const canProjectContext =
3140
+ agentContext.tokenCounter != null &&
3141
+ agentContext.maxContextTokens != null;
3142
+ const providerMessages = canProjectContext
3143
+ ? agentContext.getProviderProjectedMessages(messages)
3144
+ : messages;
3062
3145
  if (
3063
3146
  !agentContext.pruneMessages &&
3064
3147
  agentContext.tokenCounter &&
@@ -3080,6 +3163,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3080
3163
  summarizationEnabled: agentContext.summarizationEnabled,
3081
3164
  reserveRatio: agentContext.summarizationConfig?.reserveRatio,
3082
3165
  calibrationRatio: agentContext.calibrationRatio,
3166
+ fadingTier: agentContext.fadingTier,
3083
3167
  getInstructionTokens: () => agentContext.instructionTokens,
3084
3168
  log: (level, message, data) => {
3085
3169
  emitAgentLog(config, level, 'prune', message, data, {
@@ -3098,29 +3182,28 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3098
3182
  remainingContextTokens,
3099
3183
  newOriginalToolContent,
3100
3184
  calibrationRatio,
3185
+ fadingTier,
3101
3186
  resolvedInstructionOverhead,
3102
3187
  contextBudget,
3103
3188
  effectiveInstructionTokens,
3104
3189
  } = agentContext.pruneMessages({
3105
- messages,
3190
+ messages: providerMessages,
3191
+ canonicalMessages: messages,
3192
+ canonicalPrefixStable: true,
3106
3193
  usageMetadata: agentContext.currentUsage,
3107
3194
  lastCallUsage: agentContext.lastCallUsage,
3108
3195
  totalTokensFresh: agentContext.totalTokensFresh,
3109
3196
  });
3110
3197
  prunedOriginalToolContent = newOriginalToolContent;
3111
- /**
3112
- * Masking rewrites tool content in `state.messages` in place, so this
3113
- * map is the only surviving copy of the full output. Persist it on
3114
- * every prune, not just when a summary is about to be written — the
3115
- * pruner closure that produced it is discarded on the next reset, and
3116
- * with it any chance of a later summary restoring the real content.
3117
- * AgentContext bounds what accumulates.
3118
- */
3198
+ /** Preserve the masking record for the existing overflow/summarization
3199
+ * path. The graph history itself remains canonical; only the provider
3200
+ * projection above is rewritten by fading. */
3119
3201
  agentContext.preserveOriginalToolContent(newOriginalToolContent);
3120
3202
  agentContext.indexTokenCountMap = indexTokenCountMap;
3121
3203
  if (calibrationRatio != null && calibrationRatio > 0) {
3122
3204
  agentContext.calibrationRatio = calibrationRatio;
3123
3205
  }
3206
+ agentContext.fadingTier = fadingTier;
3124
3207
  if (resolvedInstructionOverhead != null) {
3125
3208
  agentContext.resolvedInstructionOverhead =
3126
3209
  resolvedInstructionOverhead;
@@ -5031,7 +5114,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
5031
5114
 
5032
5115
  const StateAnnotation = Annotation.Root({
5033
5116
  messages: Annotation<BaseMessage[]>({
5034
- reducer: messagesStateReducer,
5117
+ reducer: (a, b) => {
5118
+ agentContext.invalidateProviderProjectionForMessageUpdates(b);
5119
+ return messagesStateReducer(a, b);
5120
+ },
5035
5121
  default: () => [],
5036
5122
  }),
5037
5123
  summarizationRequest: Annotation<t.SummarizationNodeInput | undefined>({
@@ -30,7 +30,10 @@ import {
30
30
  } from '@/messages/provenance';
31
31
  import { serializeToolContentBounded } from '@/utils/toolContent';
32
32
  import { Constants, MULTI_AGENT_GRAPH_RUN_NAME } from '@/common';
33
- import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
33
+ import {
34
+ calculateMaxToolResultChars,
35
+ HARD_MAX_TOOL_RESULT_CHARS,
36
+ } from '@/utils/truncation';
34
37
  import { StandardGraph } from './Graph';
35
38
 
36
39
  /** Pattern to extract instructions from transfer ToolMessage content */
@@ -970,6 +973,29 @@ export class MultiAgentGraph extends StandardGraph {
970
973
  * @param agentId - The agent ID to check for handoff reception
971
974
  * @returns Object with filtered messages, extracted instructions, source agent, and parallel siblings
972
975
  */
976
+ /**
977
+ * Character cap for text injected into an agent's context as a routing or
978
+ * handoff prompt, derived from that agent's remaining message budget. Fading
979
+ * only shrinks tool content, so a prompt above this cap could overflow the
980
+ * agent's window or force the routing instruction out of its context.
981
+ */
982
+ private async resolveMaxRoutingPromptChars(agentId: string): Promise<number> {
983
+ const agentContext = this.agentContexts.get(agentId);
984
+ if (agentContext?.tokenCalculationPromise != null) {
985
+ await agentContext.tokenCalculationPromise;
986
+ }
987
+ const availableMessageTokens =
988
+ agentContext?.maxContextTokens == null
989
+ ? undefined
990
+ : agentContext.getTokenBudgetBreakdown().availableForMessages;
991
+ if (availableMessageTokens == null) {
992
+ return HARD_MAX_TOOL_RESULT_CHARS;
993
+ }
994
+ return availableMessageTokens <= 0
995
+ ? 0
996
+ : calculateMaxToolResultChars(availableMessageTokens);
997
+ }
998
+
973
999
  private processHandoffReception(
974
1000
  messages: BaseMessage[],
975
1001
  agentId: string
@@ -1358,6 +1384,12 @@ export class MultiAgentGraph extends StandardGraph {
1358
1384
  */
1359
1385
  const hasInstructions = instructions !== null && instructions !== '';
1360
1386
  if (hasInstructions) {
1387
+ /** Bounded like a direct-edge routing prompt: a HumanMessage is
1388
+ * not tool content, so fading could never shrink it later. */
1389
+ const boundedInstructions = serializeToolContentBounded(
1390
+ instructions,
1391
+ await this.resolveMaxRoutingPromptChars(agentId)
1392
+ );
1361
1393
  const lastMsg =
1362
1394
  filteredMessages.length > 0
1363
1395
  ? filteredMessages[filteredMessages.length - 1]
@@ -1371,12 +1403,12 @@ export class MultiAgentGraph extends StandardGraph {
1371
1403
  `[Processed tool result and transferring to ${agentId}]`
1372
1404
  )
1373
1405
  ),
1374
- buildRoutingPrompt(instructions),
1406
+ buildRoutingPrompt(boundedInstructions),
1375
1407
  ];
1376
1408
  } else {
1377
1409
  messagesForAgent = [
1378
1410
  ...filteredMessages,
1379
- buildRoutingPrompt(instructions),
1411
+ buildRoutingPrompt(boundedInstructions),
1380
1412
  ];
1381
1413
  }
1382
1414
  }
@@ -1576,18 +1608,34 @@ export class MultiAgentGraph extends StandardGraph {
1576
1608
  builder.addNode(wrapperNodeId, async (state: t.BaseGraphState) => {
1577
1609
  let promptText: string | undefined;
1578
1610
  let effectiveExcludeResults = excludeResults;
1611
+ const getMaxRoutingPromptChars = (): Promise<number> =>
1612
+ this.resolveMaxRoutingPromptChars(destination);
1579
1613
 
1580
1614
  if (typeof prompt === 'function') {
1581
- promptText = await prompt(state.messages, this.startIndex);
1615
+ const resolvedPrompt = await prompt(state.messages, this.startIndex);
1616
+ promptText =
1617
+ resolvedPrompt == null
1618
+ ? undefined
1619
+ : serializeToolContentBounded(
1620
+ resolvedPrompt,
1621
+ await getMaxRoutingPromptChars()
1622
+ );
1582
1623
  } else if (prompt != null) {
1583
1624
  if (prompt.includes('{results}')) {
1584
1625
  const resultsMessages = state.messages.slice(this.startIndex);
1585
- const resultsString = getBufferString(resultsMessages);
1626
+ const maxRoutingPromptChars = await getMaxRoutingPromptChars();
1627
+ const resultsString = serializeToolContentBounded(
1628
+ getBufferString(resultsMessages),
1629
+ maxRoutingPromptChars
1630
+ );
1586
1631
  const promptTemplate = PromptTemplate.fromTemplate(prompt);
1587
1632
  const result = await promptTemplate.invoke({
1588
1633
  results: resultsString,
1589
1634
  });
1590
- promptText = result.value;
1635
+ promptText = serializeToolContentBounded(
1636
+ result.value,
1637
+ maxRoutingPromptChars
1638
+ );
1591
1639
  effectiveExcludeResults =
1592
1640
  excludeResults !== false && promptText !== '';
1593
1641
  } else {
@@ -48,11 +48,13 @@ import {
48
48
  OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER,
49
49
  } from '@/tools/streamedToolCallSeals';
50
50
  import {
51
+ HARD_MAX_TOOL_RESULT_CHARS,
51
52
  calculateMaxToolCallInputChars,
53
+ } from '@/utils/truncation';
54
+ import {
52
55
  projectToolCallInputs,
53
56
  serializeToolCallInput,
54
57
  } from '@/messages/prune';
55
- import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
56
58
  import { toLangChainContent } from '@/messages/langchain';
57
59
 
58
60
  export type { OpenAICallOptions, OpenAIChatInput };
@@ -5,7 +5,10 @@ import {
5
5
  DEFAULT_MAX_SEALS,
6
6
  DEFAULT_PREEMPT_RESTART_GRACE_MS,
7
7
  } from '@/common';
8
- import { isReasoningContentBlock } from '@/messages/core';
8
+ import {
9
+ hasNonEmptyTextContent,
10
+ isReasoningContentBlock,
11
+ } from '@/messages/core';
9
12
 
10
13
  /**
11
14
  * Normalizes a host-supplied seal budget.
@@ -33,22 +36,6 @@ export function resolveMaxSeals(maxSeals: number | undefined): number {
33
36
  * stripped or signed on several providers and cannot stand in for the visible
34
37
  * assistant turn a sealed sequence needs.
35
38
  */
36
- function hasNonEmptyTextContent(content: AIMessageChunk['content']): boolean {
37
- if (typeof content === 'string') {
38
- return content.trim() !== '';
39
- }
40
- for (const block of content) {
41
- if (block.type !== ContentTypes.TEXT) {
42
- continue;
43
- }
44
- const text = block[ContentTypes.TEXT];
45
- if (typeof text === 'string' && text.trim() !== '') {
46
- return true;
47
- }
48
- }
49
- return false;
50
- }
51
-
52
39
  /**
53
40
  * True while a Gemini server-side tool call is still awaiting its response.
54
41
  *
@@ -60,6 +60,8 @@ export interface ContextPruningResult {
60
60
  */
61
61
  export function applyContextPruning(params: {
62
62
  messages: BaseMessage[];
63
+ /** Full graph history used only to decide whether a projected result is eligible. */
64
+ canonicalMessages?: BaseMessage[];
63
65
  indexTokenCountMap: Record<string, number | undefined>;
64
66
  tokenCounter: TokenCounter;
65
67
  config?: ContextPruningConfig;
@@ -138,7 +140,11 @@ export function applyContextPruning(params: {
138
140
  }
139
141
  const content = message.content;
140
142
  const contentLength = getToolContentCharLength(content);
141
- if (contentLength < settings.minPrunableToolChars) {
143
+ const eligibilityContent = params.canonicalMessages?.[i]?.content ?? content;
144
+ if (
145
+ getToolContentCharLength(eligibilityContent) <
146
+ settings.minPrunableToolChars
147
+ ) {
142
148
  continue;
143
149
  }
144
150
 
@@ -2793,3 +2793,55 @@ export function findLastIndex<T>(
2793
2793
  }
2794
2794
  return -1;
2795
2795
  }
2796
+
2797
+ /** Reads an own data property without invoking accessors; `undefined` otherwise. */
2798
+ function readOwnDataProperty(target: object, key: PropertyKey): unknown {
2799
+ try {
2800
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
2801
+ return descriptor != null && 'value' in descriptor ? descriptor.value : undefined;
2802
+ } catch {
2803
+ return undefined;
2804
+ }
2805
+ }
2806
+
2807
+ /** Whether message content carries non-whitespace text in any text block. */
2808
+ export function hasNonEmptyTextContent(content: BaseMessage['content']): boolean {
2809
+ if (typeof content === 'string') {
2810
+ return content.trim() !== '';
2811
+ }
2812
+ if (!Array.isArray(content) || isProxy(content)) {
2813
+ return false;
2814
+ }
2815
+ /** Index reads through own data descriptors: `for...of` would invoke a
2816
+ * hostile `Symbol.iterator` or an accessor-backed element before any guard. */
2817
+ const length = readOwnDataProperty(content, 'length');
2818
+ const blockCount = typeof length === 'number' ? length : 0;
2819
+ for (let i = 0; i < blockCount; i++) {
2820
+ const block = readOwnDataProperty(content, i);
2821
+ if (typeof block !== 'object' || block == null || isProxy(block)) {
2822
+ continue;
2823
+ }
2824
+ try {
2825
+ const type = Object.getOwnPropertyDescriptor(block, 'type');
2826
+ if (
2827
+ type == null ||
2828
+ !('value' in type) ||
2829
+ (type.value !== ContentTypes.TEXT && type.value !== 'output_text')
2830
+ ) {
2831
+ continue;
2832
+ }
2833
+ const text = Object.getOwnPropertyDescriptor(block, ContentTypes.TEXT);
2834
+ if (
2835
+ text != null &&
2836
+ 'value' in text &&
2837
+ typeof text.value === 'string' &&
2838
+ text.value.trim() !== ''
2839
+ ) {
2840
+ return true;
2841
+ }
2842
+ } catch {
2843
+ continue;
2844
+ }
2845
+ }
2846
+ return false;
2847
+ }