@librechat/agents 3.7.8 → 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 (132) 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/subagent/SubagentExecutor.cjs +8 -12
  44. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  45. package/dist/cjs/utils/tokens.cjs +8 -1
  46. package/dist/cjs/utils/tokens.cjs.map +1 -1
  47. package/dist/esm/agents/AgentContext.mjs +3 -2
  48. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  49. package/dist/esm/common/constants.mjs +2 -1
  50. package/dist/esm/common/constants.mjs.map +1 -1
  51. package/dist/esm/graphs/Graph.mjs +40 -6
  52. package/dist/esm/graphs/Graph.mjs.map +1 -1
  53. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  54. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  55. package/dist/esm/hooks/executeHooks.mjs +8 -1
  56. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  57. package/dist/esm/hooks/index.mjs +2 -1
  58. package/dist/esm/hooks/index.mjs.map +1 -1
  59. package/dist/esm/hooks/types.mjs +1 -0
  60. package/dist/esm/hooks/types.mjs.map +1 -1
  61. package/dist/esm/langfuse.mjs +43 -3
  62. package/dist/esm/langfuse.mjs.map +1 -1
  63. package/dist/esm/llm/invoke.mjs +2 -6
  64. package/dist/esm/llm/invoke.mjs.map +1 -1
  65. package/dist/esm/llm/request.mjs +6 -1
  66. package/dist/esm/llm/request.mjs.map +1 -1
  67. package/dist/esm/main.mjs +13 -12
  68. package/dist/esm/messages/format.mjs +96 -19
  69. package/dist/esm/messages/format.mjs.map +1 -1
  70. package/dist/esm/messages/index.mjs +1 -1
  71. package/dist/esm/run.mjs +201 -46
  72. package/dist/esm/run.mjs.map +1 -1
  73. package/dist/esm/session/AgentSession.mjs +1 -1
  74. package/dist/esm/session/JsonlSessionStore.mjs +1 -1
  75. package/dist/esm/summarization/index.mjs +1 -0
  76. package/dist/esm/summarization/index.mjs.map +1 -1
  77. package/dist/esm/summarization/node.mjs +21 -82
  78. package/dist/esm/summarization/node.mjs.map +1 -1
  79. package/dist/esm/summarization/semanticIndex.mjs +7 -4
  80. package/dist/esm/summarization/semanticIndex.mjs.map +1 -1
  81. package/dist/esm/summarization/shared.mjs +79 -0
  82. package/dist/esm/summarization/shared.mjs.map +1 -0
  83. package/dist/esm/tools/ToolNode.mjs +5 -5
  84. package/dist/esm/tools/local/CompileCheckTool.mjs +1 -1
  85. package/dist/esm/tools/local/LocalCodingTools.mjs +1 -1
  86. package/dist/esm/tools/local/LocalExecutionEngine.mjs +1 -1
  87. package/dist/esm/tools/runStepResume.mjs +1 -1
  88. package/dist/esm/tools/runStepResume.mjs.map +1 -1
  89. package/dist/esm/tools/subagent/SubagentExecutor.mjs +8 -12
  90. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  91. package/dist/esm/utils/tokens.mjs +8 -2
  92. package/dist/esm/utils/tokens.mjs.map +1 -1
  93. package/dist/types/common/constants.d.ts +7 -0
  94. package/dist/types/graphs/Graph.d.ts +9 -0
  95. package/dist/types/hooks/executeHooks.d.ts +5 -2
  96. package/dist/types/hooks/index.d.ts +7 -2
  97. package/dist/types/hooks/types.d.ts +45 -7
  98. package/dist/types/langfuse.d.ts +3 -1
  99. package/dist/types/llm/request.d.ts +10 -0
  100. package/dist/types/messages/format.d.ts +8 -2
  101. package/dist/types/run.d.ts +2 -1
  102. package/dist/types/summarization/index.d.ts +6 -0
  103. package/dist/types/summarization/node.d.ts +0 -4
  104. package/dist/types/summarization/semanticIndex.d.ts +2 -0
  105. package/dist/types/summarization/shared.d.ts +25 -0
  106. package/dist/types/types/run.d.ts +8 -0
  107. package/dist/types/types/stream.d.ts +6 -0
  108. package/dist/types/types/summarize.d.ts +6 -0
  109. package/dist/types/utils/tokens.d.ts +9 -0
  110. package/package.json +1 -1
  111. package/src/agents/AgentContext.ts +2 -5
  112. package/src/common/constants.ts +8 -0
  113. package/src/graphs/Graph.ts +45 -0
  114. package/src/hooks/HookRegistry.ts +3 -1
  115. package/src/hooks/executeHooks.ts +20 -2
  116. package/src/hooks/index.ts +12 -1
  117. package/src/hooks/types.ts +47 -4
  118. package/src/langfuse.ts +70 -0
  119. package/src/llm/invoke.ts +13 -29
  120. package/src/llm/request.ts +24 -0
  121. package/src/messages/format.ts +213 -27
  122. package/src/run.ts +366 -186
  123. package/src/summarization/index.ts +11 -0
  124. package/src/summarization/node.ts +117 -130
  125. package/src/summarization/semanticIndex.ts +16 -7
  126. package/src/summarization/shared.ts +130 -0
  127. package/src/tools/runStepResume.ts +9 -0
  128. package/src/tools/subagent/SubagentExecutor.ts +4 -27
  129. package/src/types/run.ts +8 -0
  130. package/src/types/stream.ts +6 -0
  131. package/src/types/summarize.ts +7 -0
  132. package/src/utils/tokens.ts +31 -8
