@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
@@ -50,7 +50,9 @@ import {
50
50
  Providers,
51
51
  } from '@/common';
52
52
  import { isTokenCounterCacheCompatible } from '@/llm/tokenCounterCacheCompatibility';
53
+ import { snapshotCompactionSemanticIndex } from '@/summarization/semanticIndex';
53
54
  import { createExactTokenCountCache } from '@/llm/contextPressureMeter';
55
+ import { buildSummaryCarrierText } from '@/summarization/shared';
54
56
  import { createSchemaOnlyTools } from '@/tools/schema';
55
57
  import { apportionTokenCounts } from '@/utils/tokens';
56
58
  import { isThinkingEnabled } from '@/llm/request';
@@ -74,6 +76,14 @@ type ProgrammaticToolInstructionTarget = {
74
76
  executesDirectly: boolean;
75
77
  };
76
78
 
79
+ /**
80
+ * Consecutive summarization attempts that may return no usable summary before
81
+ * the run stops asking. Every such attempt spends a full model call over the
82
+ * whole history and leaves the message set exactly as it was, so the cap
83
+ * bounds spend as much as it bounds the compaction loop.
84
+ */
85
+ const MAX_SUMMARIZATION_FAILURES = 3;
86
+
77
87
  /**
78
88
  * Encapsulates agent-specific state that can vary between agents in a multi-agent system
79
89
  */
@@ -108,6 +118,7 @@ export class AgentContext {
108
118
  discoveredTools,
109
119
  summarizationEnabled,
110
120
  summarizationConfig,
121
+ compactionSemanticIndex,
111
122
  initialSummary,
112
123
  contextPruningConfig,
113
124
  maxToolResultChars,
@@ -141,11 +152,18 @@ export class AgentContext {
141
152
  discoveredTools,
142
153
  summarizationEnabled,
143
154
  summarizationConfig,
155
+ compactionSemanticIndex,
144
156
  contextPruningConfig,
145
157
  maxToolResultChars,
146
158
  });
147
159
 
148
- agentContext._sourceInputs = agentConfig;
160
+ agentContext._sourceInputs =
161
+ compactionSemanticIndex == null
162
+ ? agentConfig
163
+ : {
164
+ ...agentConfig,
165
+ compactionSemanticIndex: agentContext.compactionSemanticIndex,
166
+ };
149
167
  agentContext.subagentConfigs = subagentConfigs;
150
168
  agentContext.maxSubagentDepth = maxSubagentDepth;
151
169
  /**
@@ -346,6 +364,8 @@ export class AgentContext {
346
364
  summarizationEnabled?: boolean;
347
365
  /** Summarization runtime settings used by graph pruning hooks */
348
366
  summarizationConfig?: t.SummarizationConfig;
367
+ /** Host-supplied advisory guidance consumed only when compaction runs. */
368
+ compactionSemanticIndex?: t.CompactionSemanticIndex;
349
369
  /** Current summary text produced by the summarize node, integrated into system message */
350
370
  private summaryText?: string;
351
371
  /** Token count of the current summary (tracked for token accounting) */
