@librechat/agents 3.7.8 → 3.7.10

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 (161) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +3 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/constants.cjs +2 -0
  4. package/dist/cjs/common/constants.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +40 -6
  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/llm/invoke.cjs +2 -6
  18. package/dist/cjs/llm/invoke.cjs.map +1 -1
  19. package/dist/cjs/llm/request.cjs +6 -0
  20. package/dist/cjs/llm/request.cjs.map +1 -1
  21. package/dist/cjs/main.cjs +19 -11
  22. package/dist/cjs/messages/format.cjs +95 -18
  23. package/dist/cjs/messages/format.cjs.map +1 -1
  24. package/dist/cjs/messages/index.cjs +1 -1
  25. package/dist/cjs/run.cjs +201 -46
  26. package/dist/cjs/run.cjs.map +1 -1
  27. package/dist/cjs/session/AgentSession.cjs +1 -1
  28. package/dist/cjs/session/JsonlSessionStore.cjs +1 -1
  29. package/dist/cjs/summarization/index.cjs +1 -0
  30. package/dist/cjs/summarization/index.cjs.map +1 -1
  31. package/dist/cjs/summarization/node.cjs +24 -85
  32. package/dist/cjs/summarization/node.cjs.map +1 -1
  33. package/dist/cjs/summarization/semanticIndex.cjs +7 -3
  34. package/dist/cjs/summarization/semanticIndex.cjs.map +1 -1
  35. package/dist/cjs/summarization/shared.cjs +83 -0
  36. package/dist/cjs/summarization/shared.cjs.map +1 -0
  37. package/dist/cjs/tools/ToolNode.cjs +5 -5
  38. package/dist/cjs/tools/local/CompileCheckTool.cjs +1 -1
  39. package/dist/cjs/tools/local/LocalCodingTools.cjs +1 -1
  40. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +1 -1
  41. package/dist/cjs/tools/runStepResume.cjs +1 -1
  42. package/dist/cjs/tools/runStepResume.cjs.map +1 -1
  43. package/dist/cjs/tools/search/metrics.cjs +234 -0
  44. package/dist/cjs/tools/search/metrics.cjs.map +1 -0
  45. package/dist/cjs/tools/search/rerankers.cjs +80 -68
  46. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  47. package/dist/cjs/tools/search/search.cjs +95 -45
  48. package/dist/cjs/tools/search/search.cjs.map +1 -1
  49. package/dist/cjs/tools/search/tool.cjs +77 -49
  50. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  51. package/dist/cjs/tools/search/utils.cjs.map +1 -1
  52. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +8 -12
  53. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  54. package/dist/cjs/utils/tokens.cjs +8 -1
  55. package/dist/cjs/utils/tokens.cjs.map +1 -1
  56. package/dist/esm/agents/AgentContext.mjs +3 -2
  57. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  58. package/dist/esm/common/constants.mjs +2 -1
  59. package/dist/esm/common/constants.mjs.map +1 -1
  60. package/dist/esm/graphs/Graph.mjs +40 -6
  61. package/dist/esm/graphs/Graph.mjs.map +1 -1
  62. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  63. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  64. package/dist/esm/hooks/executeHooks.mjs +8 -1
  65. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  66. package/dist/esm/hooks/index.mjs +2 -1
  67. package/dist/esm/hooks/index.mjs.map +1 -1
  68. package/dist/esm/hooks/types.mjs +1 -0
  69. package/dist/esm/hooks/types.mjs.map +1 -1
  70. package/dist/esm/langfuse.mjs +43 -3
  71. package/dist/esm/langfuse.mjs.map +1 -1
  72. package/dist/esm/llm/invoke.mjs +2 -6
  73. package/dist/esm/llm/invoke.mjs.map +1 -1
  74. package/dist/esm/llm/request.mjs +6 -1
  75. package/dist/esm/llm/request.mjs.map +1 -1
  76. package/dist/esm/main.mjs +13 -12
  77. package/dist/esm/messages/format.mjs +96 -19
  78. package/dist/esm/messages/format.mjs.map +1 -1
  79. package/dist/esm/messages/index.mjs +1 -1
  80. package/dist/esm/run.mjs +201 -46
  81. package/dist/esm/run.mjs.map +1 -1
  82. package/dist/esm/session/AgentSession.mjs +1 -1
  83. package/dist/esm/session/JsonlSessionStore.mjs +1 -1
  84. package/dist/esm/summarization/index.mjs +1 -0
  85. package/dist/esm/summarization/index.mjs.map +1 -1
  86. package/dist/esm/summarization/node.mjs +21 -82
  87. package/dist/esm/summarization/node.mjs.map +1 -1
  88. package/dist/esm/summarization/semanticIndex.mjs +7 -4
  89. package/dist/esm/summarization/semanticIndex.mjs.map +1 -1
  90. package/dist/esm/summarization/shared.mjs +79 -0
  91. package/dist/esm/summarization/shared.mjs.map +1 -0
  92. package/dist/esm/tools/ToolNode.mjs +5 -5
  93. package/dist/esm/tools/local/CompileCheckTool.mjs +1 -1
  94. package/dist/esm/tools/local/LocalCodingTools.mjs +1 -1
  95. package/dist/esm/tools/local/LocalExecutionEngine.mjs +1 -1
  96. package/dist/esm/tools/runStepResume.mjs +1 -1
  97. package/dist/esm/tools/runStepResume.mjs.map +1 -1
  98. package/dist/esm/tools/search/metrics.mjs +234 -0
  99. package/dist/esm/tools/search/metrics.mjs.map +1 -0
  100. package/dist/esm/tools/search/rerankers.mjs +80 -68
  101. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  102. package/dist/esm/tools/search/search.mjs +96 -46
  103. package/dist/esm/tools/search/search.mjs.map +1 -1
  104. package/dist/esm/tools/search/tool.mjs +77 -49
  105. package/dist/esm/tools/search/tool.mjs.map +1 -1
  106. package/dist/esm/tools/search/utils.mjs.map +1 -1
  107. package/dist/esm/tools/subagent/SubagentExecutor.mjs +8 -12
  108. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  109. package/dist/esm/utils/tokens.mjs +8 -2
  110. package/dist/esm/utils/tokens.mjs.map +1 -1
  111. package/dist/types/common/constants.d.ts +7 -0
  112. package/dist/types/graphs/Graph.d.ts +9 -0
  113. package/dist/types/hooks/executeHooks.d.ts +5 -2
  114. package/dist/types/hooks/index.d.ts +7 -2
  115. package/dist/types/hooks/types.d.ts +45 -7
  116. package/dist/types/langfuse.d.ts +3 -1
  117. package/dist/types/llm/request.d.ts +10 -0
  118. package/dist/types/messages/format.d.ts +8 -2
  119. package/dist/types/run.d.ts +2 -1
  120. package/dist/types/summarization/index.d.ts +6 -0
  121. package/dist/types/summarization/node.d.ts +0 -4
  122. package/dist/types/summarization/semanticIndex.d.ts +2 -0
  123. package/dist/types/summarization/shared.d.ts +25 -0
  124. package/dist/types/tools/search/metrics.d.ts +15 -0
  125. package/dist/types/tools/search/rerankers.d.ts +30 -5
  126. package/dist/types/tools/search/tool.d.ts +7 -1
  127. package/dist/types/tools/search/types.d.ts +88 -4
  128. package/dist/types/tools/search/utils.d.ts +2 -10
  129. package/dist/types/types/run.d.ts +8 -0
  130. package/dist/types/types/stream.d.ts +6 -0
  131. package/dist/types/types/summarize.d.ts +6 -0
  132. package/dist/types/utils/tokens.d.ts +9 -0
  133. package/package.json +1 -1
  134. package/src/agents/AgentContext.ts +2 -5
  135. package/src/common/constants.ts +8 -0
  136. package/src/graphs/Graph.ts +45 -0
  137. package/src/hooks/HookRegistry.ts +3 -1
  138. package/src/hooks/executeHooks.ts +20 -2
  139. package/src/hooks/index.ts +12 -1
  140. package/src/hooks/types.ts +47 -4
  141. package/src/langfuse.ts +70 -0
  142. package/src/llm/invoke.ts +13 -29
  143. package/src/llm/request.ts +24 -0
  144. package/src/messages/format.ts +213 -27
  145. package/src/run.ts +366 -186
  146. package/src/summarization/index.ts +11 -0
  147. package/src/summarization/node.ts +117 -130
  148. package/src/summarization/semanticIndex.ts +16 -7
  149. package/src/summarization/shared.ts +130 -0
  150. package/src/tools/runStepResume.ts +9 -0
  151. package/src/tools/search/metrics.ts +400 -0
  152. package/src/tools/search/rerankers.ts +160 -97
  153. package/src/tools/search/search.ts +139 -56
  154. package/src/tools/search/tool.ts +126 -62
  155. package/src/tools/search/types.ts +104 -4
  156. package/src/tools/search/utils.ts +2 -10
  157. package/src/tools/subagent/SubagentExecutor.ts +4 -27
  158. package/src/types/run.ts +8 -0
  159. package/src/types/stream.ts +6 -0
  160. package/src/types/summarize.ts +7 -0
  161. 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,17 +1,20 @@
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';
9
4
  import type { RenderedCompactionSemanticIndex } from '@/summarization/semanticIndex';
