@librechat/agents 3.7.7 → 3.7.9

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 (167) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +26 -5
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/constants.cjs +12 -0
  4. package/dist/cjs/common/constants.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +56 -11
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
  8. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  9. package/dist/cjs/hooks/executeHooks.cjs +8 -0
  10. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  11. package/dist/cjs/hooks/index.cjs +2 -0
  12. package/dist/cjs/hooks/index.cjs.map +1 -1
  13. package/dist/cjs/hooks/types.cjs +1 -0
  14. package/dist/cjs/hooks/types.cjs.map +1 -1
  15. package/dist/cjs/langfuse.cjs +43 -3
  16. package/dist/cjs/langfuse.cjs.map +1 -1
  17. package/dist/cjs/langfuseTraceShaping.cjs +75 -0
  18. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  19. package/dist/cjs/llm/bedrock/toolCache.cjs +1 -1
  20. package/dist/cjs/llm/fake.cjs +9 -5
  21. package/dist/cjs/llm/fake.cjs.map +1 -1
  22. package/dist/cjs/llm/invoke.cjs +3 -7
  23. package/dist/cjs/llm/invoke.cjs.map +1 -1
  24. package/dist/cjs/llm/openrouter/toolCache.cjs +1 -1
  25. package/dist/cjs/llm/prepareProviderRequest.cjs +2 -2
  26. package/dist/cjs/llm/providers.cjs +1 -1
  27. package/dist/cjs/llm/request.cjs +6 -0
  28. package/dist/cjs/llm/request.cjs.map +1 -1
  29. package/dist/cjs/llm/truncation.cjs +1 -0
  30. package/dist/cjs/main.cjs +22 -13
  31. package/dist/cjs/messages/format.cjs +375 -3
  32. package/dist/cjs/messages/format.cjs.map +1 -1
  33. package/dist/cjs/messages/index.cjs +1 -1
  34. package/dist/cjs/messages/prune.cjs +1 -1
  35. package/dist/cjs/run.cjs +217 -52
  36. package/dist/cjs/run.cjs.map +1 -1
  37. package/dist/cjs/session/AgentSession.cjs +1 -1
  38. package/dist/cjs/session/JsonlSessionStore.cjs +1 -1
  39. package/dist/cjs/stream.cjs +1 -1
  40. package/dist/cjs/summarization/index.cjs +1 -0
  41. package/dist/cjs/summarization/index.cjs.map +1 -1
  42. package/dist/cjs/summarization/node.cjs +58 -85
  43. package/dist/cjs/summarization/node.cjs.map +1 -1
  44. package/dist/cjs/summarization/semanticIndex.cjs +366 -0
  45. package/dist/cjs/summarization/semanticIndex.cjs.map +1 -0
  46. package/dist/cjs/summarization/shared.cjs +83 -0
  47. package/dist/cjs/summarization/shared.cjs.map +1 -0
  48. package/dist/cjs/tools/ToolNode.cjs +5 -5
  49. package/dist/cjs/tools/local/CompileCheckTool.cjs +1 -1
  50. package/dist/cjs/tools/local/LocalCodingTools.cjs +1 -1
  51. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +1 -1
  52. package/dist/cjs/tools/runStepResume.cjs +1 -1
  53. package/dist/cjs/tools/runStepResume.cjs.map +1 -1
  54. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +8 -12
  55. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  56. package/dist/cjs/tools/subagent/childGraphConfig.cjs +2 -1
  57. package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -1
  58. package/dist/cjs/utils/index.cjs +1 -1
  59. package/dist/cjs/utils/tokens.cjs +8 -1
  60. package/dist/cjs/utils/tokens.cjs.map +1 -1
  61. package/dist/esm/agents/AgentContext.mjs +26 -5
  62. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  63. package/dist/esm/common/constants.mjs +11 -1
  64. package/dist/esm/common/constants.mjs.map +1 -1
  65. package/dist/esm/graphs/Graph.mjs +56 -11
  66. package/dist/esm/graphs/Graph.mjs.map +1 -1
  67. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  68. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  69. package/dist/esm/hooks/executeHooks.mjs +8 -1
  70. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  71. package/dist/esm/hooks/index.mjs +2 -1
  72. package/dist/esm/hooks/index.mjs.map +1 -1
  73. package/dist/esm/hooks/types.mjs +1 -0
  74. package/dist/esm/hooks/types.mjs.map +1 -1
  75. package/dist/esm/langfuse.mjs +43 -3
  76. package/dist/esm/langfuse.mjs.map +1 -1
  77. package/dist/esm/langfuseTraceShaping.mjs +75 -0
  78. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  79. package/dist/esm/llm/bedrock/toolCache.mjs +1 -1
  80. package/dist/esm/llm/fake.mjs +9 -5
  81. package/dist/esm/llm/fake.mjs.map +1 -1
  82. package/dist/esm/llm/invoke.mjs +3 -7
  83. package/dist/esm/llm/invoke.mjs.map +1 -1
  84. package/dist/esm/llm/openrouter/toolCache.mjs +1 -1
  85. package/dist/esm/llm/prepareProviderRequest.mjs +2 -2
  86. package/dist/esm/llm/providers.mjs +1 -1
  87. package/dist/esm/llm/request.mjs +6 -1
  88. package/dist/esm/llm/request.mjs.map +1 -1
  89. package/dist/esm/llm/truncation.mjs +1 -1
  90. package/dist/esm/main.mjs +15 -14
  91. package/dist/esm/messages/format.mjs +375 -3
  92. package/dist/esm/messages/format.mjs.map +1 -1
  93. package/dist/esm/messages/index.mjs +1 -1
  94. package/dist/esm/messages/prune.mjs +1 -1
  95. package/dist/esm/run.mjs +217 -52
  96. package/dist/esm/run.mjs.map +1 -1
  97. package/dist/esm/session/AgentSession.mjs +1 -1
  98. package/dist/esm/session/JsonlSessionStore.mjs +1 -1
  99. package/dist/esm/stream.mjs +1 -1
  100. package/dist/esm/summarization/index.mjs +1 -0
  101. package/dist/esm/summarization/index.mjs.map +1 -1
  102. package/dist/esm/summarization/node.mjs +57 -84
  103. package/dist/esm/summarization/node.mjs.map +1 -1
  104. package/dist/esm/summarization/semanticIndex.mjs +363 -0
  105. package/dist/esm/summarization/semanticIndex.mjs.map +1 -0
  106. package/dist/esm/summarization/shared.mjs +79 -0
  107. package/dist/esm/summarization/shared.mjs.map +1 -0
  108. package/dist/esm/tools/ToolNode.mjs +5 -5
  109. package/dist/esm/tools/local/CompileCheckTool.mjs +1 -1
  110. package/dist/esm/tools/local/LocalCodingTools.mjs +1 -1
  111. package/dist/esm/tools/local/LocalExecutionEngine.mjs +1 -1
  112. package/dist/esm/tools/runStepResume.mjs +1 -1
  113. package/dist/esm/tools/runStepResume.mjs.map +1 -1
  114. package/dist/esm/tools/subagent/SubagentExecutor.mjs +8 -12
  115. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  116. package/dist/esm/tools/subagent/childGraphConfig.mjs +2 -1
  117. package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -1
  118. package/dist/esm/utils/index.mjs +1 -1
  119. package/dist/esm/utils/tokens.mjs +8 -2
  120. package/dist/esm/utils/tokens.mjs.map +1 -1
  121. package/dist/types/agents/AgentContext.d.ts +27 -1
  122. package/dist/types/common/constants.d.ts +17 -0
  123. package/dist/types/graphs/Graph.d.ts +24 -0
  124. package/dist/types/hooks/executeHooks.d.ts +5 -2
  125. package/dist/types/hooks/index.d.ts +7 -2
  126. package/dist/types/hooks/types.d.ts +45 -7
  127. package/dist/types/langfuse.d.ts +3 -1
  128. package/dist/types/llm/fake.d.ts +12 -2
  129. package/dist/types/llm/request.d.ts +10 -0
  130. package/dist/types/messages/format.d.ts +17 -2
  131. package/dist/types/run.d.ts +19 -6
  132. package/dist/types/summarization/index.d.ts +6 -0
  133. package/dist/types/summarization/node.d.ts +0 -4
  134. package/dist/types/summarization/semanticIndex.d.ts +21 -0
  135. package/dist/types/summarization/shared.d.ts +25 -0
  136. package/dist/types/types/graph.d.ts +9 -1
  137. package/dist/types/types/run.d.ts +8 -0
  138. package/dist/types/types/stream.d.ts +6 -0
  139. package/dist/types/types/summarize.d.ts +44 -0
  140. package/dist/types/utils/tokens.d.ts +9 -0
  141. package/package.json +2 -1
  142. package/src/agents/AgentContext.ts +63 -6
  143. package/src/common/constants.ts +19 -0
  144. package/src/graphs/Graph.ts +86 -8
  145. package/src/hooks/HookRegistry.ts +3 -1
  146. package/src/hooks/executeHooks.ts +20 -2
  147. package/src/hooks/index.ts +12 -1
  148. package/src/hooks/types.ts +47 -4
  149. package/src/langfuse.ts +70 -0
  150. package/src/langfuseTraceShaping.ts +94 -0
  151. package/src/llm/fake.ts +35 -4
  152. package/src/llm/invoke.ts +13 -29
  153. package/src/llm/request.ts +24 -0
  154. package/src/messages/format.ts +839 -5
  155. package/src/run.ts +393 -187
  156. package/src/summarization/index.ts +11 -0
  157. package/src/summarization/node.ts +202 -158
  158. package/src/summarization/semanticIndex.ts +662 -0
  159. package/src/summarization/shared.ts +130 -0
  160. package/src/tools/runStepResume.ts +9 -0
  161. package/src/tools/subagent/SubagentExecutor.ts +4 -27
  162. package/src/tools/subagent/childGraphConfig.ts +3 -0
  163. package/src/types/graph.ts +9 -0
  164. package/src/types/run.ts +8 -0
  165. package/src/types/stream.ts +6 -0
  166. package/src/types/summarize.ts +57 -0
  167. package/src/utils/tokens.ts +31 -8
