@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
package/src/run.ts CHANGED
@@ -3,27 +3,28 @@ import { nanoid } from 'nanoid';
3
3
  import { PromptTemplate } from '@langchain/core/prompts';
4
4
  import { RunnableLambda } from '@langchain/core/runnables';
5
5
  import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
6
- import {
7
- Command,
8
- INTERRUPT,
9
- MemorySaver,
10
- isInterrupted,
11
- } from '@langchain/langgraph';
12
6
  import {
13
7
  AIMessage,
14
8
  BaseMessage,
15
9
  HumanMessage,
16
10
  SystemMessage,
17
11
  } from '@langchain/core/messages';
12
+ import {
13
+ Command,
14
+ INTERRUPT,
15
+ MemorySaver,
16
+ Overwrite,
17
+ isInterrupted,
18
+ } from '@langchain/langgraph';
18
19
  import type {
19
20
  MessageContentComplex,
20
21
  UsageMetadata,
21
22
  } from '@langchain/core/messages';
22
23
  import type { StringPromptValue } from '@langchain/core/prompt_values';
23
24
  import type { RunnableConfig } from '@langchain/core/runnables';
25
+ import type { AggregatedHookResult, HookRegistry } from '@/hooks';
24
26
  import type { MultiAgentGraph } from '@/graphs/MultiAgentGraph';
25
27
  import type { StandardGraph } from '@/graphs/Graph';
26
- import type { HookRegistry } from '@/hooks';
27
28
  import type * as t from '@/types';