10
5
  import type { StreamLimitState } from '@/llm/streamLimits';
11
6
  import type { AgentContext } from '@/agents/AgentContext';
7
+ import type { EncodingName } from '@/utils/tokens';
12
8
  import type { HookRegistry } from '@/hooks';
13
9
  import type { OnChunk } from '@/llm/invoke';
14
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';
15
18
  import {
16
19
  addTailCacheControl,
17
20
  addBedrockTailCacheControl,
@@ -35,6 +38,11 @@ import {
35
38
  resolveIntraTurnRetainTokens,
36
39
  splitAtRecencyBoundary,
37
40
  } from '@/messages/recency';
41
+ import {
42
+ createTokenCounter,
43
+ encodingForModel,
44
+ encodingOfTokenCounter,
45
+ } from '@/utils/tokens';
38
46
  import {
39
47
  Constants,
40
48
  ContentTypes,
@@ -42,6 +50,10 @@ import {
42
50
  StepTypes,
43
51
  Providers,
44
52
  } from '@/common';
53
+ import {
54
+ getMaxOutputTokensKey,
55
+ resolveClientOptionsModel,
56
+ } from '@/llm/request';
45
57
  import { renderCompactionSemanticIndex } from '@/summarization/semanticIndex';
46
58
  import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
47
59
  import { prepareToolsForPromptCache } from '@/llm/promptCacheTools';
@@ -49,13 +61,11 @@ import { attemptInvoke, tryFallbackProviders } from '@/llm/invoke';
49
61
  import { calculateMaxToolResultChars } from '@/utils/truncation';
50
62
  import { makeIsDeferred } from '@/messages/anthropicToolCache';
51
63
  import { createRemoveAllMessage } from '@/messages/reducer';
52
- import { getMaxOutputTokensKey } from '@/llm/request';
64
+ import { getProviderFamily } from '@/llm/providerRegistry';
53
65
  import { initializeModel } from '@/llm/init';
54
66
  import { getChunkContent } from '@/stream';
55
67
  import { executeHooks } from '@/hooks';
56
68
 
57
- const SUMMARIZATION_PARAM_KEYS = new Set(['maxSummaryTokens']);
58
-
59
69
  /**
60
70
  * Default number of recent user-led turns preserved verbatim during
61
71
  * compaction. A turn begins at a HumanMessage and includes every
@@ -66,89 +76,6 @@ const SUMMARIZATION_PARAM_KEYS = new Set(['maxSummaryTokens']);
66
76
  * `retainRecent.turns` to `0` reverts to the legacy "summarize every
67
77
  * message" behavior.
68
78
  */
69
- /**
70
- * Token overhead of the XML wrapper + instruction text added around the
71
- * summary at injection time in AgentContext.buildSystemRunnable:
72
- * `<summary>\n${text}\n</summary>\n\nYour context window was compacted...`
73
- * ~33 tokens on Anthropic, ~24-27 on OpenAI. Using 33 as a safe ceiling.
74
- */
75
- const SUMMARY_WRAPPER_OVERHEAD_TOKENS = 33;
76
-
77
- /** Structured checkpoint prompt for fresh summarization (no prior summary). */
78
- 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.
79
-
80
- 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.
81
-
82
- ## Checkpoint
83
-
84
- ## Goal
85
- What I asked you to do and any sub-goals you identified.
86
-
87
- ## Constraints & Preferences
88
- Any rules, preferences, or configuration I established.
89
-
90
- ## Progress
91
- ### Done
92
- - What you completed and the outcomes
93
-
94
- ### In Progress
95
- - What you're currently working on
96
-
97
- ## Key Decisions
98
- Decisions you made and why.
99
-
100
- ## Next Steps
101
- Concrete task actions remaining, in priority order.
102
-
103
- ## Critical Context
104
- Exact identifiers, names, error messages, URLs, and details you need to preserve verbatim.
105
-
106
- Rules:
107
- - Record what you did and observed, don't judge or re-evaluate it
108
- - For each tool call: the tool name, key inputs, and the outcome
109
- - Preserve exact identifiers, names, errors, and references verbatim
110
- - Short declarative sentences
111
- - Skip empty sections`;
112
-
113
- /** Prompt for re-compaction when a prior summary exists. */
114
- 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.
115
-
116
- 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.
117
-
118
- 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.
119
-
120
- Rules:
121
- - Merge new progress into existing sections, don't duplicate headers
122
- - Compress older completed items into one-line entries
123
- - Move items from "In Progress" to "Done" when you completed them
124
- - Update "Next Steps" to reflect current task priorities.
125
- - For each new tool call: the tool name, key inputs, and the outcome
126
- - Preserve exact identifiers, names, errors, and references verbatim
127
- - Skip empty sections`;
128
-
129
- function separateParameters(parameters: Record<string, unknown>): {
130
- llmParams: Record<string, unknown>;
131
- maxSummaryTokens?: number;
132
- } {
133
- const llmParams: Record<string, unknown> = {};
134
- let maxSummaryTokens: number | undefined;
135
-
136
- for (const [key, value] of Object.entries(parameters)) {
137
- if (SUMMARIZATION_PARAM_KEYS.has(key)) {
138
- if (
139
- key === 'maxSummaryTokens' &&
140
- typeof value === 'number' &&
141
- value > 0
142
- ) {
143
- maxSummaryTokens = value;
144
- }
145
- } else {
146
- llmParams[key] = value;
147
- }
148
- }
149
-
150
- return { llmParams, maxSummaryTokens };
151
- }
152
79
 
153
80
  /**
154
81
  * Generates a structural metadata summary without making an LLM call.
@@ -340,7 +267,7 @@ function buildSummarizationClientConfig(
340
267
  summarizationConfig?.updatePrompt ?? DEFAULT_UPDATE_SUMMARIZATION_PROMPT;
341
268
 
342
269
  const { llmParams, maxSummaryTokens: paramMaxSummaryTokens } =
343
- separateParameters(parameters);
270
+ separateSummarizationParameters(parameters);
344
271
 
345
272
  const isSelfSummarize = provider === (agentContext.provider as string);
346
273
  const baseOptions =
@@ -375,23 +302,104 @@ function buildSummarizationClientConfig(
375
302
  };
376
303
  }
377
304
 
378
- /** Computes the token count for a summary, preferring provider output tokens when available. */
379
- 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(
380
337
  summaryText: string,
381
- summaryUsage: Partial<UsageMetadata> | undefined,
382
- tokenCounter?: (message: BaseMessage) => number
383
- ): number {
384
- const providerOutputTokens = Number(summaryUsage?.output_tokens) || 0;
385
- if (providerOutputTokens > 0) {
386
- 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);
387
350
  }
388
- if (tokenCounter) {
389
- return (
390
- tokenCounter(new SystemMessage(summaryText)) +
391
- SUMMARY_WRAPPER_OVERHEAD_TOKENS
392
- );
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';
393
398
  }
394
- return 0;
399
+ return agentContext.provider === Providers.ANTHROPIC ||
400
+ getProviderFamily(agentContext.provider) === 'anthropic'
401
+ ? 'claude'
402
+ : 'o200k_base';
395
403
  }
396
404
 
397
405
  /**
@@ -1470,10 +1478,9 @@ export function createSummarizeNode({
1470
1478
 
1471
1479
  const summaryText = enrichSummary(rawText, messagesToRefine);
1472
1480
 
1473
- const tokenCount = computeSummaryTokenCount(
1481
+ const tokenCount = await computeSummaryTokenCount(
1474
1482
  summaryText,
1475
- summaryUsage,
1476
- agentContext.tokenCounter
1483
+ agentContext
1477
1484
  );
1478
1485
 
1479
1486
  if (usedIntraTurnFallback) {
@@ -1600,26 +1607,6 @@ function extractResponseText(response: { content: string | object }): string {
1600
1607
  return parts.join('').trim();
1601
1608
  }
1602
1609
 
1603
- function buildSummarizationInstruction(
1604
- promptText: string,
1605
- updatePromptText: string | undefined,
1606
- priorSummaryText: string,
1607
- semanticIndexAppendix = ''
1608
- ): string {
1609
- const effectivePrompt = priorSummaryText
1610
- ? (updatePromptText ?? promptText)
1611
- : promptText;
1612
- const parts = semanticIndexAppendix
1613
- ? [semanticIndexAppendix, '\n\n', effectivePrompt]
1614
- : [effectivePrompt];
1615
- if (priorSummaryText) {
1616
- parts.push(
1617
- `\n\n<previous-summary>\n${priorSummaryText}\n</previous-summary>`
1618
- );
1619
- }
1620
- return parts.join('');
1621
- }
1622
-
1623
1610
  /** Creates an `onChunk` callback that dispatches `ON_SUMMARIZE_DELTA` events for streaming. */
1624
1611
  export function createSummarizationChunkHandler({
1625
1612
  stepId,
@@ -50,6 +50,13 @@ export function setCompactionSemanticIndexProvidedEntryCount(
50
50
  snapshotProvidedEntryCounts.set(index, providedEntryCount);
51
51
  }
52
52
 
53
+ /** Reads producer-side cardinality without exposing snapshot bookkeeping. */
54
+ export function getCompactionSemanticIndexProvidedEntryCount(
55
+ index: CompactionSemanticIndex
56
+ ): number {
57
+ return snapshotProvidedEntryCounts.get(index) ?? index.length;
58
+ }
59
+
53
60
  type SourceReference = {
54
61
  contentOrders: Map<number, number>;
55
62
  };
@@ -102,10 +109,13 @@ function snapshotEntry(
102
109
  } = entry;
103
110
  if (
104
111
  typeof sourceMessageId !== 'string' ||
105
- sourceMessageId.length >
106
- COMPACTION_SEMANTIC_INDEX_LIMITS.maxIdentityChars ||
107
- typeof sourceContentIndex !== 'number' ||
108
- typeof revision !== 'number' ||
112
+ normalizeIdentity(sourceMessageId) == null ||
113
+ !Number.isSafeInteger(sourceContentIndex) ||
114
+ sourceContentIndex < 0 ||
115
+ sourceContentIndex >
116
+ COMPACTION_SEMANTIC_INDEX_LIMITS.maxSourceContentIndex ||
117
+ !Number.isSafeInteger(revision) ||
118
+ revision < 0 ||
109
119
  !VALID_ENTRY_TYPES.has(type)
110
120
  ) {
111
121
  return undefined;
@@ -143,14 +153,13 @@ function snapshotEntry(
143
153
  if (type === 'reasoning_label') {
144
154
  const reasoningStepId = entry.reasoningStepId;
145
155
  return typeof reasoningStepId === 'string' &&
146
- reasoningStepId.length <=
147
- COMPACTION_SEMANTIC_INDEX_LIMITS.maxIdentityChars
156
+ normalizeIdentity(reasoningStepId) != null
148
157
  ? Object.freeze({ type, reasoningStepId, ...common })
149
158
  : undefined;
150
159
  }
151
160
  const toolCallId = entry.toolCallId;
152
161
  return typeof toolCallId === 'string' &&
153
- toolCallId.length <= COMPACTION_SEMANTIC_INDEX_LIMITS.maxIdentityChars
162
+ normalizeIdentity(toolCallId) != null
154
163
  ? Object.freeze({ type, toolCallId, ...common })
155
164
  : undefined;
156
165
  }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Summarization primitives shared by the in-run summarize node and by callers
3
+ * that compact a conversation outside a run. Kept apart from `node.ts` so the
4
+ * package can export them without exporting the graph node itself.
5
+ */
6
+
7
+ /**
8
+ * Instruction that follows the summary body inside the carrier. Private on
9
+ * purpose: it is only ever correct alongside `buildSummaryCarrierText`, and a
10
+ * caller that reaches for the text separately is a caller that can drift from
11
+ * the accounting.
12
+ */
13
+ const SUMMARY_CARRIER_INSTRUCTION =
14
+ 'This is your own checkpoint: you wrote it to preserve context after compaction. Pick up where you left off based on the summary above. Do not repeat prior tasks, information or acknowledge this checkpoint message directly.';
15
+
16
+ /**
17
+ * Wraps a persisted summary in the carrier it is re-injected as, ahead of the
18
+ * messages that survived compaction.
19
+ *
20
+ * A stored summary costs what this returns, not what its body costs, so a
21
+ * caller budgeting for one measures this rather than adding a remembered
22
+ * constant to the bare text. The wrapper alone is ~48 tokens on `o200k_base`
23
+ * and more on Anthropic: too much to leave out of a context calculation, and
24
+ * too easy to get wrong from memory once the instruction is edited.
25
+ */
26
+ export function buildSummaryCarrierText(summaryText: string): string {
27
+ return (
28
+ '<summary>\n' +
29
+ summaryText +
30
+ '\n</summary>\n\n' +
31
+ SUMMARY_CARRIER_INSTRUCTION
32
+ );
33
+ }
34
+
35
+ /** Structured checkpoint prompt for fresh summarization (no prior summary). */
36
+ 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.
37
+
38
+ 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.
39
+
40
+ ## Checkpoint
41
+
42
+ ## Goal
43
+ What I asked you to do and any sub-goals you identified.
44
+
45
+ ## Constraints & Preferences
46
+ Any rules, preferences, or configuration I established.
47
+
48
+ ## Progress
49
+ ### Done
50
+ - What you completed and the outcomes
51
+
52
+ ### In Progress
53
+ - What you're currently working on
54
+
55
+ ## Key Decisions
56
+ Decisions you made and why.
57
+
58
+ ## Next Steps
59
+ Concrete task actions remaining, in priority order.
60
+
61
+ ## Critical Context
62
+ Exact identifiers, names, error messages, URLs, and details you need to preserve verbatim.
63
+
64
+ Rules:
65
+ - Record what you did and observed, don't judge or re-evaluate it
66
+ - For each tool call: the tool name, key inputs, and the outcome
67
+ - Preserve exact identifiers, names, errors, and references verbatim
68
+ - Short declarative sentences
69
+ - Skip empty sections`;
70
+
71
+ /** Prompt for re-compaction when a prior summary exists. */
72
+ 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.
73
+
74
+ 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.
75
+
76
+ 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.
77
+
78
+ Rules:
79
+ - Merge new progress into existing sections, don't duplicate headers
80
+ - Compress older completed items into one-line entries
81
+ - Move items from "In Progress" to "Done" when you completed them
82
+ - Update "Next Steps" to reflect current task priorities.
83
+ - For each new tool call: the tool name, key inputs, and the outcome
84
+ - Preserve exact identifiers, names, errors, and references verbatim
85
+ - Skip empty sections`;
86
+
87
+ const SUMMARIZATION_PARAM_KEYS = new Set(['maxSummaryTokens']);
88
+
89
+ export function separateSummarizationParameters(
90
+ parameters: Record<string, unknown>
91
+ ): {
92
+ llmParams: Record<string, unknown>;
93
+ maxSummaryTokens?: number;
94
+ } {
95
+ const llmParams: Record<string, unknown> = {};
96
+ let maxSummaryTokens: number | undefined;
97
+
98
+ for (const [key, value] of Object.entries(parameters)) {
99
+ if (SUMMARIZATION_PARAM_KEYS.has(key)) {
100
+ if (
101
+ key === 'maxSummaryTokens' &&
102
+ typeof value === 'number' &&
103
+ value > 0
104
+ ) {
105
+ maxSummaryTokens = value;
106
+ }
107
+ } else {
108
+ llmParams[key] = value;
109
+ }
110
+ }
111
+
112
+ return { llmParams, maxSummaryTokens };
113
+ }
114
+
115
+ export function buildSummarizationInstruction(
116
+ promptText: string,
117
+ updatePromptText: string | undefined,
118
+ priorSummaryText?: string,
119
+ semanticIndexAppendix = ''
120
+ ): string {
121
+ const prior = priorSummaryText?.trim() ?? '';
122
+ const effectivePrompt = prior ? (updatePromptText ?? promptText) : promptText;
123
+ const parts = semanticIndexAppendix
124
+ ? [semanticIndexAppendix, '\n\n', effectivePrompt]
125
+ : [effectivePrompt];
126
+ if (prior) {
127
+ parts.push(`\n\n<previous-summary>\n${prior}\n</previous-summary>`);
128
+ }
129
+ return parts.join('');
130
+ }
@@ -23,6 +23,15 @@ export function isRunStepResumeState(
23
23
  (state.revision ?? -1) < 0 ||
24
24
  !Number.isSafeInteger(state.nextIndex) ||
25
25
  (state.nextIndex ?? -1) < 0 ||
26
+ (state.stopContinuationCount != null &&
27
+ (!Number.isSafeInteger(state.stopContinuationCount) ||
28
+ state.stopContinuationCount < 0)) ||
29
+ (state.stopContinuationExecutionId != null &&
30
+ (typeof state.stopContinuationExecutionId !== 'string' ||
31
+ state.stopContinuationExecutionId.length === 0)) ||
32
+ (state.streamSegment != null &&
33
+ (!Number.isSafeInteger(state.streamSegment) ||
34
+ state.streamSegment < 0)) ||
26
35
  !Array.isArray(state.toolCallSteps) ||
27
36
  !Array.isArray(state.steps)
28
37
  ) {