@@ -1,5 +1,16 @@
1
1
  import type { SummarizationTrigger } from '@/types';
2
2
 
3
+ /**
4
+ * The summary-boundary seam a caller compacting outside a run needs. The two
5
+ * default prompts stay out of it: LibreChat's manual flow deliberately words
6
+ * its own, so exporting these would publish an API with no consumer.
7
+ */
8
+ export {
9
+ buildSummarizationInstruction,
10
+ buildSummaryCarrierText,
11
+ separateSummarizationParameters,
12
+ } from './shared';
13
+
3
14
  const VALID_TRIGGER_TYPES = [
4
15
  'token_ratio',
5
16
  'remaining_tokens',
@@ -1,16 +1,27 @@
1
- import {
2
- AIMessage,
3
- ToolMessage,
4
- HumanMessage,
5
- SystemMessage,
6
- } from '@langchain/core/messages';
1
+ import { AIMessage, ToolMessage, HumanMessage } from '@langchain/core/messages';
7
2
  import type { UsageMetadata, BaseMessage } from '@langchain/core/messages';
8
3
  import type { RunnableConfig } from '@langchain/core/runnables';
4
+ import type { RenderedCompactionSemanticIndex } from '@/summarization/semanticIndex';
9
5
  import type { StreamLimitState } from '@/llm/streamLimits';
10
6
  import type { AgentContext } from '@/agents/AgentContext';
7
+ import type { EncodingName } from '@/utils/tokens';
11
8
  import type { HookRegistry } from '@/hooks';
12
9
  import type { OnChunk } from '@/llm/invoke';
13
10
  import type * as t from '@/types';
11
+ import {
12
+ DEFAULT_SUMMARIZATION_PROMPT,
13
+ DEFAULT_UPDATE_SUMMARIZATION_PROMPT,
14
+ buildSummaryCarrierText,
15
+ separateSummarizationParameters,
16
+ buildSummarizationInstruction,
17
+ } from './shared';
18
+ import {
19
+ addTailCacheControl,
20
+ addBedrockTailCacheControl,
21
+ resolvePromptCacheTtl,
22
+ resolveBedrockPromptCacheTtl,
23
+ type PromptCacheTtl,
24
+ } from '@/messages/cache';
14
25
  import {
15
26
  cloneToolMessageWithContent,
16
27
  compactToolContent,
@@ -22,18 +33,16 @@ import {
22
33
  StreamLimitExceededError,
23
34
  STREAM_LIMIT_EPOCH_KEY,
24
35
  } from '@/llm/streamLimits';
25
- import {
26
- addTailCacheControl,
27
- addBedrockTailCacheControl,
28
- resolvePromptCacheTtl,
29
- resolveBedrockPromptCacheTtl,
30
- type PromptCacheTtl,
31
- } from '@/messages/cache';
32
36
  import {
33
37
  DEFAULT_RETAIN_RECENT_TURNS,
34
38
  resolveIntraTurnRetainTokens,
35
39
  splitAtRecencyBoundary,
36
40
  } from '@/messages/recency';
41
+ import {
42
+ createTokenCounter,
43
+ encodingForModel,
44
+ encodingOfTokenCounter,
45
+ } from '@/utils/tokens';
37
46
  import {
38
47
  Constants,
39
48
  ContentTypes,
@@ -41,19 +50,22 @@ import {
41
50
  StepTypes,
42
51
  Providers,
43
52
  } from '@/common';
53
+ import {
54
+ getMaxOutputTokensKey,
55
+ resolveClientOptionsModel,
56
+ } from '@/llm/request';
57
+ import { renderCompactionSemanticIndex } from '@/summarization/semanticIndex';
44
58
  import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
45
- import { attemptInvoke, tryFallbackProviders } from '@/llm/invoke';
46
59
  import { prepareToolsForPromptCache } from '@/llm/promptCacheTools';
60
+ import { attemptInvoke, tryFallbackProviders } from '@/llm/invoke';
47
61
  import { calculateMaxToolResultChars } from '@/utils/truncation';
48
62
  import { makeIsDeferred } from '@/messages/anthropicToolCache';
49
63
  import { createRemoveAllMessage } from '@/messages/reducer';
50
- import { getMaxOutputTokensKey } from '@/llm/request';
64
+ import { getProviderFamily } from '@/llm/providerRegistry';
51
65
  import { initializeModel } from '@/llm/init';
52
66
  import { getChunkContent } from '@/stream';
53
67
  import { executeHooks } from '@/hooks';
54
68
 
55
- const SUMMARIZATION_PARAM_KEYS = new Set(['maxSummaryTokens']);
56
-
57
69
  /**
58
70
  * Default number of recent user-led turns preserved verbatim during
59
71
  * compaction. A turn begins at a HumanMessage and includes every
@@ -64,89 +76,6 @@ const SUMMARIZATION_PARAM_KEYS = new Set(['maxSummaryTokens']);
64
76
  * `retainRecent.turns` to `0` reverts to the legacy "summarize every
65
77
  * message" behavior.
66
78
  */
67
- /**
68
- * Token overhead of the XML wrapper + instruction text added around the
69
- * summary at injection time in AgentContext.buildSystemRunnable:
70
- * `<summary>\n${text}\n</summary>\n\nYour context window was compacted...`
71
- * ~33 tokens on Anthropic, ~24-27 on OpenAI. Using 33 as a safe ceiling.
72
- */
73
- const SUMMARY_WRAPPER_OVERHEAD_TOKENS = 33;
74
-
75
- /** Structured checkpoint prompt for fresh summarization (no prior summary). */
76
- export const DEFAULT_SUMMARIZATION_PROMPT = `Hold on, before you continue I need you to write me a checkpoint of everything so far. Your context window is filling up and this checkpoint replaces the messages above, so capture everything you need to pick right back up.
77
-
78
- Don't second-guess or fact-check anything you did, your tool results reflect exactly what happened. If a tool result appears truncated, that's just a display artifact from context management: the tool executed fully. Just record what you did and what you observed. Only the checkpoint, don't respond to me or continue the conversation.
79
-
80
- ## Checkpoint
81
-
82
- ## Goal
83
- What I asked you to do and any sub-goals you identified.
84
-
85
- ## Constraints & Preferences
86
- Any rules, preferences, or configuration I established.
87
-
88
- ## Progress
89
- ### Done
90
- - What you completed and the outcomes
91
-
92
- ### In Progress
93
- - What you're currently working on
94
-
95
- ## Key Decisions
96
- Decisions you made and why.
97
-
98
- ## Next Steps
99
- Concrete task actions remaining, in priority order.
100
-
101
- ## Critical Context
102
- Exact identifiers, names, error messages, URLs, and details you need to preserve verbatim.
103
-
104
- Rules:
105
- - Record what you did and observed, don't judge or re-evaluate it
106
- - For each tool call: the tool name, key inputs, and the outcome
107
- - Preserve exact identifiers, names, errors, and references verbatim
108
- - Short declarative sentences
109
- - Skip empty sections`;
110
-
111
- /** Prompt for re-compaction when a prior summary exists. */
112
- export const DEFAULT_UPDATE_SUMMARIZATION_PROMPT = `Hold on again, update your checkpoint. Merge the new messages into your existing checkpoint and give me a single consolidated replacement.
113
-
114
- Keep it roughly the same length as your last checkpoint. Compress older details to make room for what's new, don't just append. Give recent actions more detail, compress older items to one-liners.
115
-
116
- Don't fact-check or second-guess anything, your tool results are ground truth. If a tool result appears truncated, that's just a display artifact: the tool executed fully. Only the checkpoint, don't respond to me or continue the conversation.
117
-
118
- Rules:
119
- - Merge new progress into existing sections, don't duplicate headers
120
- - Compress older completed items into one-line entries
121
- - Move items from "In Progress" to "Done" when you completed them
122
- - Update "Next Steps" to reflect current task priorities.
123
- - For each new tool call: the tool name, key inputs, and the outcome
124
- - Preserve exact identifiers, names, errors, and references verbatim
125
- - Skip empty sections`;
126
-
127
- function separateParameters(parameters: Record<string, unknown>): {
128
- llmParams: Record<string, unknown>;
129
- maxSummaryTokens?: number;
130
- } {
131
- const llmParams: Record<string, unknown> = {};
132
- let maxSummaryTokens: number | undefined;
133
-
134
- for (const [key, value] of Object.entries(parameters)) {
135
- if (SUMMARIZATION_PARAM_KEYS.has(key)) {
136
- if (
137
- key === 'maxSummaryTokens' &&
138
- typeof value === 'number' &&
139
- value > 0
140
- ) {
141
- maxSummaryTokens = value;
142
- }
143
- } else {
144
- llmParams[key] = value;
145
- }
146
- }
147
-
148
- return { llmParams, maxSummaryTokens };
149
- }
150
79
 
151
80
  /**
152
81
  * Generates a structural metadata summary without making an LLM call.
@@ -338,7 +267,7 @@ function buildSummarizationClientConfig(
338
267
  summarizationConfig?.updatePrompt ?? DEFAULT_UPDATE_SUMMARIZATION_PROMPT;
339
268
 
340
269
  const { llmParams, maxSummaryTokens: paramMaxSummaryTokens } =
341
- separateParameters(parameters);
270
+ separateSummarizationParameters(parameters);
342
271
 
343
272
  const isSelfSummarize = provider === (agentContext.provider as string);
344
273
  const baseOptions =
@@ -373,23 +302,104 @@ function buildSummarizationClientConfig(
373
302
  };
374
303
  }
375
304
 
376
- /** Computes the token count for a summary, preferring provider output tokens when available. */
377
- function computeSummaryTokenCount(
305
+ /**
306
+ * Sizes a summary from the text that will actually be re-injected, carrier
307
+ * included, so the stored count is a measurement rather than a body count plus
308
+ * a remembered constant.
309
+ *
310
+ * The provider's `output_tokens` is deliberately not consulted. On a reasoning
311
+ * summarizer the two diverge by the hidden thinking, which is billed but never
312
+ * written into the checkpoint, so using it here would make every later context
313
+ * calculation reserve room for tokens that are never sent. Provider usage stays
314
+ * exclusively a billing input.
315
+ *
316
+ * A missing host counter is not hypothetical: `shouldSummarizeOverflow` fires
317
+ * precisely when there is nothing to count with, so that branch is the
318
+ * overflow-recovery summary, and its count is persisted and then reserved by
319
+ * `AgentContext.instructionTokens` on the retry. Undercounting it is what makes
320
+ * the retry overflow again, which rules out a character heuristic: measured
321
+ * against `o200k_base` and Anthropic's tokenizer, four-characters-per-token
322
+ * understates base64 by 1.5x and Korean by 4.6x, and coefficients large enough
323
+ * to cover those overestimate English prose by roughly 4x. So this falls back
324
+ * to the tokenizer this package already bundles rather than to an estimate.
325
+ *
326
+ * A host counter that is present is not automatically the right one either. In
327
+ * a heterogeneous multi-agent run `Run.create` derives a single counter from
328
+ * `agents[0]` and `StandardGraph` hands that same counter to every
329
+ * `AgentContext`, so a Claude agent behind a GPT first agent would otherwise
330
+ * measure its carrier in `o200k_base` and under-reserve by up to 1.9x on CJK.
331
+ * When the counter came from `createTokenCounter` its encoding is known, so a
332
+ * disagreement with the receiving agent's encoding takes the bundled path
333
+ * instead. A counter the host built itself is unstamped and stays authoritative:
334
+ * its units are the ones the rest of that host's accounting is denominated in.
335
+ */
336
+ async function computeSummaryTokenCount(
378
337
  summaryText: string,
379
- summaryUsage: Partial<UsageMetadata> | undefined,
380
- tokenCounter?: (message: BaseMessage) => number
381
- ): number {
382
- const providerOutputTokens = Number(summaryUsage?.output_tokens) || 0;
383
- if (providerOutputTokens > 0) {
384
- return providerOutputTokens + SUMMARY_WRAPPER_OVERHEAD_TOKENS;
338
+ agentContext: AgentContext
339
+ ): Promise<number> {
340
+ const carrier = new HumanMessage(buildSummaryCarrierText(summaryText));
341
+ const encoding = encodingForReceivingAgent(agentContext);
342
+ const hostCounter = agentContext.tokenCounter;
343
+ const hostEncoding =
344
+ hostCounter != null ? encodingOfTokenCounter(hostCounter) : undefined;
345
+ if (
346
+ hostCounter != null &&
347
+ (hostEncoding == null || hostEncoding === encoding)
348
+ ) {
349
+ return hostCounter(carrier);
385
350
  }
386
- if (tokenCounter) {
387
- return (
388
- tokenCounter(new SystemMessage(summaryText)) +
389
- SUMMARY_WRAPPER_OVERHEAD_TOKENS
390
- );
351
+ const bundledCounter = await createTokenCounter(encoding);
352
+ return bundledCounter(carrier);
353
+ }
354
+
355
+ /**
356
+ * Encoding of the model that will *receive* the carrier.
357
+ *
358
+ * That is the agent's own model, never the summarizer's. The two are the same
359
+ * only by default: `summarizationConfig.model` is undefined for ordinary
360
+ * self-summarization and can name a different provider entirely when a cheap
361
+ * dedicated summarizer is configured. The count produced here is spent by
362
+ * `AgentContext.instructionTokens` against the agent's context window, so it
363
+ * has to be denominated in the agent's tokenizer, and Anthropic's counts run
364
+ * well above `o200k_base` on the same text: measuring CJK with the wrong one
365
+ * understates it by up to 1.9x.
366
+ *
367
+ * Both signals are read, because only one of them can ever be positive.
368
+ * `encodingForModel` matches the substring `claude`, so a name it accepts is
369
+ * proof, which is what keeps a Claude model reached through Bedrock or
370
+ * OpenRouter resolving to `claude`. A name it rejects is only the absence of
371
+ * proof: `production` is an opaque deployment alias, not a statement that the
372
+ * model behind it is something other than Claude. Both option keys are
373
+ * consulted for that name, since `modelName` is LangChain's alias for `model`
374
+ * and hosts configure agents through either, so reading one key alone would
375
+ * report an unconfigured model and quietly hand a Claude agent the
376
+ * `o200k_base` tokenizer.
377
+ *
378
+ * The provider therefore decides every case the name leaves open, not just the
379
+ * case where no model was recorded at all, and it is read as a family rather
380
+ * than as one enum value: a host can register its own provider with
381
+ * `family: 'anthropic'` (the trait `isThinkingEnabled` already reads the same
382
+ * way), and such a provider serves Claude behind whatever name and deployment
383
+ * alias the host chose. `BEDROCK` is left out by the same rule, since its
384
+ * family is `bedrock` and it also serves Llama, Titan and Mistral. The enum
385
+ * check stays ahead of the family lookup because the registry is populated by
386
+ * importing `@/llm/providers`, which a root-barrel consumer defers.
387
+ *
388
+ * On an opaque alias this deliberately parts ways with `Run.create`, which
389
+ * infers from the model name alone and stamps its counter `o200k_base`. That
390
+ * stamp records which tokenizer the counter is, not which one the agent needs,
391
+ * so the disagreement routes the carrier to the bundled Claude tokenizer by
392
+ * the same rule that rejects another agent's counter.
393
+ */
394
+ function encodingForReceivingAgent(agentContext: AgentContext): EncodingName {
395
+ const model = resolveClientOptionsModel(agentContext.clientOptions);
396
+ if (model != null && encodingForModel(model) === 'claude') {
397
+ return 'claude';
391
398
  }
392
- return 0;
399
+ return agentContext.provider === Providers.ANTHROPIC ||
400
+ getProviderFamily(agentContext.provider) === 'anthropic'
401
+ ? 'claude'
402
+ : 'o200k_base';
393
403
  }
394
404
 
395
405
  /**
@@ -625,6 +635,7 @@ async function executeSummarizationWithFallback(params: {
625
635
  summarizeConfig?: RunnableConfig;
626
636
  stepId: string;
627
637
  usePromptCache: boolean;
638
+ semanticIndex: RenderedCompactionSemanticIndex;
628
639
  log: LogFn;
629
640
  /** Carries the run's stream limits so the event cap covers summary streams. */
630
641
  graph?: StreamLimitState & {
@@ -651,6 +662,7 @@ async function executeSummarizationWithFallback(params: {
651
662
  summarizeConfig,
652
663
  stepId,
653
664
  usePromptCache,
665
+ semanticIndex,
654
666
  log,
655
667
  graph,
656
668
  } = params;
@@ -693,6 +705,7 @@ async function executeSummarizationWithFallback(params: {
693
705
  promptText: clientConfig.promptText,
694
706
  updatePromptText: clientConfig.updatePromptText,
695
707
  priorSummaryText,
708
+ semanticIndexAppendix: semanticIndex.appendix,
696
709
  config: summarizeConfig,
697
710
  stepId,
698
711
  provider: clientConfig.provider,
@@ -704,20 +717,20 @@ async function executeSummarizationWithFallback(params: {
704
717
  clientConfig.provider === Providers.OPENROUTER ||
705
718
  clientConfig.provider === Providers.BEDROCK
706
719
  ? (
707
- clientConfig.clientOptions as {
708
- promptCacheTtl?: PromptCacheTtl;
709
- }
720
+ clientConfig.clientOptions as {
721
+ promptCacheTtl?: PromptCacheTtl;
722
+ }
710
723
  ).promptCacheTtl
711
724
  : undefined,
712
725
  bedrockModelId:
713
726
  clientConfig.provider === Providers.BEDROCK
714
727
  ? resolveBedrockCompactionCacheModel(
715
- clientConfig.clientOptions as
716
- | {
717
- applicationInferenceProfile?: string;
718
- model?: string;
719
- }
720
- | undefined
728
+ clientConfig.clientOptions as
729
+ | {
730
+ applicationInferenceProfile?: string;
731
+ model?: string;
732
+ }
733
+ | undefined
721
734
  )
722
735
  : undefined,
723
736
  log,
@@ -785,7 +798,8 @@ async function executeSummarizationWithFallback(params: {
785
798
  buildSummarizationInstruction(
786
799
  clientConfig.promptText,
787
800
  clientConfig.updatePromptText,
788
- priorSummaryText
801
+ priorSummaryText,
802
+ semanticIndex.appendix
789
803
  )
790
804
  ),
791
805
  ],
@@ -1092,6 +1106,29 @@ export function createSummarizeNode({
1092
1106
  return { summarizationRequest: undefined };
1093
1107
  }
1094
1108
 
1109
+ /**
1110
+ * A summarizer that has already returned nothing several times in a row
1111
+ * keeps returning nothing, and every empty result leaves the message set
1112
+ * exactly as it was — so the next prune cycle re-triggers on identical
1113
+ * state. Stopping here bounds that loop instead of letting the run spend
1114
+ * its recursion budget dispatching empty summary steps.
1115
+ */
1116
+ if (agentContext.summarizationExhausted) {
1117
+ emitAgentLog(
1118
+ config,
1119
+ 'warn',
1120
+ 'summarize',
1121
+ 'Summarization skipped — consecutive attempts produced no usable summary',
1122
+ {
1123
+ failures: agentContext.summarizationFailures,
1124
+ reason: request.reason ?? 'trigger',
1125
+ },
1126
+ { runId: graph.runId, agentId: request.agentId }
1127
+ );
1128
+ agentContext.markSummarizationTriggered(state.messages.length);
1129
+ return { summarizationRequest: undefined };
1130
+ }
1131
+
1095
1132
  const maxCtx = agentContext.maxContextTokens ?? 0;
1096
1133
  if (maxCtx > 0 && agentContext.instructionTokens >= maxCtx) {
1097
1134
  emitAgentLog(
@@ -1188,6 +1225,10 @@ export function createSummarizeNode({
1188
1225
  agentContext,
1189
1226
  agentContext.summarizationConfig
1190
1227
  );
1228
+ const semanticIndex = renderCompactionSemanticIndex(
1229
+ agentContext.compactionSemanticIndex,
1230
+ messagesToRefine
1231
+ );
1191
1232
 
1192
1233
  const stepKey = `summarize-${request.agentId}`;
1193
1234
  const [stepId, stepIndex] = generateStepId(stepKey);
@@ -1229,6 +1270,8 @@ export function createSummarizeNode({
1229
1270
  model: clientConfig.modelName,
1230
1271
  messagesToRefineCount: messagesToRefine.length,
1231
1272
  summaryVersion: agentContext.summaryVersion + 1,
1273
+ semanticIndexEntryCount: semanticIndex.entryCount,
1274
+ semanticIndexCharCount: semanticIndex.charCount,
1232
1275
  } satisfies t.SummarizeStartEvent,
1233
1276
  runnableConfig
1234
1277
  );
@@ -1276,6 +1319,9 @@ export function createSummarizeNode({
1276
1319
  isSelfSummarize: isSelfSummarizeModel,
1277
1320
  hasPromptCache,
1278
1321
  provider: clientConfig.provider,
1322
+ semanticIndexEntryCount: semanticIndex.entryCount,
1323
+ semanticIndexCharCount: semanticIndex.charCount,
1324
+ semanticIndexOmittedEntryCount: semanticIndex.omittedEntryCount,
1279
1325
  });
1280
1326
 
1281
1327
  const summarizeConfig: RunnableConfig | undefined = config
@@ -1291,6 +1337,10 @@ export function createSummarizeNode({
1291
1337
  agentId: request.agentId,
1292
1338
  summarization_provider: clientConfig.provider,
1293
1339
  summarization_model: clientConfig.modelName,
1340
+ compaction_semantic_index_entries: semanticIndex.entryCount,
1341
+ compaction_semantic_index_chars: semanticIndex.charCount,
1342
+ compaction_semantic_index_omitted_entries:
1343
+ semanticIndex.omittedEntryCount,
1294
1344
  /**
1295
1345
  * Per-call model attribution for usage consumers (the subagent
1296
1346
  * usage-capture handler): the summarizer's model can differ from
@@ -1340,6 +1390,7 @@ export function createSummarizeNode({
1340
1390
  summarizeConfig,
1341
1391
  stepId,
1342
1392
  usePromptCache: isSelfSummarizeModel && hasPromptCache,
1393
+ semanticIndex,
1343
1394
  log,
1344
1395
  graph,
1345
1396
  });
@@ -1365,6 +1416,7 @@ export function createSummarizeNode({
1365
1416
  `Summarization failed during ${preservationReason}; keeping history rather than replacing it with a metadata stub`
1366
1417
  );
1367
1418
  agentContext.markSummarizationTriggered(state.messages.length);
1419
+ agentContext.recordSummarizationFailure();
1368
1420
  /**
1369
1421
  * The run step was already dispatched, so it has to be resolved here or
1370
1422
  * consumers tracking step lifecycle keep an unfinished placeholder for
@@ -1393,7 +1445,22 @@ export function createSummarizeNode({
1393
1445
  }
1394
1446
 
1395
1447
  if (!rawText) {
1396
- agentContext.markSummarizationTriggered(0);
1448
+ /**
1449
+ * An empty summary compacts nothing, so the state the pruner sees next
1450
+ * is byte-identical to the one that just triggered. Resetting the guard
1451
+ * to `0` here made `shouldSkipSummarization` answer `false` forever,
1452
+ * and the agent node re-triggered on that unchanged state until the
1453
+ * graph hit its recursion cap — a loop of empty summary steps, each one
1454
+ * a billed model call. Recording the current count keeps the guard
1455
+ * honest; the failure tally bounds the retries once new messages do
1456
+ * arrive and legitimately lift it.
1457
+ */
1458
+ agentContext.markSummarizationTriggered(state.messages.length);
1459
+ agentContext.recordSummarizationFailure();
1460
+ log('warn', 'Summarization produced empty output', {
1461
+ failures: agentContext.summarizationFailures,
1462
+ messagesToRefineCount: messagesToRefine.length,
1463
+ });
1397
1464
  if (runnableConfig) {
1398
1465
  await safeDispatchCustomEvent(
1399
1466
  GraphEvents.ON_SUMMARIZE_COMPLETE,
@@ -1411,10 +1478,9 @@ export function createSummarizeNode({
1411
1478
 
1412
1479
  const summaryText = enrichSummary(rawText, messagesToRefine);
1413
1480
 
1414
- const tokenCount = computeSummaryTokenCount(
1481
+ const tokenCount = await computeSummaryTokenCount(
1415
1482
  summaryText,
1416
- summaryUsage,
1417
- agentContext.tokenCounter
1483
+ agentContext
1418
1484
  );
1419
1485
 
1420
1486
  if (usedIntraTurnFallback) {
@@ -1541,23 +1607,6 @@ function extractResponseText(response: { content: string | object }): string {
1541
1607
  return parts.join('').trim();
1542
1608
  }
1543
1609
 
1544
- function buildSummarizationInstruction(
1545
- promptText: string,
1546
- updatePromptText: string | undefined,
1547
- priorSummaryText: string
1548
- ): string {
1549
- const effectivePrompt = priorSummaryText
1550
- ? (updatePromptText ?? promptText)
1551
- : promptText;
1552
- const parts = [effectivePrompt];
1553
- if (priorSummaryText) {
1554
- parts.push(
1555
- `\n\n<previous-summary>\n${priorSummaryText}\n</previous-summary>`
1556
- );
1557
- }
1558
- return parts.join('');
1559
- }
1560
-
1561
1610
  /** Creates an `onChunk` callback that dispatches `ON_SUMMARIZE_DELTA` events for streaming. */
1562
1611
  export function createSummarizationChunkHandler({
1563
1612
  stepId,
@@ -1666,10 +1715,7 @@ export function applySummarizationHistoryCache(params: {
1666
1715
  if (params.provider === Providers.BEDROCK) {
1667
1716
  return addBedrockTailCacheControl(
1668
1717
  [...params.messages],
1669
- resolveBedrockPromptCacheTtl(
1670
- params.promptCacheTtl,
1671
- params.bedrockModelId
1672
- )
1718
+ resolveBedrockPromptCacheTtl(params.promptCacheTtl, params.bedrockModelId)
1673
1719
  );
1674
1720
  }
1675
1721
  if (
@@ -1685,9 +1731,7 @@ export function applySummarizationHistoryCache(params: {
1685
1731
  }
1686
1732
 
1687
1733
  export function resolveBedrockCompactionCacheModel(
1688
- options:
1689
- | { applicationInferenceProfile?: string; model?: string }
1690
- | undefined
1734
+ options: { applicationInferenceProfile?: string; model?: string } | undefined
1691
1735
  ): string | undefined {
1692
1736
  return options?.model;
1693
1737
  }
@@ -1705,6 +1749,7 @@ async function summarizeWithCacheHit({
1705
1749
  promptText,
1706
1750
  updatePromptText,
1707
1751
  priorSummaryText,
1752
+ semanticIndexAppendix,
1708
1753
  config,
1709
1754
  stepId,
1710
1755
  provider,
@@ -1720,6 +1765,7 @@ async function summarizeWithCacheHit({
1720
1765
  promptText: string;
1721
1766
  updatePromptText?: string;
1722
1767
  priorSummaryText: string;
1768
+ semanticIndexAppendix?: string;
1723
1769
  config?: RunnableConfig;
1724
1770
  stepId?: string;
1725
1771
  provider: t.ProviderName;
@@ -1733,7 +1779,8 @@ async function summarizeWithCacheHit({
1733
1779
  const instruction = buildSummarizationInstruction(
1734
1780
  promptText,
1735
1781
  updatePromptText,
1736
- priorSummaryText
1782
+ priorSummaryText,
1783
+ semanticIndexAppendix
1737
1784
  );
1738
1785
 
1739
1786
  const cachedHistory = applySummarizationHistoryCache({
@@ -1743,10 +1790,7 @@ async function summarizeWithCacheHit({
1743
1790
  promptCacheTtl,
1744
1791
  bedrockModelId,
1745
1792
  });
1746
- const invokeMessages = [
1747
- ...cachedHistory,
1748
- new HumanMessage(instruction),
1749
- ];
1793
+ const invokeMessages = [...cachedHistory, new HumanMessage(instruction)];
1750
1794
 
1751
1795
  const result = await attemptInvoke(
1752
1796
  {