28
29
  import {
29
30
  Callback,
@@ -33,9 +34,9 @@ import {
33
34
  REASONING_LABEL_RUN_NAME,
34
35
  ACTIVITY_PHASE_RUN_NAME,
35
36
  ACTIVITY_PHASE_LABEL_RUN_NAME,
37
+ DEFAULT_MAX_STOP_CONTINUATIONS,
36
38
  DEFAULT_RECURSION_LIMIT,
37
39
  } from '@/common';
38
- import { isBuiltRuntime } from '@/lazyRequire';
39
40
  import {
40
41
  requireValidSubagentResumeManifest,
41
42
  stripSubagentResumeManifest,
@@ -94,15 +95,18 @@ import { applyGraphRuntimeConfig } from '@/graphs/applyGraphRuntimeConfig';
94
95
  import { LANGFUSE_OPERATION_METADATA_KEY } from '@/langfuseOperation';
95
96
  import { createTokenCounter, encodingForModel } from '@/utils/tokens';
96
97
  import { stampSyntheticProviderMessage } from '@/messages/provenance';
98
+ import { isOpenAILike, isLibreChatOpenAIModel } from '@/utils/llm';
99
+ import { executeHooks, mergeAggregatedHookResults } from '@/hooks';
100
+ import { convertInjectedMessages } from '@/messages/injected';
97
101
  import { initializeLangfuseTracing } from './instrumentation';
98
102
  import { seedRunInitialSessions } from '@/utils/toolSessions';
99
103
  import { getTraceIdSeed } from '@/langfuseRuntimeContext';
104
+ import { resolveClientOptionsModel } from '@/llm/request';
100
105
  import { createGraph } from '@/graphs/createGraph';
101
106
  import { resolveMaxSeals } from '@/llm/preempt';
107
+ import { isBuiltRuntime } from '@/lazyRequire';
102
108
  import { initializeModel } from '@/llm/init';
103
109
  import { HandlerRegistry } from '@/events';
104
- import { isOpenAILike, isLibreChatOpenAIModel } from '@/utils/llm';
105
- import { executeHooks } from '@/hooks';
106
110
 
107
111
  /** Source-mode runs have no dist siblings for the lazy-loading seam, so every
108
112
  * lazily loadable module is imported through the active loader before the
@@ -130,6 +134,61 @@ export const defaultOmitOptions = new Set([
130
134
  const ACTIVITY_LABEL_TRACE_NAME = 'LibreChat Activity Label';
131
135
  const ACTIVITY_PHASE_TRACE_NAME = 'LibreChat Activity Phase';
132
136
  const REASONING_LABEL_TRACE_NAME = 'LibreChat Reasoning Label';
137
+ const OUTPUT_TRUNCATED_HALT_REASON = 'output_truncated';
138
+
139
+ function resolveMaxStopContinuations(value: number | undefined): number {
140
+ if (value == null || !Number.isFinite(value)) {
141
+ return DEFAULT_MAX_STOP_CONTINUATIONS;
142
+ }
143
+ return Math.max(0, Math.floor(value));
144
+ }
145
+
146
+ function materializeStopContinuation(
147
+ result: AggregatedHookResult
148
+ ): BaseMessage[] {
149
+ const messages: BaseMessage[] = [];
150
+ const contexts = result.additionalContexts.filter(
151
+ (context) => context.trim() !== ''
152
+ );
153
+ if (contexts.length > 0) {
154
+ messages.push(
155
+ stampSyntheticProviderMessage(
156
+ new HumanMessage({
157
+ content: contexts.join('\n\n'),
158
+ additional_kwargs: {
159
+ role: 'system',
160
+ isMeta: true,
161
+ source: 'hook',
162
+ },
163
+ })
164
+ )
165
+ );
166
+ }
167
+ messages.push(...convertInjectedMessages(result.injectedMessages));
168
+ return messages;
169
+ }
170
+
171
+ function advanceCheckpointCursor(
172
+ config: t.RunStreamConfig
173
+ ): t.RunStreamConfig {
174
+ const configurable = { ...config.configurable };
175
+ delete configurable.checkpoint_id;
176
+ delete configurable.checkpoint_map;
177
+ return { ...config, configurable };
178
+ }
179
+
180
+ function assertFinalAdmissionSucceeded(result: AggregatedHookResult): void {
181
+ if (result.hasHookFailures !== true && result.errors.length === 0) {
182
+ return;
183
+ }
184
+ const detail =
185
+ result.errors.length > 0
186
+ ? result.errors.join('; ')
187
+ : 'one or more internal finalizers failed';
188
+ throw new Error(
189
+ `StopFinalize terminal admission failed: ${detail}`
190
+ );
191
+ }
133
192
 
134
193
  const CUSTOM_GRAPH_EVENTS = new Set<string>([
135
194
  GraphEvents.ON_AGENT_UPDATE,
@@ -227,7 +286,10 @@ function getInterruptHookSessionId(payload: unknown): string | undefined {
227
286
 
228
287
  type InterruptStateSnapshot = {
229
288
  config?: RunnableConfig;
230
- values?: { messages?: BaseMessage[] };
289
+ values?: {
290
+ messages?: BaseMessage[];
291
+ runStepState?: t.RunStepResumeState;
292
+ };
231
293
  tasks?: Array<{
232
294
  interrupts?: Array<{ id?: string; value?: unknown }>;
233
295
  state?: RunnableConfig | InterruptStateSnapshot;
@@ -240,10 +302,79 @@ type WorkflowWithStateHistory = {
240
302
  options?: { subgraphs?: boolean }
241
303
  ): Promise<InterruptStateSnapshot>;
242
304
  getStateHistory?(
243
- config: RunnableConfig
305
+ config: RunnableConfig,
306
+ options?: { filter?: Record<string, unknown>; limit?: number }
244
307
  ): AsyncIterableIterator<InterruptStateSnapshot>;
245
308
  };
246
309
 
310
+ async function resolveCompletedSegmentConfig(
311
+ workflow: t.CompiledStateWorkflow,
312
+ config: t.RunStreamConfig,
313
+ executionId: string,
314
+ streamSegment: number
315
+ ): Promise<t.RunStreamConfig> {
316
+ const stateWorkflow = workflow as t.CompiledStateWorkflow &
317
+ WorkflowWithStateHistory;
318
+ const stateHistory = stateWorkflow.getStateHistory;
319
+ if (typeof stateHistory !== 'function') {
320
+ throw new Error(
321
+ 'Cannot continue a checkpointed run without exact state history.'
322
+ );
323
+ }
324
+ const lookup = advanceCheckpointCursor(config);
325
+
326
+ const resolveSnapshot = (
327
+ snapshot: InterruptStateSnapshot | undefined
328
+ ): t.RunStreamConfig | undefined => {
329
+ if (snapshot == null) {
330
+ return undefined;
331
+ }
332
+ const runStepState =
333
+ snapshot.values?.runStepState ??
334
+ snapshot.tasks
335
+ ?.flatMap((task) => task.interrupts ?? [])
336
+ .map((pendingInterrupt) =>
337
+ getRunStepResumeState(pendingInterrupt.value)
338
+ )
339
+ .find((state): state is t.RunStepResumeState => state != null);
340
+ if (
341
+ runStepState?.stopContinuationExecutionId !== executionId ||
342
+ runStepState.streamSegment !== streamSegment
343
+ ) {
344
+ return undefined;
345
+ }
346
+ const checkpointId = snapshot.config?.configurable?.checkpoint_id;
347
+ if (typeof checkpointId !== 'string' || checkpointId.length === 0) {
348
+ return undefined;
349
+ }
350
+ return {
351
+ ...config,
352
+ configurable: {
353
+ ...config.configurable,
354
+ ...snapshot.config?.configurable,
355
+ },
356
+ };
357
+ };
358
+
359
+ if (typeof stateWorkflow.getState === 'function') {
360
+ const latest = await stateWorkflow.getState(lookup);
361
+ const latestConfig = resolveSnapshot(latest);
362
+ if (latestConfig != null) {
363
+ return latestConfig;
364
+ }
365
+ }
366
+
367
+ for await (const snapshot of stateHistory.call(workflow, lookup)) {
368
+ const snapshotConfig = resolveSnapshot(snapshot);
369
+ if (snapshotConfig != null) {
370
+ return snapshotConfig;
371
+ }
372
+ }
373
+ throw new Error(
374
+ 'Cannot identify the checkpoint committed by the completed graph segment.'
375
+ );
376
+ }
377
+
247
378
  function getFirstPersistedInterrupt(
248
379
  snapshot: InterruptStateSnapshot
249
380
  ): { id: string; value: unknown } | undefined {
@@ -283,6 +414,27 @@ function getPersistedMessages(
283
414
 
284
415
  type ResumeCommandUpdate = ConstructorParameters<typeof Command>[0]['update'];
285
416
 
417
+ function overwriteResumeRunStepState(
418
+ command: Command,
419
+ state: t.RunStepResumeState
420
+ ): Command {
421
+ const overwrite = new Overwrite(state);
422
+ const update = Array.isArray(command.update)
423
+ ? [
424
+ ...command.update.filter(([key]) => key !== 'runStepState'),
425
+ ['runStepState', overwrite] as [string, unknown],
426
+ ]
427
+ : { ...(command.update ?? {}), runStepState: overwrite };
428
+ const resumed = new Command({
429
+ ...(command.graph == null ? {} : { graph: command.graph }),
430
+ ...(command.resume === undefined ? {} : { resume: command.resume }),
431
+ update,
432
+ ...(command.goto === undefined ? {} : { goto: command.goto }),
433
+ });
434
+ resumed.lc_direct_tool_output = command.lc_direct_tool_output;
435
+ return resumed;
436
+ }
437
+
286
438
  function getResumeUpdateMessages(
287
439
  update: ResumeCommandUpdate
288
440
  ): BaseMessage[] | undefined {
@@ -315,6 +467,7 @@ export class Run<_T extends t.BaseGraphState> {
315
467
  private toolExecution?: t.ToolExecutionConfig;
316
468
  private subagentUsageSink?: t.SubagentUsageSink;
317
469
  private preemption?: t.StreamPreemption;
470
+ private maxStopContinuations: number;
318
471
  private streamLimits?: t.StreamLimits;
319
472
  private subagentTasks?: t.SubagentTaskConfig;
320
473
  private indexTokenCountMap?: Record<string, number>;
@@ -386,6 +539,9 @@ export class Run<_T extends t.BaseGraphState> {
386
539
  this.subagentUsageSink = config.subagentUsageSink;
387
540
  this.subagentTasks = config.subagentTasks;
388
541
  this.preemption = config.preemption;
542
+ this.maxStopContinuations = resolveMaxStopContinuations(
543
+ config.maxStopContinuations
544
+ );
389
545
  this.streamLimits = config.streamLimits;
390
546
 
391
547
  if (!config.graphConfig) {
@@ -728,12 +884,16 @@ export class Run<_T extends t.BaseGraphState> {
728
884
  config: t.RunConfig
729
885
  ): Promise<Run<T>> {
730
886
  await ensureSourceModeProviders();
731
- /** Create tokenCounter if indexTokenCountMap is provided but tokenCounter is not */
887
+ /** Create tokenCounter if indexTokenCountMap is provided but tokenCounter is
888
+ * not. The model is read through both option keys: `modelName` is
889
+ * LangChain's alias for `model`, so consulting one alone would give a
890
+ * Claude-backed agent an `o200k_base` counter and undercount every message
891
+ * it measures. */
732
892
  if (config.indexTokenCountMap && !config.tokenCounter) {
733
893
  const gc = config.graphConfig;
734
894
  const clientOpts =
735
895
  'agents' in gc ? gc.agents[0]?.clientOptions : gc.clientOptions;
736
- const model = (clientOpts as { model?: string } | undefined)?.model ?? '';
896
+ const model = resolveClientOptionsModel(clientOpts) ?? '';
737
897
  config.tokenCounter = await createTokenCounter(encodingForModel(model));
738
898
  }
739
899
  return new Run<T>(config);
@@ -794,6 +954,18 @@ export class Run<_T extends t.BaseGraphState> {
794
954
  return this.Graph?.getToolCount() ?? 0;
795
955
  }
796
956
 
957
+ /**
958
+ * True when the run's last turn ended at `END` because the provider hit
959
+ * its output token ceiling while producing plain text/reasoning — no tool
960
+ * call, so `assertNotTruncatedToolCall` never sees it and the graph reads
961
+ * the turn as an ordinary completion. Hosts check this alongside
962
+ * `getPreemptStats()` / `getHaltReason()` to decide whether to persist the
963
+ * response as unfinished instead of a silently truncated "complete" one.
964
+ */
965
+ getOutputTruncated(): boolean {
966
+ return this.Graph?.outputTruncatedIncomplete ?? false;
967
+ }
968
+
797
969
  /**
798
970
  * Creates a custom event callback handler that intercepts custom events
799
971
  * and processes them through our handler registry instead of EventStreamCallbackHandler
@@ -1042,11 +1214,16 @@ export class Run<_T extends t.BaseGraphState> {
1042
1214
  delete config.configurable?.[SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY];
1043
1215
  delete config.configurable?.[SUBAGENT_RESUME_MANIFEST_CONFIG_KEY];
1044
1216
  }
1217
+ let overwriteLegacyResumeState = false;
1045
1218
  if (isResume) {
1046
1219
  await this.restoreInterruptFromCheckpoint(
1047
1220
  config,
1048
1221
  (inputs as Command).update
1049
1222
  );
1223
+ if (graph.getStopContinuationExecutionId() === '') {
1224
+ graph.startStopContinuationExecution(nanoid());
1225
+ overwriteLegacyResumeState = this.hasCheckpointer;
1226
+ }
1050
1227
  }
1051
1228
 
1052
1229
  /**
@@ -1097,6 +1274,7 @@ export class Run<_T extends t.BaseGraphState> {
1097
1274
  checkpointId === '' ? 0 : ++this.checkpointForkSeq,
1098
1275
  ]);
1099
1276
  graph.resetValues(streamOptions?.keepContent, checkpointScope);
1277
+ graph.startStopContinuationExecution(nanoid());
1100
1278
  }
1101
1279
  this._interrupt = undefined;
1102
1280
  this._haltedReason = undefined;
@@ -1162,6 +1340,7 @@ export class Run<_T extends t.BaseGraphState> {
1162
1340
  : undefined,
1163
1341
  traceAnchor: graph.langfuseTraceAnchor,
1164
1342
  runId: graph.langfuseScopeRunId,
1343
+ deferRootRunId: this.id,
1165
1344
  // The aggregate multi-agent policy from the runtime scope — the
1166
1345
  // handler must restore THIS (not the primary agent's config-derived
1167
1346
  // policy) when rejecting a foreign scope.
@@ -1176,6 +1355,7 @@ export class Run<_T extends t.BaseGraphState> {
1176
1355
  throw new Error('Run ID not provided');
1177
1356
  }
1178
1357
 
1358
+ config.runId = this.id;
1179
1359
  config.run_id = this.id;
1180
1360
  config.configurable = Object.assign(config.configurable ?? {}, {
1181
1361
  run_id: this.id,
@@ -1224,191 +1404,209 @@ export class Run<_T extends t.BaseGraphState> {
1224
1404
  let terminalAt: number | undefined;
1225
1405
 
1226
1406
  const consumeStream = async (): Promise<void> => {
1227
- /**
1228
- * `streamEvents` accepts both state inputs and `Command` (resume) at
1229
- * runtime, but our `CompiledStateWorkflow` type narrows the first
1230
- * arg to `BaseGraphState`. Cast on the call so the resume path
1231
- * type-checks without widening the wrapper for every caller.
1232
- */
1233
- const stream = graphRunnable.streamEvents(
1234
- inputs as t.IState,
1235
- { ...config, runName: graph.runName },
1236
- {
1237
- raiseError: true,
1238
- /**
1239
- * Prevent EventStreamCallbackHandler from processing custom events.
1240
- * Custom events are already handled via our createCustomEventCallback()
1241
- * which routes them through the handlerRegistry.
1242
- * Without this flag, EventStreamCallbackHandler throws errors when
1243
- * custom events are dispatched for run IDs not in its internal map
1244
- * (due to timing issues in parallel execution or after run cleanup).
1245
- */
1246
- ignoreCustomEvent: true,
1247
- }
1248
- );
1407
+ let streamInputs: t.IState | Command = inputs;
1408
+ if (!isResume && this.hasCheckpointer) {
1409
+ streamInputs = {
1410
+ ...(inputs as t.IState),
1411
+ runStepState: new Overwrite(graph.createRunStepResumeState()),
1412
+ } as unknown as t.IState;
1413
+ } else if (overwriteLegacyResumeState) {
1414
+ streamInputs = overwriteResumeRunStepState(
1415
+ inputs as Command,
1416
+ graph.createRunStepResumeState()
1417
+ );
1418
+ }
1419
+ let streamConfig = config;
1249
1420
 
1250
- for await (const event of stream) {
1251
- const { data, metadata, ...info } = event;
1421
+ for (;;) {
1422
+ /**
1423
+ * `streamEvents` accepts both state inputs and `Command` (resume) at
1424
+ * runtime, but our `CompiledStateWorkflow` type narrows the first
1425
+ * arg to `BaseGraphState`. Cast on the call so the resume path
1426
+ * type-checks without widening the wrapper for every caller.
1427
+ */
1428
+ const stream = graphRunnable.streamEvents(
1429
+ streamInputs as t.IState,
1430
+ { ...streamConfig, runName: graph.runName },
1431
+ {
1432
+ raiseError: true,
1433
+ /** Custom events are handled by createCustomEventCallback(). */
1434
+ ignoreCustomEvent: true,
1435
+ }
1436
+ );
1252
1437
 
1253
- const eventName: t.EventName = info.event;
1438
+ for await (const event of stream) {
1439
+ const { data, metadata, ...info } = event;
1440
+ const eventName: t.EventName = info.event;
1254
1441
 
1255
- /** Skip custom events as they're handled by our callback */
1256
- if (CUSTOM_GRAPH_EVENTS.has(eventName)) {
1257
- continue;
1258
- }
1442
+ if (CUSTOM_GRAPH_EVENTS.has(eventName)) {
1443
+ continue;
1444
+ }
1259
1445
 
1260
- /**
1261
- * Detect interrupts surfaced by LangGraph as a synthetic
1262
- * `__interrupt__` field on the streamed chunk and stash the
1263
- * first one for the host to read via `run.getInterrupt()`
1264
- * once the stream drains. Captured as `unknown` because the
1265
- * SDK does not validate the runtime payload shape — the
1266
- * built-in ToolNode raises a `HumanInterruptPayload`
1267
- * (`tool_approval` / `ask_user_question`), but custom nodes
1268
- * can pass any payload to `interrupt()`. Callers narrow with
1269
- * the `isToolApprovalInterrupt` / `isAskUserQuestionInterrupt`
1270
- * guards or assert via `getInterrupt<T>()`.
1271
- */
1272
- if (
1273
- this._interrupt == null &&
1274
- data.chunk != null &&
1275
- isInterrupted<unknown>(data.chunk)
1276
- ) {
1277
- const interrupts = data.chunk[INTERRUPT];
1278
- if (interrupts.length > 0) {
1279
- const first = interrupts[0];
1280
- /**
1281
- * Capture the interrupt unconditionally — `interrupt(null)`
1282
- * and `interrupt(undefined)` are valid pauses (a custom
1283
- * node may want to pause without metadata) and the host
1284
- * still needs to know the run is awaiting resume. Gating
1285
- * on `payload != null` would silently downgrade a paused
1286
- * run to "completed" and let the `Stop` hook fire,
1287
- * breaking host resume handling.
1288
- */
1289
- this._interrupt = {
1290
- interruptId: first.id ?? '',
1291
- threadId,
1292
- payload: first.value,
1293
- };
1446
+ if (
1447
+ this._interrupt == null &&
1448
+ data.chunk != null &&
1449
+ isInterrupted<unknown>(data.chunk)
1450
+ ) {
1451
+ const interrupts = data.chunk[INTERRUPT];
1452
+ if (interrupts.length > 0) {
1453
+ const first = interrupts[0];
1454
+ this._interrupt = {
1455
+ interruptId: first.id ?? '',
1456
+ threadId,
1457
+ payload: first.value,
1458
+ };
1459
+ }
1460
+ }
1461
+
1462
+ const modelEndAt =
1463
+ eventName === GraphEvents.CHAT_MODEL_END ? Date.now() : undefined;
1464
+ const handler = this.handlerRegistry?.getHandler(eventName);
1465
+ if (handler) {
1466
+ await handler.handle(eventName, data, metadata, this.Graph);
1467
+ }
1468
+
1469
+ if (eventName === GraphEvents.CHAT_MODEL_END && this.Graph != null) {
1470
+ await this.Graph.closeOpenMessageStep(metadata, modelEndAt);
1471
+ }
1472
+
1473
+ const haltSignal = this.hookRegistry?.getHaltSignal(this.id);
1474
+ if (haltSignal != null) {
1475
+ this._haltedReason = haltSignal.reason;
1476
+ break;
1294
1477
  }
1295
1478
  }
1296
1479
 
1297
- /**
1298
- * Stamped before the handler runs: the close below happens after an
1299
- * arbitrarily slow host handler resolves, and the step's duration
1300
- * should end when the model did, not when the host finished with it.
1301
- */
1302
- const modelEndAt =
1303
- eventName === GraphEvents.CHAT_MODEL_END ? Date.now() : undefined;
1304
- const handler = this.handlerRegistry?.getHandler(eventName);
1305
- if (handler) {
1306
- await handler.handle(eventName, data, metadata, this.Graph);
1480
+ terminalAt = Date.now();
1481
+
1482
+ if (this._interrupt != null) {
1483
+ const interruptConfig = this.hasCheckpointer
1484
+ ? advanceCheckpointCursor(streamConfig)
1485
+ : streamConfig;
1486
+ await this.resolveInterruptResumeConfig(interruptConfig);
1487
+ return;
1488
+ }
1489
+ if (this._haltedReason != null) {
1490
+ return;
1307
1491
  }
1308
1492
 
1309
- /**
1310
- * A finished model call ends its lane's open message step. Placed
1311
- * here — not in `ModelEndHandler` — because hosts replace the
1312
- * CHAT_MODEL_END handler with their own instance, which would
1313
- * silently drop the close.
1314
- */
1315
- if (eventName === GraphEvents.CHAT_MODEL_END && this.Graph != null) {
1316
- await this.Graph.closeOpenMessageStep(metadata, modelEndAt);
1493
+ let stopReason = graph.preemptHaltReason;
1494
+ if (stopReason == null && graph.preemptIncomplete) {
1495
+ stopReason = 'preempt_incomplete';
1496
+ }
1497
+ if (stopReason == null && graph.outputTruncatedIncomplete) {
1498
+ stopReason = OUTPUT_TRUNCATED_HALT_REASON;
1317
1499
  }
1318
1500
 
1319
- /**
1320
- * Mid-flight halt: any hook (PreToolUse, PostToolUse,
1321
- * PostToolBatch, SubagentStart/Stop, PreCompact, PostCompact)
1322
- * that returned `preventContinuation: true` raises a halt
1323
- * signal on the registry via `executeHooks`. We poll between
1324
- * stream events and break out as soon as one is set so the
1325
- * graph doesn't take another model turn after the halting
1326
- * operation completes.
1327
- *
1328
- * This `break` is NOT graceful, despite what a `continue: false`
1329
- * reading suggests. Leaving the `for await` calls the iterator's
1330
- * `return()`, which cancels the reader
1331
- * (`@langchain/core/utils/stream`), and langgraph's stream wrapper
1332
- * turns that cancel into `_abortController.abort()`
1333
- * (`pregel/stream.js`). The in-flight model call or tool batch is
1334
- * torn down where it stands — it does not finish first.
1335
- *
1336
- * A halt is therefore the wrong tool for "stop generating but keep
1337
- * what you have". That is what `RunConfig.preemption` is for: it
1338
- * seals the stream at a provider-safe boundary and keeps the run.
1339
- */
1340
- const haltSignal = this.hookRegistry?.getHaltSignal(this.id);
1341
- if (haltSignal != null) {
1342
- this._haltedReason = haltSignal.reason;
1343
- break;
1501
+ const stopMessages = graph.getRunMessages() ?? stateInputs?.messages ?? [];
1502
+ const stopContinuationCount = graph.getStopContinuationCount();
1503
+ const continuationBudgetRemaining = Math.max(
1504
+ 0,
1505
+ this.maxStopContinuations - stopContinuationCount
1506
+ );
1507
+ let stopResult: AggregatedHookResult | undefined;
1508
+ if (this.hookRegistry?.hasHookFor('Stop', this.id) === true) {
1509
+ stopResult = await executeHooks({
1510
+ registry: this.hookRegistry,
1511
+ input: {
1512
+ hook_event_name: 'Stop',
1513
+ runId: this.id,
1514
+ threadId,
1515
+ agentId: graph.defaultAgentId,
1516
+ messages: stopMessages,
1517
+ stopReason,
1518
+ stopHookActive: stopContinuationCount > 0,
1519
+ continuationCount: stopContinuationCount,
1520
+ continuationBudgetRemaining,
1521
+ },
1522
+ sessionId: this.id,
1523
+ signal: config.signal,
1524
+ }).catch((): undefined => undefined);
1344
1525
  }
1345
- }
1346
1526
 
1347
- terminalAt = Date.now();
1527
+ if (this.hookRegistry?.hasHookFor('StopFinalize', this.id) === true) {
1528
+ const stopInjected =
1529
+ stopResult == null ? [] : materializeStopContinuation(stopResult);
1530
+ const continuationPrevented =
1531
+ stopReason != null || stopResult?.preventContinuation === true;
1532
+ const finalized = await executeHooks({
1533
+ registry: this.hookRegistry,
1534
+ input: {
1535
+ hook_event_name: 'StopFinalize',
1536
+ runId: this.id,
1537
+ threadId,
1538
+ agentId: graph.defaultAgentId,
1539
+ messages: stopMessages,
1540
+ stopReason,
1541
+ stopHookActive: stopContinuationCount > 0,
1542
+ continuationCount: stopContinuationCount,
1543
+ continuationBudgetRemaining,
1544
+ continuationPlanned:
1545
+ !continuationPrevented &&
1546
+ stopResult?.stopDecision === 'block' &&
1547
+ stopInjected.length > 0 &&
1548
+ continuationBudgetRemaining > 0,
1549
+ continuationPrevented,
1550
+ },
1551
+ sessionId: this.id,
1552
+ signal: config.signal,
1553
+ });
1554
+ assertFinalAdmissionSucceeded(finalized);
1555
+ stopResult = mergeAggregatedHookResults(stopResult, finalized);
1556
+ }
1348
1557
 
1349
- if (this._interrupt != null) {
1350
- await this.resolveInterruptResumeConfig(config);
1351
- }
1558
+ if (stopResult?.preventContinuation === true) {
1559
+ this._haltedReason =
1560
+ stopResult.stopReason ?? stopResult.reason ?? 'preventContinuation';
1561
+ return;
1562
+ }
1352
1563
 
1353
- /**
1354
- * Skip the Stop hook when the run paused on a HITL interrupt
1355
- * (still pending human input) or was halted by a hook (the host
1356
- * already chose to stop, so a Stop hook firing now would be
1357
- * misleading). The host fires Stop on the resumed-and-completed
1358
- * run instead.
1359
- */
1360
- if (
1361
- this._interrupt == null &&
1362
- this._haltedReason == null &&
1363
- this.hookRegistry?.hasHookFor('Stop', this.id) === true
1364
- ) {
1365
- await executeHooks({
1366
- registry: this.hookRegistry,
1367
- input: {
1368
- hook_event_name: 'Stop',
1369
- runId: this.id,
1370
- threadId,
1371
- agentId: graph.defaultAgentId,
1372
- messages: graph.getRunMessages() ?? stateInputs?.messages ?? [],
1564
+ const requestsContinuation =
1565
+ stopReason == null && stopResult?.stopDecision === 'block';
1566
+ if (requestsContinuation && stopResult != null) {
1567
+ const injected = materializeStopContinuation(stopResult);
1568
+ if (injected.length > 0) {
1569
+ if (stopContinuationCount >= this.maxStopContinuations) {
1570
+ throw new Error(
1571
+ 'Stop hook attempted to inject messages after the terminal continuation budget was exhausted.'
1572
+ );
1573
+ }
1574
+ const completedSegmentConfig = this.hasCheckpointer
1575
+ ? await resolveCompletedSegmentConfig(
1576
+ graphRunnable,
1577
+ streamConfig,
1578
+ graph.getStopContinuationExecutionId(),
1579
+ graph.getStreamSegment()
1580
+ )
1581
+ : streamConfig;
1582
+ const nextContinuationCount = stopContinuationCount + 1;
1583
+ graph.setStopContinuationCount(nextContinuationCount);
1584
+ graph.advanceStreamSegment();
1373
1585
  /**
1374
- * A seal whose boundary ended the turn early must say so. The
1375
- * hook-supplied reason wins when a `PreemptBoundary` hook halted
1376
- * with one a persistence/audit `Stop` hook should record the
1377
- * actual cause, not the generic label and `preempt_incomplete`
1378
- * is reserved for the boundary that simply had nothing to inject.
1586
+ * A checkpointer already owns the completed graph state, so only
1587
+ * the delta is submitted. Without one, each invocation starts from
1588
+ * empty state and must be seeded with the live full transcript.
1589
+ * Graph sidecars and the outer Run stay intact in both cases.
1379
1590
  */
1380
- stopReason:
1381
- graph.preemptHaltReason ??
1382
- (graph.preemptIncomplete ? 'preempt_incomplete' : undefined),
1383
- stopHookActive: false, // will be true when stop is triggered by a hook (Phase 2)
1384
- },
1385
- sessionId: this.id,
1386
- }).catch(() => {
1387
- /* Stop hook errors must not masquerade as stream failures */
1388
- });
1389
- }
1591
+ streamInputs = {
1592
+ messages: this.hasCheckpointer
1593
+ ? injected
1594
+ : [...graph.messages, ...injected],
1595
+ runStepState: graph.createRunStepResumeState(),
1596
+ };
1597
+ streamConfig = completedSegmentConfig;
1598
+ continue;
1599
+ }
1600
+ }
1390
1601
 
1391
- /**
1392
- * A `PreemptBoundary` hook that returned `preventContinuation` has its
1393
- * registry halt cleared by the graph — that is what stops the halt from
1394
- * cancelling the stream before the sealed turn commits — so the reason
1395
- * is carried across on the graph instead. Surfaced here, AFTER the
1396
- * `Stop` dispatch above, so the host still receives a completion signal
1397
- * to persist the partial answer with while `getHaltReason()` correctly
1398
- * reports that a hook stopped the run rather than the model finishing.
1399
- *
1400
- * An empty boundary — sealed, but nothing to inject because the host's
1401
- * queue was drained or cancelled in the meantime — cut the answer short
1402
- * just as surely, only without a hook-supplied reason. It surfaces
1403
- * through the same channel under the same name the `Stop` dispatch
1404
- * already used for its `stopReason`, so terminal consumers
1405
- * (`AgentSession` emits `run.halted`, not `run.completed`) cannot
1406
- * finalize a truncated answer as a natural finish.
1407
- */
1408
- if (this._haltedReason == null && graph.preemptHaltReason != null) {
1409
- this._haltedReason = graph.preemptHaltReason;
1410
- } else if (this._haltedReason == null && graph.preemptIncomplete) {
1411
- this._haltedReason = 'preempt_incomplete';
1602
+ if (graph.preemptHaltReason != null) {
1603
+ this._haltedReason = graph.preemptHaltReason;
1604
+ } else if (graph.preemptIncomplete) {
1605
+ this._haltedReason = 'preempt_incomplete';
1606
+ } else if (graph.outputTruncatedIncomplete) {
1607
+ this._haltedReason = OUTPUT_TRUNCATED_HALT_REASON;
1608
+ }
1609
+ return;
1412
1610
  }
1413
1611
  };
1414
1612
 
@@ -1432,6 +1630,10 @@ export class Run<_T extends t.BaseGraphState> {
1432
1630
  } catch (err) {
1433
1631
  terminalAt = Date.now();
1434
1632
  streamThrew = true;
1633
+ await langfuseHandler?.handleChainError(
1634
+ err instanceof Error ? err : new Error(String(err)),
1635
+ this.id
1636
+ );
1435
1637
  /**
1436
1638
  * Corroborate cancellation against an actually-aborted signal. A
1437
1639
  * provider SDK or host handler can reject with an `AbortError` while
@@ -1614,13 +1816,16 @@ export class Run<_T extends t.BaseGraphState> {
1614
1816
  }
1615
1817
 
1616
1818
  /**
1617
- * Returns the reason a hook halted the run via
1618
- * `preventContinuation: true`, or `undefined` if no hook halted.
1819
+ * Returns why the run ended without a natural completion, or `undefined`
1820
+ * when it completed normally. Reasons include hook- and prompt-driven
1821
+ * halts, `preempt_incomplete` when a cooperative seal ended the turn
1822
+ * without continuation content, and `output_truncated` when the provider
1823
+ * stopped a plain-text/reasoning response at its output-token ceiling.
1619
1824
  *
1620
1825
  * Hosts inspect this after `processStream` returns to distinguish a
1621
- * natural completion (`undefined`) from a hook-driven halt (a
1622
- * truthy string). Independent from `getInterrupt()` — a halted run
1623
- * has no interrupt; an interrupted run has no halt reason.
1826
+ * natural completion from a terminal partial response. Independent from
1827
+ * `getInterrupt()` — a halted run has no interrupt; an interrupted run has
1828
+ * no halt reason.
1624
1829
  */
1625
1830
  getHaltReason(): string | undefined {
1626
1831
  return this._haltedReason;
@@ -1833,7 +2038,8 @@ export class Run<_T extends t.BaseGraphState> {
1833
2038
  return;
1834
2039
  }
1835
2040
  this.Graph?.restoreRunStepResumeState(
1836
- getRunStepResumeState(persistedInterrupt.value)
2041
+ getRunStepResumeState(persistedInterrupt.value) ??
2042
+ snapshot.values?.runStepState
1837
2043
  );
1838
2044
  const persistedMessages = getPersistedMessages(snapshot);
1839
2045
  if (persistedMessages != null) {