@@ -6,10 +6,6 @@ import type { HookRegistry } from '@/hooks';
6
6
  import type { OnChunk } from '@/llm/invoke';
7
7
  import type * as t from '@/types';
8
8
  import { type PromptCacheTtl } from '@/messages/cache';
9
- /** Structured checkpoint prompt for fresh summarization (no prior summary). */
10
- export declare 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.\n\nDon'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.\n\n## Checkpoint\n\n## Goal\nWhat I asked you to do and any sub-goals you identified.\n\n## Constraints & Preferences\nAny rules, preferences, or configuration I established.\n\n## Progress\n### Done\n- What you completed and the outcomes\n\n### In Progress\n- What you're currently working on\n\n## Key Decisions\nDecisions you made and why.\n\n## Next Steps\nConcrete task actions remaining, in priority order.\n\n## Critical Context\nExact identifiers, names, error messages, URLs, and details you need to preserve verbatim.\n\nRules:\n- Record what you did and observed, don't judge or re-evaluate it\n- For each tool call: the tool name, key inputs, and the outcome\n- Preserve exact identifiers, names, errors, and references verbatim\n- Short declarative sentences\n- Skip empty sections";
11
- /** Prompt for re-compaction when a prior summary exists. */
12
- export declare 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.\n\nKeep 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.\n\nDon'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.\n\nRules:\n- Merge new progress into existing sections, don't duplicate headers\n- Compress older completed items into one-line entries\n- Move items from \"In Progress\" to \"Done\" when you completed them\n- Update \"Next Steps\" to reflect current task priorities.\n- For each new tool call: the tool name, key inputs, and the outcome\n- Preserve exact identifiers, names, errors, and references verbatim\n- Skip empty sections";
13
9
  interface CreateSummarizeNodeParams {
14
10
  agentContext: AgentContext;
15
11
  graph: {
@@ -3,6 +3,8 @@ import type { CompactionSemanticIndex } from '@/types';
3
3
  export { COMPACTION_SEMANTIC_INDEX_LIMITS } from '@/common';
4
4
  /** Records producer-side omissions without retaining the discarded entries. */
5
5
  export declare function setCompactionSemanticIndexProvidedEntryCount(index: CompactionSemanticIndex, providedEntryCount: number): void;
6
+ /** Reads producer-side cardinality without exposing snapshot bookkeeping. */
7
+ export declare function getCompactionSemanticIndexProvidedEntryCount(index: CompactionSemanticIndex): number;
6
8
  export type RenderedCompactionSemanticIndex = {
7
9
  appendix: string;
8
10
  providedEntryCount: number;
@@ -0,0 +1,25 @@
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
+ * Wraps a persisted summary in the carrier it is re-injected as, ahead of the
8
+ * messages that survived compaction.
9
+ *
10
+ * A stored summary costs what this returns, not what its body costs, so a
11
+ * caller budgeting for one measures this rather than adding a remembered
12
+ * constant to the bare text. The wrapper alone is ~48 tokens on `o200k_base`
13
+ * and more on Anthropic: too much to leave out of a context calculation, and
14
+ * too easy to get wrong from memory once the instruction is edited.
15
+ */
16
+ export declare function buildSummaryCarrierText(summaryText: string): string;
17
+ /** Structured checkpoint prompt for fresh summarization (no prior summary). */
18
+ export declare 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.\n\nDon'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.\n\n## Checkpoint\n\n## Goal\nWhat I asked you to do and any sub-goals you identified.\n\n## Constraints & Preferences\nAny rules, preferences, or configuration I established.\n\n## Progress\n### Done\n- What you completed and the outcomes\n\n### In Progress\n- What you're currently working on\n\n## Key Decisions\nDecisions you made and why.\n\n## Next Steps\nConcrete task actions remaining, in priority order.\n\n## Critical Context\nExact identifiers, names, error messages, URLs, and details you need to preserve verbatim.\n\nRules:\n- Record what you did and observed, don't judge or re-evaluate it\n- For each tool call: the tool name, key inputs, and the outcome\n- Preserve exact identifiers, names, errors, and references verbatim\n- Short declarative sentences\n- Skip empty sections";
19
+ /** Prompt for re-compaction when a prior summary exists. */
20
+ export declare 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.\n\nKeep 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.\n\nDon'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.\n\nRules:\n- Merge new progress into existing sections, don't duplicate headers\n- Compress older completed items into one-line entries\n- Move items from \"In Progress\" to \"Done\" when you completed them\n- Update \"Next Steps\" to reflect current task priorities.\n- For each new tool call: the tool name, key inputs, and the outcome\n- Preserve exact identifiers, names, errors, and references verbatim\n- Skip empty sections";
21
+ export declare function separateSummarizationParameters(parameters: Record<string, unknown>): {
22
+ llmParams: Record<string, unknown>;
23
+ maxSummaryTokens?: number;
24
+ };
25
+ export declare function buildSummarizationInstruction(promptText: string, updatePromptText: string | undefined, priorSummaryText?: string, semanticIndexAppendix?: string): string;
@@ -233,6 +233,14 @@ export type RunConfig = {
233
233
  * block to prevent leaks.
234
234
  */
235
235
  hooks?: HookRegistry;
236
+ /**
237
+ * Maximum number of times a `Stop` hook may keep this Run warm by returning
238
+ * `decision: 'block'` with messages to inject. Defaults to
239
+ * `DEFAULT_MAX_STOP_CONTINUATIONS`; non-finite values use the default and
240
+ * values at or below zero disable terminal continuation while preserving the
241
+ * final Stop notification.
242
+ */
243
+ maxStopContinuations?: number;
236
244
  /**
237
245
  * Opt-in cooperative preemption for this run. Requires a `hooks` registry
238
246
  * with a `PreemptBoundary` matcher — the seal only stops the stream, the
@@ -87,6 +87,12 @@ export interface RunStepResumeState {
87
87
  version: 1;
88
88
  revision: number;
89
89
  nextIndex: number;
90
+ /** Warm terminal continuations already admitted for this execution. */
91
+ stopContinuationCount?: number;
92
+ /** Identifies the fresh execution that owns this continuation lifecycle. */
93
+ stopContinuationExecutionId?: string;
94
+ /** Distinguishes LangGraph event keys when a warm continuation restarts steps. */
95
+ streamSegment?: number;
90
96
  toolCallSteps: Array<{
91
97
  toolCallId: string;
92
98
  stepId: string;
@@ -62,6 +62,12 @@ export type CompactionReasoningSemanticIndexEntry = CompactionSemanticIndexEntry
62
62
  */
63
63
  export type CompactionSemanticIndexEntry = CompactionToolSemanticIndexEntry | CompactionActivitySemanticIndexEntry | CompactionReasoningSemanticIndexEntry;
64
64
  export type CompactionSemanticIndex = readonly CompactionSemanticIndexEntry[];
65
+ /** Serializable continuation state for a bounded compaction semantic index. */
66
+ export type CompactionSemanticIndexSnapshot = {
67
+ entries: CompactionSemanticIndex;
68
+ /** Cumulative entries supplied before validation and bounded retention. */
69
+ providedEntryCount: number;
70
+ };
65
71
  export type SummarizationConfig = {
66
72
  provider?: ProviderName;
67
73
  model?: string;
@@ -69,6 +69,15 @@ export declare function getTokenCountForMessage(message: BaseMessage, getTokenCo
69
69
  * computed as a single rounded product of the summed raw counts.
70
70
  */
71
71
  export declare function apportionTokenCounts(rawCounts: Record<string, number>, multiplier: number, targetTotal: number): Record<string, number>;
72
+ /**
73
+ * Encoding a counter measures in, for counters built here.
74
+ *
75
+ * `undefined` for a counter the host supplied itself: unknown, not wrong. A
76
+ * caller that needs a count in a specific encoding can therefore tell "counts
77
+ * in the encoding I need" from "counts in a different one" without treating
78
+ * every foreign counter as suspect.
79
+ */
80
+ export declare function encodingOfTokenCounter(tokenCounter: (message: BaseMessage) => number): EncodingName | undefined;
72
81
  /**
73
82
  * Creates a token counter function using the specified encoding.
74
83
  * Lazily loads the encoding data on first use via dynamic import.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.7.8",
3
+ "version": "3.7.9",
4
4
  "reova": {
5
5
  "enabled": true,
6
6
  "endpoint": "https://telemetry.reo.dev/data"
@@ -52,6 +52,7 @@ import {
52
52
  import { isTokenCounterCacheCompatible } from '@/llm/tokenCounterCacheCompatibility';
53
53
  import { snapshotCompactionSemanticIndex } from '@/summarization/semanticIndex';
54
54
  import { createExactTokenCountCache } from '@/llm/contextPressureMeter';
55
+ import { buildSummaryCarrierText } from '@/summarization/shared';
55
56
  import { createSchemaOnlyTools } from '@/tools/schema';
56
57
  import { apportionTokenCounts } from '@/utils/tokens';
57
58
  import { isThinkingEnabled } from '@/llm/request';
@@ -967,11 +968,7 @@ export class AgentContext {
967
968
  private buildSummaryHumanMessage(
968
969
  promptCacheProvider: PromptCacheProvider | undefined
969
970
  ): HumanMessage {
970
- const wrappedSummary =
971
- '<summary>\n' +
972
- (this.summaryText as string) +
973
- '\n</summary>\n\n' +
974
- '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);
975
972
 
976
973
  if (promptCacheProvider !== Providers.ANTHROPIC) {
977
974
  return new HumanMessage(wrappedSummary);
@@ -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
@@ -771,6 +771,9 @@ export abstract class Graph<
771
771
  contentData: t.RunStep[] = [];
772
772
  protected nextContentIndex = 0;
773
773
  protected runStepStateRevision = 0;
774
+ protected stopContinuationCount = 0;
775
+ protected stopContinuationExecutionId = '';
776
+ protected streamSegment = 0;
774
777
  stepKeyIds: Map<string, string[]> = new Map<string, string[]>();
775
778
  contentIndexMap: Map<string, number> = new Map();
776
779
  toolCallStepIds: Map<string, string> = new Map();
@@ -900,6 +903,9 @@ export abstract class Graph<
900
903
  this.contentData = [];
901
904
  this.nextContentIndex = 0;
902
905
  this.runStepStateRevision = 0;
906
+ this.stopContinuationCount = 0;
907
+ this.stopContinuationExecutionId = '';
908
+ this.streamSegment = 0;
903
909
  this.contentIndexMap = new Map();
904
910
  this.stepKeyIds = new Map();
905
911
  this.toolCallStepIds.clear();
@@ -1537,6 +1543,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1537
1543
  this.cachedRunMessages = undefined;
1538
1544
  this.cachedDiscoveredTools = undefined;
1539
1545
  this.config = resetIfNotEmpty(this.config, undefined);
1546
+ this.stopContinuationCount = 0;
1547
+ this.stopContinuationExecutionId = '';
1548
+ this.streamSegment = 0;
1540
1549
  if (keepContent !== true) {
1541
1550
  this.contentData = resetIfNotEmpty(this.contentData, []);
1542
1551
  this.nextContentIndex = 0;
@@ -1835,6 +1844,13 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1835
1844
  version: 1,
1836
1845
  revision: this.runStepStateRevision,
1837
1846
  nextIndex: this.nextContentIndex,
1847
+ stopContinuationCount: this.stopContinuationCount,
1848
+ ...(this.stopContinuationExecutionId === ''
1849
+ ? {}
1850
+ : {
1851
+ stopContinuationExecutionId: this.stopContinuationExecutionId,
1852
+ }),
1853
+ streamSegment: this.streamSegment,
1838
1854
  toolCallSteps: [...this.toolCallStepIds].map(([toolCallId, stepId]) => ({
1839
1855
  toolCallId,
1840
1856
  stepId,
@@ -1854,6 +1870,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1854
1870
 
1855
1871
  this.nextContentIndex = state.nextIndex;
1856
1872
  this.runStepStateRevision = state.revision;
1873
+ this.stopContinuationCount = state.stopContinuationCount ?? 0;
1874
+ this.stopContinuationExecutionId =
1875
+ state.stopContinuationExecutionId ?? '';
1876
+ this.streamSegment = state.streamSegment ?? 0;
1857
1877
  for (const { toolCallId, stepId } of state.toolCallSteps) {
1858
1878
  this.toolCallStepIds.set(toolCallId, stepId);
1859
1879
  }
@@ -1888,6 +1908,30 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1888
1908
  return undefined;
1889
1909
  }
1890
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
+
1891
1935
  /**
1892
1936
  * Derives the same lane key `dispatchRunStep` stamps as `runStep.agentId`.
1893
1937
  * The multi-agent check gates the lookup because `getAgentContext` signals
@@ -2336,6 +2380,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2336
2380
  metadata.langgraph_node as string,
2337
2381
  metadata.langgraph_step as number,
2338
2382
  checkpointNs,
2383
+ this.streamSegment,
2339
2384
  ];
2340
2385
 
2341
2386
  return keyList;
@@ -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
  }
package/src/langfuse.ts CHANGED
@@ -79,6 +79,8 @@ type LangfuseHandlerParams = {
79
79
  * only adopted when stamped with the same run (see
80
80
  * `LangfuseRuntimeContext.runId`). */
81
81
  runId?: string;
82
+ /** Keep this root open while one processStream call executes graph segments. */
83
+ deferRootRunId?: string;
82
84
  /** The run's resolved tool-output policy — for multi-agent streams the
83
85
  * conservative aggregate across agents, which `this.langfuse` (the
84
86
  * primary agent's config) cannot reproduce. Applied when a foreign
@@ -293,9 +295,19 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
293
295
  private readonly agentId?: string;
294
296
  private readonly parentSpanContext?: SpanContext;
295
297
  private readonly runId?: string;
298
+ private readonly deferredRootRunId?: string;
296
299
  private readonly identity: HandlerIdentity;
297
300
  private readonly toolOutputTracing?: ResolvedLangfuseToolOutputTracingConfig;
298
301
  private readonly trackedRunIds = new Set<string>();
302
+ private deferredRootStarted = false;
303
+ private deferredRootOutcome:
304
+ | {
305
+ type: 'end';
306
+ output: Parameters<CallbackHandler['handleChainEnd']>[0];
307
+ parentRunId?: string;
308
+ }
309
+ | { type: 'error'; error: Error; parentRunId?: string }
310
+ | undefined;
299
311
 
300
312
  constructor(params?: AgentLangfuseHandlerParams) {
301
313
  const {
@@ -306,6 +318,7 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
306
318
  parentSpanContext,
307
319
  inheritTraceIdentity,
308
320
  runId,
321
+ deferRootRunId,
309
322
  toolOutputTracing,
310
323
  traceName,
311
324
  ...handlerParams
@@ -326,6 +339,10 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
326
339
  this.agentId = agentId;
327
340
  this.parentSpanContext = parentSpanContext;
328
341
  this.runId = runId;
342
+ this.deferredRootRunId = deferRootRunId;
343
+ if (deferRootRunId != null) {
344
+ this.awaitHandlers = true;
345
+ }
329
346
  this.toolOutputTracing = toolOutputTracing;
330
347
  this.identity = {
331
348
  userId: inheritTraceIdentity === true ? undefined : handlerParams.userId,
@@ -528,6 +545,17 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
528
545
  override handleChainStart(
529
546
  ...args: Parameters<CallbackHandler['handleChainStart']>
530
547
  ): ReturnType<CallbackHandler['handleChainStart']> {
548
+ const [, , runId, parentRunId] = args;
549
+ if (
550
+ runId === this.deferredRootRunId &&
551
+ parentRunId == null &&
552
+ this.deferredRootStarted
553
+ ) {
554
+ return Promise.resolve();
555
+ }
556
+ if (runId === this.deferredRootRunId && parentRunId == null) {
557
+ this.deferredRootStarted = true;
558
+ }
531
559
  return this.withRuntimeContext(
532
560
  () => super.handleChainStart(...args),
533
561
  this.startsDetachedRun(args[2], args[3]),
@@ -539,6 +567,13 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
539
567
  ...args: Parameters<CallbackHandler['handleChainError']>
540
568
  ): ReturnType<CallbackHandler['handleChainError']> {
541
569
  const [error, runId, parentRunId] = args;
570
+ if (runId === this.deferredRootRunId && parentRunId == null) {
571
+ this.deferredRootOutcome = {
572
+ type: 'error',
573
+ error: error instanceof Error ? error : new Error(String(error)),
574
+ };
575
+ return Promise.resolve();
576
+ }
542
577
  if (error != null && parentRunId != null && isGraphInterrupt(error)) {
543
578
  return super.handleChainEnd(
544
579
  GRAPH_INTERRUPT_CONTROL_FLOW,
@@ -556,6 +591,36 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
556
591
  return super.handleChainError(...args);
557
592
  }
558
593
 
594
+ override handleChainEnd(
595
+ ...args: Parameters<CallbackHandler['handleChainEnd']>
596
+ ): ReturnType<CallbackHandler['handleChainEnd']> {
597
+ const [output, runId, parentRunId] = args;
598
+ if (runId === this.deferredRootRunId && parentRunId == null) {
599
+ this.deferredRootOutcome = { type: 'end', output };
600
+ return Promise.resolve();
601
+ }
602
+ return super.handleChainEnd(...args);
603
+ }
604
+
605
+ async finishDeferredRoot(): Promise<void> {
606
+ const runId = this.deferredRootRunId;
607
+ const outcome = this.deferredRootOutcome;
608
+ if (!this.deferredRootStarted || runId == null || outcome == null) {
609
+ return;
610
+ }
611
+ this.deferredRootStarted = false;
612
+ this.deferredRootOutcome = undefined;
613
+ if (outcome.type === 'error') {
614
+ await super.handleChainError(
615
+ outcome.error,
616
+ runId,
617
+ outcome.parentRunId
618
+ );
619
+ return;
620
+ }
621
+ await super.handleChainEnd(outcome.output, runId, outcome.parentRunId);
622
+ }
623
+
559
624
  override handleAgentAction(
560
625
  ...args: Parameters<CallbackHandler['handleAgentAction']>
561
626
  ): ReturnType<CallbackHandler['handleAgentAction']> {
@@ -792,6 +857,7 @@ export function createLangfuseHandler({
792
857
  parentSpanContext,
793
858
  inheritTraceIdentity,
794
859
  runId,
860
+ deferRootRunId,
795
861
  toolOutputTracing,
796
862
  traceName,
797
863
  }: AgentLangfuseHandlerParams): CallbackHandler | undefined {
@@ -813,6 +879,7 @@ export function createLangfuseHandler({
813
879
  parentSpanContext,
814
880
  inheritTraceIdentity,
815
881
  runId,
882
+ deferRootRunId,
816
883
  toolOutputTracing,
817
884
  traceName,
818
885
  });
@@ -865,6 +932,9 @@ export function isLangfuseCallbackHandler(value: unknown): boolean {
865
932
  }
866
933
 
867
934
  export async function disposeLangfuseHandler(value: unknown): Promise<void> {
935
+ if (value instanceof ScopedLangfuseCallbackHandler) {
936
+ await value.finishDeferredRoot();
937
+ }
868
938
  if (
869
939
  value == null ||
870
940
  parseBooleanEnv(process.env[LANGFUSE_FORCE_FLUSH_ON_DISPOSE]) !== true