@@ -376,6 +396,13 @@ export class AgentContext {
376
396
  * Summarization is allowed to fire again only when new messages appear.
377
397
  */
378
398
  private _lastSummarizationMsgCount: number = 0;
399
+ /**
400
+ * Consecutive summarization attempts that produced no usable summary.
401
+ * An empty or failed summary leaves the message set exactly as it was, so
402
+ * the next prune cycle would ask again on identical state. Cleared by
403
+ * `setSummary` and by `reset()`.
404
+ */
405
+ private _summarizationFailures: number = 0;
379
406
  /**
380
407
  * Forced compactions performed after a provider rejected a prompt as too
381
408
  * large. Bounds the recovery loop so a model that keeps refusing cannot
@@ -430,6 +457,7 @@ export class AgentContext {
430
457
  discoveredTools,
431
458
  summarizationEnabled,
432
459
  summarizationConfig,
460
+ compactionSemanticIndex,
433
461
  contextPruningConfig,
434
462
  maxToolResultChars,
435
463
  }: {
@@ -456,6 +484,7 @@ export class AgentContext {
456
484
  discoveredTools?: string[];
457
485
  summarizationEnabled?: boolean;
458
486
  summarizationConfig?: t.SummarizationConfig;
487
+ compactionSemanticIndex?: t.CompactionSemanticIndex;
459
488
  contextPruningConfig?: t.ContextPruningConfig;
460
489
  maxToolResultChars?: number;
461
490
  }) {
@@ -495,6 +524,11 @@ export class AgentContext {
495
524
  this.useLegacyContent = useLegacyContent ?? false;
496
525
  this.summarizationEnabled = summarizationEnabled;
497
526
  this.summarizationConfig = summarizationConfig;
527
+ if (compactionSemanticIndex != null) {
528
+ this.compactionSemanticIndex = snapshotCompactionSemanticIndex(
529
+ compactionSemanticIndex
530
+ );
531
+ }
498
532
  this.contextPruningConfig = contextPruningConfig;
499
533
  this.maxToolResultChars = maxToolResultChars;
500
534
 
@@ -934,11 +968,7 @@ export class AgentContext {
934
968
  private buildSummaryHumanMessage(
935
969
  promptCacheProvider: PromptCacheProvider | undefined
936
970
  ): HumanMessage {
937
- const wrappedSummary =
938
- '<summary>\n' +
939
- (this.summaryText as string) +
940
- '\n</summary>\n\n' +
941
- '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.';
971
+ const wrappedSummary = buildSummaryCarrierText(this.summaryText as string);
942
972
 
943
973
  if (promptCacheProvider !== Providers.ANTHROPIC) {
944
974
  return new HumanMessage(wrappedSummary);
@@ -1209,6 +1239,7 @@ export class AgentContext {
1209
1239
  this.summaryTokenCount = this._durableSummaryTokenCount;
1210
1240
  this.summaryPrecedesMessages = this.durableSummaryPrecedesMessages;
1211
1241
  this._lastSummarizationMsgCount = 0;
1242
+ this._summarizationFailures = 0;
1212
1243
  this.lastCallUsage = undefined;
1213
1244
  this.totalTokensFresh = false;
1214
1245
  this.restoreContextBudgetAfterOverflow();
@@ -1478,6 +1509,7 @@ export class AgentContext {
1478
1509
  this._durableSummaryTokenCount = tokenCount;
1479
1510
  this.durableSummaryPrecedesMessages = this.summaryPrecedesMessages;
1480
1511
  this._summaryVersion += 1;
1512
+ this._summarizationFailures = 0;
1481
1513
  this.systemRunnableStale = true;
1482
1514
  this.pruneMessages = undefined;
1483
1515
  }
@@ -1547,6 +1579,31 @@ export class AgentContext {
1547
1579
  this._lastSummarizationMsgCount = msgCount;
1548
1580
  }
1549
1581
 
1582
+ /**
1583
+ * Records a summarization attempt that produced no usable summary — an
1584
+ * empty model response, or a provider failure the run declined to paper
1585
+ * over with a metadata stub. Cleared by the next successful summary.
1586
+ */
1587
+ recordSummarizationFailure(): void {
1588
+ this._summarizationFailures += 1;
1589
+ }
1590
+
1591
+ get summarizationFailures(): number {
1592
+ return this._summarizationFailures;
1593
+ }
1594
+
1595
+ /**
1596
+ * True once consecutive no-progress attempts reach {@link MAX_SUMMARIZATION_FAILURES}.
1597
+ * A summarizer that has returned nothing this many times in a row will keep
1598
+ * returning nothing: each empty result leaves the history unchanged, so the
1599
+ * next prune cycle re-triggers on the same state and the run burns its
1600
+ * recursion budget on empty summary steps. Summarization stays off for the
1601
+ * remainder of the run; `reset()` restores it for the next one.
1602
+ */
1603
+ get summarizationExhausted(): boolean {
1604
+ return this._summarizationFailures >= MAX_SUMMARIZATION_FAILURES;
1605
+ }
1606
+
1550
1607
  get overflowRecoveryAttempts(): number {
1551
1608
  return this._overflowRecoveryAttempts;
1552
1609
  }
@@ -20,6 +20,14 @@ export const DEFAULT_TOOL_TOKEN_MULTIPLIER = 1.4;
20
20
  */
21
21
  export const DEFAULT_MAX_SEALS = 8;
22
22
 
23
+ /**
24
+ * Default ceiling on Stop-hook continuations within one Run. A blocking Stop
25
+ * hook can keep a naturally terminal run warm by injecting another user turn;
26
+ * the ceiling prevents a faulty hook or continuously arriving input from
27
+ * keeping one processStream call alive forever.
28
+ */
29
+ export const DEFAULT_MAX_STOP_CONTINUATIONS = 8;
30
+
23
31
  /**
24
32
  * Per-hook timeout for `PreemptBoundary`, deliberately far above
25
33
  * `DEFAULT_HOOK_TIMEOUT_MS`. A host drain has already popped its queue and
@@ -42,3 +50,14 @@ export const ACTIVITY_LABEL_RUN_NAME = 'StepLabel';
42
50
  export const REASONING_LABEL_RUN_NAME = 'ReasoningLabel';
43
51
  export const ACTIVITY_PHASE_RUN_NAME = 'MultiStepLabel';
44
52
  export const ACTIVITY_PHASE_LABEL_RUN_NAME = 'MultiStepLabelGeneration';
53
+
54
+ /** Shared admission and rendering bounds for compaction navigation hints. */
55
+ export const COMPACTION_SEMANTIC_INDEX_LIMITS = Object.freeze({
56
+ maxInputEntries: 256,
57
+ maxEntries: 64,
58
+ maxEntryChars: 512,
59
+ maxTotalChars: 4_096,
60
+ maxInputTextChars: 4_096,
61
+ maxIdentityChars: 512,
62
+ maxSourceContentIndex: 4_095,
63
+ } as const);
@@ -153,10 +153,10 @@ import { createLocalCodingToolBundle } from '@/tools/local/LocalCodingTools';
153
153
  import { SUBAGENT_REPLAY_CONTROLLER } from '@/tools/subagent/SubagentReplay';
154
154
  import { applyGraphRuntimeConfig } from '@/graphs/applyGraphRuntimeConfig';
155
155
  import { createContextPressureMeter } from '@/llm/contextPressureMeter';
156
- import { prepareToolsForPromptCache } from '@/llm/promptCacheTools';
157
156
  import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
158
157
  import { prepareProviderRequest } from '@/llm/prepareProviderRequest';
159
158
  import { createCloudflareCodingToolBundle } from '@/tools/cloudflare';
159
+ import { prepareToolsForPromptCache } from '@/llm/promptCacheTools';
160
160
  import { providerRequiresStrictAlternation } from '@/llm/providers';
161
161
  import { buildSubagentToolParams } from '@/tools/SubagentTool';
162
162
  import { initializeLangfuseTracing } from '@/instrumentation';
@@ -164,6 +164,7 @@ import { shouldTriggerSummarization } from '@/summarization';
164
164
  import { isRunStepResumeState } from '@/tools/runStepResume';
165
165
  import { resolveLocalToolsForBinding } from '@/tools/local';
166
166
  import { createSummarizeNode } from '@/summarization/node';
167
+ import { getTruncationStopReason } from '@/llm/truncation';
167
168
  import { messagesStateReducer } from '@/messages/reducer';
168
169
  import { createSchemaOnlyTools } from '@/tools/schema';
169
170
  import { AgentContext } from '@/agents/AgentContext';
@@ -770,6 +771,9 @@ export abstract class Graph<
770
771
  contentData: t.RunStep[] = [];
771
772
  protected nextContentIndex = 0;
772
773
  protected runStepStateRevision = 0;
774
+ protected stopContinuationCount = 0;
775
+ protected stopContinuationExecutionId = '';
776
+ protected streamSegment = 0;
773
777
  stepKeyIds: Map<string, string[]> = new Map<string, string[]>();
774
778
  contentIndexMap: Map<string, number> = new Map();
775
779
  toolCallStepIds: Map<string, string> = new Map();
@@ -899,6 +903,9 @@ export abstract class Graph<
899
903
  this.contentData = [];
900
904
  this.nextContentIndex = 0;
901
905
  this.runStepStateRevision = 0;
906
+ this.stopContinuationCount = 0;
907
+ this.stopContinuationExecutionId = '';
908
+ this.streamSegment = 0;
902
909
  this.contentIndexMap = new Map();
903
910
  this.stepKeyIds = new Map();
904
911
  this.toolCallStepIds.clear();
@@ -1425,6 +1432,21 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1425
1432
  * hosts persist it as unfinished rather than complete.
1426
1433
  */
1427
1434
  preemptIncomplete = false;
1435
+ /**
1436
+ * True when `routeMessage` sent a turn to `END` because the last AI
1437
+ * message carries no tool call, AND the provider reports it stopped for
1438
+ * hitting the output token ceiling (`getTruncationStopReason`). Plain-text
1439
+ * and reasoning turns cut off this way carry no tool call for
1440
+ * `assertNotTruncatedToolCall` to catch, so `toolsCondition` reads them as
1441
+ * an ordinary finished turn otherwise — hosts read this flag to persist
1442
+ * the turn as unfinished instead of a silently truncated "complete" answer.
1443
+ *
1444
+ * Deliberately separate from `preemptIncomplete`/`preemptHaltReason`: this
1445
+ * has no interaction with the preempt/seal machinery (in particular the
1446
+ * `preemptHaltReason` check at each model node's entry), so setting it
1447
+ * cannot suppress an unrelated agent's turn in a multi-agent graph.
1448
+ */
1449
+ outputTruncatedIncomplete = false;
1428
1450
  /**
1429
1451
  * `stopReason` from a `PreemptBoundary` hook that halted the turn.
1430
1452
  *
@@ -1521,6 +1543,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1521
1543
  this.cachedRunMessages = undefined;
1522
1544
  this.cachedDiscoveredTools = undefined;
1523
1545
  this.config = resetIfNotEmpty(this.config, undefined);
1546
+ this.stopContinuationCount = 0;
1547
+ this.stopContinuationExecutionId = '';
1548
+ this.streamSegment = 0;
1524
1549
  if (keepContent !== true) {
1525
1550
  this.contentData = resetIfNotEmpty(this.contentData, []);
1526
1551
  this.nextContentIndex = 0;
@@ -1702,6 +1727,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1702
1727
  this.preemptEmptyBoundaries = 0;
1703
1728
  this.preemptIncomplete = false;
1704
1729
  this.preemptHaltReason = undefined;
1730
+ this.outputTruncatedIncomplete = false;
1705
1731
  }
1706
1732
 
1707
1733
  /**
@@ -1818,6 +1844,13 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1818
1844
  version: 1,
1819
1845
  revision: this.runStepStateRevision,
1820
1846
  nextIndex: this.nextContentIndex,
1847
+ stopContinuationCount: this.stopContinuationCount,
1848
+ ...(this.stopContinuationExecutionId === ''
1849
+ ? {}
1850
+ : {
1851
+ stopContinuationExecutionId: this.stopContinuationExecutionId,
1852
+ }),
1853
+ streamSegment: this.streamSegment,
1821
1854
  toolCallSteps: [...this.toolCallStepIds].map(([toolCallId, stepId]) => ({
1822
1855
  toolCallId,
1823
1856
  stepId,
@@ -1837,6 +1870,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1837
1870
 
1838
1871
  this.nextContentIndex = state.nextIndex;
1839
1872
  this.runStepStateRevision = state.revision;
1873
+ this.stopContinuationCount = state.stopContinuationCount ?? 0;
1874
+ this.stopContinuationExecutionId =
1875
+ state.stopContinuationExecutionId ?? '';
1876
+ this.streamSegment = state.streamSegment ?? 0;
1840
1877
  for (const { toolCallId, stepId } of state.toolCallSteps) {
1841
1878
  this.toolCallStepIds.set(toolCallId, stepId);
1842
1879
  }
@@ -1871,6 +1908,30 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1871
1908
  return undefined;
1872
1909
  }
1873
1910
 
1911
+ getStopContinuationCount(): number {
1912
+ return this.stopContinuationCount;
1913
+ }
1914
+
1915
+ setStopContinuationCount(count: number): void {
1916
+ this.stopContinuationCount = count;
1917
+ }
1918
+
1919
+ getStopContinuationExecutionId(): string {
1920
+ return this.stopContinuationExecutionId;
1921
+ }
1922
+
1923
+ startStopContinuationExecution(executionId: string): void {
1924
+ this.stopContinuationExecutionId = executionId;
1925
+ }
1926
+
1927
+ getStreamSegment(): number {
1928
+ return this.streamSegment;
1929
+ }
1930
+
1931
+ advanceStreamSegment(): void {
1932
+ this.streamSegment += 1;
1933
+ }
1934
+
1874
1935
  /**
1875
1936
  * Derives the same lane key `dispatchRunStep` stamps as `runStep.agentId`.
1876
1937
  * The multi-agent check gates the lookup because `getAgentContext` signals
@@ -2319,6 +2380,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2319
2380
  metadata.langgraph_node as string,
2320
2381
  metadata.langgraph_step as number,
2321
2382
  checkpointNs,
2383
+ this.streamSegment,
2322
2384
  ];
2323
2385
 
2324
2386
  return keyList;
@@ -2801,9 +2863,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2801
2863
  provider,
2802
2864
  clientOptions,
2803
2865
  tools,
2804
- isDeferred: makeIsDeferred(
2805
- agentContext.getEffectiveToolDefinitions()
2806
- ),
2866
+ isDeferred: makeIsDeferred(agentContext.getEffectiveToolDefinitions()),
2807
2867
  });
2808
2868
  }
2809
2869
 
@@ -3030,9 +3090,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3030
3090
  messagesToRefine.length > 0;
3031
3091
 
3032
3092
  if (hasPrunedMessages) {
3033
- const shouldSkip = agentContext.shouldSkipSummarization(
3034
- messages.length
3035
- );
3093
+ const shouldSkip =
3094
+ agentContext.summarizationExhausted ||
3095
+ agentContext.shouldSkipSummarization(messages.length);
3036
3096
  const triggerResult =
3037
3097
  !shouldSkip &&
3038
3098
  shouldTriggerSummarization({
@@ -3089,6 +3149,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3089
3149
  messageCount: messages.length,
3090
3150
  messagesToRefineCount: messagesToRefine.length,
3091
3151
  contextLength: context.length,
3152
+ summarizationFailures: agentContext.summarizationFailures,
3153
+ summarizationExhausted: agentContext.summarizationExhausted,
3092
3154
  },
3093
3155
  { runId: this.runId, agentId }
3094
3156
  );
@@ -4818,11 +4880,27 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4818
4880
  if (state.summarizationRequest != null) {
4819
4881
  return summarizeNode;
4820
4882
  }
4821
- return toolsCondition(
4883
+ const decision = toolsCondition(
4822
4884
  state as t.BaseGraphState,
4823
4885
  toolNode,
4824
4886
  this.invokedToolIds
4825
4887
  );
4888
+ /**
4889
+ * `toolsCondition` only looks at `tool_calls` — a plain-text/reasoning
4890
+ * turn cut off by the output token ceiling has none, so it reads as an
4891
+ * ordinary finished turn and routes here to END. Flag it so hosts can
4892
+ * tell a genuinely finished answer from one the model never got to
4893
+ * complete. See `outputTruncatedIncomplete` for why this stays clear
4894
+ * of the preempt/seal halt fields.
4895
+ */
4896
+ if (decision === END) {
4897
+ const { messages } = state as t.BaseGraphState;
4898
+ const lastMessage = messages[messages.length - 1];
4899
+ if (getTruncationStopReason(lastMessage) != null) {
4900
+ this.outputTruncatedIncomplete = true;
4901
+ }
4902
+ }
4903
+ return decision;
4826
4904
  };
4827
4905
 
4828
4906
  const StateAnnotation = Annotation.Root({
@@ -43,7 +43,9 @@ type MatcherBucket = Partial<Record<HookEvent, HookMatcher<HookEvent>[]>>;
43
43
  /**
44
44
  * Events whose hooks can change a tool call's input or output. Presence of
45
45
  * any of these disables eager tool execution and early completion emission;
46
- * observation-only events (`PostToolBatch`, `Stop`, telemetry hooks) do not.
46
+ * hooks that cannot rewrite a tool result (`PostToolBatch`, `Stop`, telemetry
47
+ * hooks) do not. A Stop hook may continue the Run, but only after every tool
48
+ * result in the terminal graph segment is already authoritative.
47
49
  */
48
50
  const RESULT_ALTERING_HOOK_EVENTS = [
49
51
  'PreToolUse',
@@ -313,6 +313,9 @@ function applyAggregatedResult(
313
313
  target.additionalContexts.push(...source.additionalContexts);
314
314
  target.injectedMessages.push(...source.injectedMessages);
315
315
  target.errors.push(...source.errors);
316
+ if (source.hasHookFailures === true) {
317
+ target.hasHookFailures = true;
318
+ }
316
319
  if (source.decision != null) {
317
320
  applyToolDecision(target, source.decision, source.reason);
318
321
  }
@@ -347,6 +350,19 @@ function combineAggregatedResults(
347
350
  return combined;
348
351
  }
349
352
 
353
+ /** Deterministically fold results from serialized hook phases. */
354
+ export function mergeAggregatedHookResults(
355
+ ...results: Array<AggregatedHookResult | undefined>
356
+ ): AggregatedHookResult | undefined {
357
+ let combined: AggregatedHookResult | undefined;
358
+ for (const result of results) {
359
+ if (result != null) {
360
+ combined = combineAggregatedResults(combined, result);
361
+ }
362
+ }
363
+ return combined;
364
+ }
365
+
350
366
  function createPendingApprovalReplay(
351
367
  approvalResult: AggregatedHookResult,
352
368
  pendingApproval: AggregatedHookResult | undefined,
@@ -377,6 +393,7 @@ function fold(outcomes: readonly HookOutcome[]): {
377
393
  for (const outcome of outcomes) {
378
394
  const isOnceMatcher = outcome.matcher.once === true;
379
395
  if (outcome.error !== null) {
396
+ aggregated.hasHookFailures = true;
380
397
  if (outcome.matcher.internal !== true) {
381
398
  aggregated.errors.push(outcome.error);
382
399
  }
@@ -455,8 +472,9 @@ function fold(outcomes: readonly HookOutcome[]): {
455
472
  * ## Internal matchers
456
473
  *
457
474
  * A matcher with `internal: true` is excluded from both the `errors` array
458
- * and the logger output. Use it for infrastructure hooks whose failures
459
- * should not pollute user-visible diagnostics.
475
+ * and the logger output. `hasHookFailures` still records that the call was
476
+ * not fully successful, so fail-closed infrastructure boundaries can reject
477
+ * without exposing the suppressed diagnostic.
460
478
  *
461
479
  * ## Once semantics — atomic at-most-once
462
480
  *
@@ -9,7 +9,11 @@
9
9
  // `StandardGraph.createCallModel` (PreemptBoundary).
10
10
  export { HookRegistry } from './HookRegistry';
11
11
  export type { HookHaltSignal } from './HookRegistry';
12
- export { executeHooks, DEFAULT_HOOK_TIMEOUT_MS } from './executeHooks';
12
+ export {
13
+ executeHooks,
14
+ mergeAggregatedHookResults,
15
+ DEFAULT_HOOK_TIMEOUT_MS,
16
+ } from './executeHooks';
13
17
  /**
14
18
  * Feature probe for hosts: hook outputs support `injectedMessages`
15
19
  * (per-message graph-state injection at the `PostToolBatch` boundary).
@@ -28,6 +32,11 @@ export const HOOK_INJECTED_MESSAGES_CAPABLE = true;
28
32
  * the user as a dead button rather than as an unsupported feature.
29
33
  */
30
34
  export const HOOK_PREEMPT_BOUNDARY_CAPABLE = true;
35
+ /**
36
+ * Feature probe for hosts: a blocking `Stop` hook can inject queued messages
37
+ * and continue within the same `Run.processStream` lifecycle.
38
+ */
39
+ export const HOOK_STOP_CONTINUATION_CAPABLE = true;
31
40
  export {
32
41
  matchesQuery,
33
42
  hasNestedQuantifier,
@@ -71,6 +80,7 @@ export type {
71
80
  SubagentStartHookInput,
72
81
  SubagentStopHookInput,
73
82
  StopHookInput,
83
+ StopFinalizeHookInput,
74
84
  StopFailureHookInput,
75
85
  PreCompactHookInput,
76
86
  PostCompactHookInput,
@@ -85,6 +95,7 @@ export type {
85
95
  SubagentStartHookOutput,
86
96
  SubagentStopHookOutput,
87
97
  StopHookOutput,
98
+ StopFinalizeHookOutput,
88
99
  StopFailureHookOutput,
89
100
  PreCompactHookOutput,
90
101
  PostCompactHookOutput,
@@ -22,6 +22,7 @@ export const HOOK_EVENTS = [
22
22
  'SubagentStart',
23
23
  'SubagentStop',
24
24
  'Stop',
25
+ 'StopFinalize',
25
26
  'StopFailure',
26
27
  'PreCompact',
27
28
  'PostCompact',
@@ -209,7 +210,34 @@ export interface StopHookInput extends BaseHookInput {
209
210
  hook_event_name: 'Stop';
210
211
  messages: BaseMessage[];
211
212
  stopReason?: string;
213
+ /** True after this hook has already kept the current Run warm once. */
212
214
  stopHookActive: boolean;
215
+ /** Number of terminal continuations already admitted in this Run. */
216
+ continuationCount: number;
217
+ /**
218
+ * Remaining continuations the SDK can honor. A host that durably claims
219
+ * queued work MUST seal admission instead when this reaches zero.
220
+ */
221
+ continuationBudgetRemaining: number;
222
+ }
223
+
224
+ /**
225
+ * Serialized terminal admission phase. Unlike ordinary `Stop` hooks (which
226
+ * execute in parallel), this fires after their outputs have been folded so a
227
+ * durable host can make one final claim-or-seal decision with full knowledge
228
+ * of whether another hook already kept the Run warm.
229
+ */
230
+ export interface StopFinalizeHookInput extends BaseHookInput {
231
+ hook_event_name: 'StopFinalize';
232
+ messages: BaseMessage[];
233
+ stopReason?: string;
234
+ stopHookActive: boolean;
235
+ continuationCount: number;
236
+ continuationBudgetRemaining: number;
237
+ /** A regular Stop hook already supplied a valid warm continuation. */
238
+ continuationPlanned: boolean;
239
+ /** A halt reason or Stop output forbids another graph segment. */
240
+ continuationPrevented: boolean;
213
241
  }
214
242
 
215
243
  export interface StopFailureHookInput extends BaseHookInput {
@@ -259,6 +287,7 @@ export type HookInput =
259
287
  | SubagentStartHookInput
260
288
  | SubagentStopHookInput
261
289
  | StopHookInput
290
+ | StopFinalizeHookInput
262
291
  | StopFailureHookInput
263
292
  | PreCompactHookInput
264
293
  | PostCompactHookInput;
@@ -276,6 +305,7 @@ export type HookInputByEvent = {
276
305
  SubagentStart: SubagentStartHookInput;
277
306
  SubagentStop: SubagentStopHookInput;
278
307
  Stop: StopHookInput;
308
+ StopFinalize: StopFinalizeHookInput;
279
309
  StopFailure: StopFailureHookInput;
280
310
  PreCompact: PreCompactHookInput;
281
311
  PostCompact: PostCompactHookInput;
@@ -298,10 +328,11 @@ export interface BaseHookOutput {
298
328
  * mid-run steering message). Accumulated across hooks in registration
299
329
  * order.
300
330
  *
301
- * Consumed at exactly two dispatch sites, both of which run the same
302
- * converter so the emitted shapes cannot drift: `PostToolBatch` (the tool
303
- * boundary) and `PreemptBoundary` (a cooperative mid-generation seal).
304
- * Every other event ignores the field.
331
+ * Consumed at three dispatch sites, all of which run the same converter so
332
+ * the emitted shapes cannot drift: `PostToolBatch` (the tool boundary),
333
+ * `PreemptBoundary` (a cooperative mid-generation seal), and a blocking
334
+ * `Stop` hook (terminal Run continuation). Every other event ignores the
335
+ * field.
305
336
  */
306
337
  injectedMessages?: InjectedMessage[];
307
338
  /** True to prevent the next model turn. Any hook can set this. */
@@ -435,10 +466,18 @@ export interface SubagentStartHookOutput extends BaseHookOutput {
435
466
  export type SubagentStopHookOutput = BaseHookOutput;
436
467
 
437
468
  export interface StopHookOutput extends BaseHookOutput {
469
+ /**
470
+ * `block` keeps a naturally terminal Run warm only when the output also
471
+ * supplies non-empty `injectedMessages` or `additionalContext`. The SDK
472
+ * starts another graph segment inside the same `processStream` call; no
473
+ * RunStart/UserPromptSubmit hooks or terminal cleanup occur between them.
474
+ */
438
475
  decision?: StopDecision;
439
476
  reason?: string;
440
477
  }
441
478
 
479
+ export type StopFinalizeHookOutput = StopHookOutput;
480
+
442
481
  export type StopFailureHookOutput = BaseHookOutput;
443
482
 
444
483
  export type PreCompactHookOutput = BaseHookOutput;
@@ -458,6 +497,7 @@ export type HookOutputByEvent = {
458
497
  SubagentStart: SubagentStartHookOutput;
459
498
  SubagentStop: SubagentStopHookOutput;
460
499
  Stop: StopHookOutput;
500
+ StopFinalize: StopFinalizeHookOutput;
461
501
  StopFailure: StopFailureHookOutput;
462
502
  PreCompact: PreCompactHookOutput;
463
503
  PostCompact: PostCompactHookOutput;
@@ -476,6 +516,7 @@ export type HookOutput =
476
516
  | SubagentStartHookOutput
477
517
  | SubagentStopHookOutput
478
518
  | StopHookOutput
519
+ | StopFinalizeHookOutput
479
520
  | StopFailureHookOutput
480
521
  | PreCompactHookOutput
481
522
  | PostCompactHookOutput;
@@ -593,6 +634,8 @@ export interface AggregatedHookResult {
593
634
  * `preventContinuation` do not overwrite the reason.
594
635
  */
595
636
  stopReason?: string;
637
+ /** True when any hook failed, including internal hooks with hidden diagnostics. */
638
+ hasHookFailures?: true;
596
639
  /** Error messages from hooks that threw; always present (possibly empty). */
597
640
  errors: string[];
598
641
  }