@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
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,6 +34,7 @@ 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
40
  import {
@@ -94,15 +96,17 @@ import { LANGFUSE_OPERATION_METADATA_KEY } from '@/langfuseOperation';
94
96
  import { createTokenCounter, encodingForModel } from '@/utils/tokens';
95
97
  import { stampSyntheticProviderMessage } from '@/messages/provenance';
96
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';
102
107
  import { isBuiltRuntime } from '@/lazyRequire';
103
108
  import { initializeModel } from '@/llm/init';
104
109
  import { HandlerRegistry } from '@/events';
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
@@ -132,6 +136,60 @@ const ACTIVITY_PHASE_TRACE_NAME = 'LibreChat Activity Phase';
132
136
  const REASONING_LABEL_TRACE_NAME = 'LibreChat Reasoning Label';
133
137
  const OUTPUT_TRUNCATED_HALT_REASON = 'output_truncated';
134
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
+ }
192
+
135
193
  const CUSTOM_GRAPH_EVENTS = new Set<string>([
136
194
  GraphEvents.ON_AGENT_UPDATE,
137
195
  GraphEvents.ON_RUN_STEP,
@@ -228,7 +286,10 @@ function getInterruptHookSessionId(payload: unknown): string | undefined {
228
286
 
229
287
  type InterruptStateSnapshot = {
230
288
  config?: RunnableConfig;
231
- values?: { messages?: BaseMessage[] };
289
+ values?: {
290
+ messages?: BaseMessage[];
291
+ runStepState?: t.RunStepResumeState;
292
+ };
232
293
  tasks?: Array<{
233
294
  interrupts?: Array<{ id?: string; value?: unknown }>;
234
295
  state?: RunnableConfig | InterruptStateSnapshot;
@@ -241,10 +302,79 @@ type WorkflowWithStateHistory = {
241
302
  options?: { subgraphs?: boolean }
242
303
  ): Promise<InterruptStateSnapshot>;
243
304
  getStateHistory?(
244
- config: RunnableConfig
305
+ config: RunnableConfig,
306
+ options?: { filter?: Record<string, unknown>; limit?: number }
245
307
  ): AsyncIterableIterator<InterruptStateSnapshot>;
246
308
  };
247
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
+
248
378
  function getFirstPersistedInterrupt(
249
379
  snapshot: InterruptStateSnapshot
250
380
  ): { id: string; value: unknown } | undefined {
@@ -284,6 +414,27 @@ function getPersistedMessages(
284
414
 
285
415
  type ResumeCommandUpdate = ConstructorParameters<typeof Command>[0]['update'];
286
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
+
287
438
  function getResumeUpdateMessages(
288
439
  update: ResumeCommandUpdate
289
440
  ): BaseMessage[] | undefined {
@@ -316,6 +467,7 @@ export class Run<_T extends t.BaseGraphState> {
316
467
  private toolExecution?: t.ToolExecutionConfig;
317
468
  private subagentUsageSink?: t.SubagentUsageSink;
318
469
  private preemption?: t.StreamPreemption;
470
+ private maxStopContinuations: number;
319
471
  private streamLimits?: t.StreamLimits;
320
472
  private subagentTasks?: t.SubagentTaskConfig;
321
473
  private indexTokenCountMap?: Record<string, number>;
@@ -387,6 +539,9 @@ export class Run<_T extends t.BaseGraphState> {
387
539
  this.subagentUsageSink = config.subagentUsageSink;
388
540
  this.subagentTasks = config.subagentTasks;
389
541
  this.preemption = config.preemption;
542
+ this.maxStopContinuations = resolveMaxStopContinuations(
543
+ config.maxStopContinuations
544
+ );
390
545
  this.streamLimits = config.streamLimits;
391
546
 
392
547
  if (!config.graphConfig) {
@@ -729,12 +884,16 @@ export class Run<_T extends t.BaseGraphState> {
729
884
  config: t.RunConfig
730
885
  ): Promise<Run<T>> {
731
886
  await ensureSourceModeProviders();
732
- /** 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. */
733
892
  if (config.indexTokenCountMap && !config.tokenCounter) {
734
893
  const gc = config.graphConfig;
735
894
  const clientOpts =
736
895
  'agents' in gc ? gc.agents[0]?.clientOptions : gc.clientOptions;
737
- const model = (clientOpts as { model?: string } | undefined)?.model ?? '';
896
+ const model = resolveClientOptionsModel(clientOpts) ?? '';
738
897
  config.tokenCounter = await createTokenCounter(encodingForModel(model));
739
898
  }
740
899
  return new Run<T>(config);
@@ -1055,11 +1214,16 @@ export class Run<_T extends t.BaseGraphState> {
1055
1214
  delete config.configurable?.[SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY];
1056
1215
  delete config.configurable?.[SUBAGENT_RESUME_MANIFEST_CONFIG_KEY];
1057
1216
  }
1217
+ let overwriteLegacyResumeState = false;
1058
1218
  if (isResume) {
1059
1219
  await this.restoreInterruptFromCheckpoint(
1060
1220
  config,
1061
1221
  (inputs as Command).update
1062
1222
  );
1223
+ if (graph.getStopContinuationExecutionId() === '') {
1224
+ graph.startStopContinuationExecution(nanoid());
1225
+ overwriteLegacyResumeState = this.hasCheckpointer;
1226
+ }
1063
1227
  }
1064
1228
 
1065
1229
  /**
@@ -1110,6 +1274,7 @@ export class Run<_T extends t.BaseGraphState> {
1110
1274
  checkpointId === '' ? 0 : ++this.checkpointForkSeq,
1111
1275
  ]);
1112
1276
  graph.resetValues(streamOptions?.keepContent, checkpointScope);
1277
+ graph.startStopContinuationExecution(nanoid());
1113
1278
  }
1114
1279
  this._interrupt = undefined;
1115
1280
  this._haltedReason = undefined;
@@ -1175,6 +1340,7 @@ export class Run<_T extends t.BaseGraphState> {
1175
1340
  : undefined,
1176
1341
  traceAnchor: graph.langfuseTraceAnchor,
1177
1342
  runId: graph.langfuseScopeRunId,
1343
+ deferRootRunId: this.id,
1178
1344
  // The aggregate multi-agent policy from the runtime scope — the
1179
1345
  // handler must restore THIS (not the primary agent's config-derived
1180
1346
  // policy) when rejecting a foreign scope.
@@ -1189,6 +1355,7 @@ export class Run<_T extends t.BaseGraphState> {
1189
1355
  throw new Error('Run ID not provided');
1190
1356
  }
1191
1357
 
1358
+ config.runId = this.id;
1192
1359
  config.run_id = this.id;
1193
1360
  config.configurable = Object.assign(config.configurable ?? {}, {
1194
1361
  run_id: this.id,
@@ -1237,144 +1404,92 @@ export class Run<_T extends t.BaseGraphState> {
1237
1404
  let terminalAt: number | undefined;
1238
1405
 
1239
1406
  const consumeStream = async (): Promise<void> => {
1240
- /**
1241
- * `streamEvents` accepts both state inputs and `Command` (resume) at
1242
- * runtime, but our `CompiledStateWorkflow` type narrows the first
1243
- * arg to `BaseGraphState`. Cast on the call so the resume path
1244
- * type-checks without widening the wrapper for every caller.
1245
- */
1246
- const stream = graphRunnable.streamEvents(
1247
- inputs as t.IState,
1248
- { ...config, runName: graph.runName },
1249
- {
1250
- raiseError: true,
1251
- /**
1252
- * Prevent EventStreamCallbackHandler from processing custom events.
1253
- * Custom events are already handled via our createCustomEventCallback()
1254
- * which routes them through the handlerRegistry.
1255
- * Without this flag, EventStreamCallbackHandler throws errors when
1256
- * custom events are dispatched for run IDs not in its internal map
1257
- * (due to timing issues in parallel execution or after run cleanup).
1258
- */
1259
- ignoreCustomEvent: true,
1260
- }
1261
- );
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;
1262
1420
 
1263
- for await (const event of stream) {
1264
- 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
+ );
1265
1437
 
1266
- 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;
1267
1441
 
1268
- /** Skip custom events as they're handled by our callback */
1269
- if (CUSTOM_GRAPH_EVENTS.has(eventName)) {
1270
- continue;
1271
- }
1442
+ if (CUSTOM_GRAPH_EVENTS.has(eventName)) {
1443
+ continue;
1444
+ }
1272
1445
 
1273
- /**
1274
- * Detect interrupts surfaced by LangGraph as a synthetic
1275
- * `__interrupt__` field on the streamed chunk and stash the
1276
- * first one for the host to read via `run.getInterrupt()`
1277
- * once the stream drains. Captured as `unknown` because the
1278
- * SDK does not validate the runtime payload shape — the
1279
- * built-in ToolNode raises a `HumanInterruptPayload`
1280
- * (`tool_approval` / `ask_user_question`), but custom nodes
1281
- * can pass any payload to `interrupt()`. Callers narrow with
1282
- * the `isToolApprovalInterrupt` / `isAskUserQuestionInterrupt`
1283
- * guards or assert via `getInterrupt<T>()`.
1284
- */
1285
- if (
1286
- this._interrupt == null &&
1287
- data.chunk != null &&
1288
- isInterrupted<unknown>(data.chunk)
1289
- ) {
1290
- const interrupts = data.chunk[INTERRUPT];
1291
- if (interrupts.length > 0) {
1292
- const first = interrupts[0];
1293
- /**
1294
- * Capture the interrupt unconditionally — `interrupt(null)`
1295
- * and `interrupt(undefined)` are valid pauses (a custom
1296
- * node may want to pause without metadata) and the host
1297
- * still needs to know the run is awaiting resume. Gating
1298
- * on `payload != null` would silently downgrade a paused
1299
- * run to "completed" and let the `Stop` hook fire,
1300
- * breaking host resume handling.
1301
- */
1302
- this._interrupt = {
1303
- interruptId: first.id ?? '',
1304
- threadId,
1305
- payload: first.value,
1306
- };
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
+ }
1307
1460
  }
1308
- }
1309
1461
 
1310
- /**
1311
- * Stamped before the handler runs: the close below happens after an
1312
- * arbitrarily slow host handler resolves, and the step's duration
1313
- * should end when the model did, not when the host finished with it.
1314
- */
1315
- const modelEndAt =
1316
- eventName === GraphEvents.CHAT_MODEL_END ? Date.now() : undefined;
1317
- const handler = this.handlerRegistry?.getHandler(eventName);
1318
- if (handler) {
1319
- await handler.handle(eventName, data, metadata, this.Graph);
1320
- }
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
+ }
1321
1468
 
1322
- /**
1323
- * A finished model call ends its lane's open message step. Placed
1324
- * here — not in `ModelEndHandler` — because hosts replace the
1325
- * CHAT_MODEL_END handler with their own instance, which would
1326
- * silently drop the close.
1327
- */
1328
- if (eventName === GraphEvents.CHAT_MODEL_END && this.Graph != null) {
1329
- await this.Graph.closeOpenMessageStep(metadata, modelEndAt);
1330
- }
1469
+ if (eventName === GraphEvents.CHAT_MODEL_END && this.Graph != null) {
1470
+ await this.Graph.closeOpenMessageStep(metadata, modelEndAt);
1471
+ }
1331
1472
 
1332
- /**
1333
- * Mid-flight halt: any hook (PreToolUse, PostToolUse,
1334
- * PostToolBatch, SubagentStart/Stop, PreCompact, PostCompact)
1335
- * that returned `preventContinuation: true` raises a halt
1336
- * signal on the registry via `executeHooks`. We poll between
1337
- * stream events and break out as soon as one is set so the
1338
- * graph doesn't take another model turn after the halting
1339
- * operation completes.
1340
- *
1341
- * This `break` is NOT graceful, despite what a `continue: false`
1342
- * reading suggests. Leaving the `for await` calls the iterator's
1343
- * `return()`, which cancels the reader
1344
- * (`@langchain/core/utils/stream`), and langgraph's stream wrapper
1345
- * turns that cancel into `_abortController.abort()`
1346
- * (`pregel/stream.js`). The in-flight model call or tool batch is
1347
- * torn down where it stands — it does not finish first.
1348
- *
1349
- * A halt is therefore the wrong tool for "stop generating but keep
1350
- * what you have". That is what `RunConfig.preemption` is for: it
1351
- * seals the stream at a provider-safe boundary and keeps the run.
1352
- */
1353
- const haltSignal = this.hookRegistry?.getHaltSignal(this.id);
1354
- if (haltSignal != null) {
1355
- this._haltedReason = haltSignal.reason;
1356
- break;
1473
+ const haltSignal = this.hookRegistry?.getHaltSignal(this.id);
1474
+ if (haltSignal != null) {
1475
+ this._haltedReason = haltSignal.reason;
1476
+ break;
1477
+ }
1357
1478
  }
1358
- }
1359
1479
 
1360
- terminalAt = Date.now();
1480
+ terminalAt = Date.now();
1361
1481
 
1362
- if (this._interrupt != null) {
1363
- await this.resolveInterruptResumeConfig(config);
1364
- }
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;
1491
+ }
1365
1492
 
1366
- /**
1367
- * Skip the Stop hook when the run paused on a HITL interrupt
1368
- * (still pending human input) or was halted by a hook (the host
1369
- * already chose to stop, so a Stop hook firing now would be
1370
- * misleading). The host fires Stop on the resumed-and-completed
1371
- * run instead.
1372
- */
1373
- if (
1374
- this._interrupt == null &&
1375
- this._haltedReason == null &&
1376
- this.hookRegistry?.hasHookFor('Stop', this.id) === true
1377
- ) {
1378
1493
  let stopReason = graph.preemptHaltReason;
1379
1494
  if (stopReason == null && graph.preemptIncomplete) {
1380
1495
  stopReason = 'preempt_incomplete';
@@ -1382,56 +1497,116 @@ export class Run<_T extends t.BaseGraphState> {
1382
1497
  if (stopReason == null && graph.outputTruncatedIncomplete) {
1383
1498
  stopReason = OUTPUT_TRUNCATED_HALT_REASON;
1384
1499
  }
1385
- await executeHooks({
1386
- registry: this.hookRegistry,
1387
- input: {
1388
- hook_event_name: 'Stop',
1389
- runId: this.id,
1390
- threadId,
1391
- agentId: graph.defaultAgentId,
1392
- messages: graph.getRunMessages() ?? stateInputs?.messages ?? [],
1500
+
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);
1525
+ }
1526
+
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
+ }
1557
+
1558
+ if (stopResult?.preventContinuation === true) {
1559
+ this._haltedReason =
1560
+ stopResult.stopReason ?? stopResult.reason ?? 'preventContinuation';
1561
+ return;
1562
+ }
1563
+
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();
1393
1585
  /**
1394
- * A seal whose boundary ended the turn early must say so. The
1395
- * hook-supplied reason wins when a `PreemptBoundary` hook halted
1396
- * with one a persistence/audit `Stop` hook should record the
1397
- * actual cause, not the generic label and `preempt_incomplete`
1398
- * 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.
1399
1590
  */
1400
- stopReason,
1401
- stopHookActive: false, // will be true when stop is triggered by a hook (Phase 2)
1402
- },
1403
- sessionId: this.id,
1404
- }).catch(() => {
1405
- /* Stop hook errors must not masquerade as stream failures */
1406
- });
1407
- }
1591
+ streamInputs = {
1592
+ messages: this.hasCheckpointer
1593
+ ? injected
1594
+ : [...graph.messages, ...injected],
1595
+ runStepState: graph.createRunStepResumeState(),
1596
+ };
1597
+ streamConfig = completedSegmentConfig;
1598
+ continue;
1599
+ }
1600
+ }
1408
1601
 
1409
- /**
1410
- * A `PreemptBoundary` hook that returned `preventContinuation` has its
1411
- * registry halt cleared by the graph — that is what stops the halt from
1412
- * cancelling the stream before the sealed turn commits — so the reason
1413
- * is carried across on the graph instead. Surfaced here, AFTER the
1414
- * `Stop` dispatch above, so the host still receives a completion signal
1415
- * to persist the partial answer with while `getHaltReason()` correctly
1416
- * reports that a hook stopped the run rather than the model finishing.
1417
- *
1418
- * An empty boundary — sealed, but nothing to inject because the host's
1419
- * queue was drained or cancelled in the meantime — cut the answer short
1420
- * just as surely, only without a hook-supplied reason. It surfaces
1421
- * through the same channel under the same name the `Stop` dispatch
1422
- * already used for its `stopReason`, so terminal consumers
1423
- * (`AgentSession` emits `run.halted`, not `run.completed`) cannot
1424
- * finalize a truncated answer as a natural finish.
1425
- */
1426
- if (this._haltedReason == null && graph.preemptHaltReason != null) {
1427
- this._haltedReason = graph.preemptHaltReason;
1428
- } else if (this._haltedReason == null && graph.preemptIncomplete) {
1429
- this._haltedReason = 'preempt_incomplete';
1430
- } else if (
1431
- this._haltedReason == null &&
1432
- graph.outputTruncatedIncomplete
1433
- ) {
1434
- this._haltedReason = OUTPUT_TRUNCATED_HALT_REASON;
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;
1435
1610
  }
1436
1611
  };
1437
1612
 
@@ -1455,6 +1630,10 @@ export class Run<_T extends t.BaseGraphState> {
1455
1630
  } catch (err) {
1456
1631
  terminalAt = Date.now();
1457
1632
  streamThrew = true;
1633
+ await langfuseHandler?.handleChainError(
1634
+ err instanceof Error ? err : new Error(String(err)),
1635
+ this.id
1636
+ );
1458
1637
  /**
1459
1638
  * Corroborate cancellation against an actually-aborted signal. A
1460
1639
  * provider SDK or host handler can reject with an `AbortError` while
@@ -1859,7 +2038,8 @@ export class Run<_T extends t.BaseGraphState> {
1859
2038
  return;
1860
2039
  }
1861
2040
  this.Graph?.restoreRunStepResumeState(
1862
- getRunStepResumeState(persistedInterrupt.value)
2041
+ getRunStepResumeState(persistedInterrupt.value) ??
2042
+ snapshot.values?.runStepState
1863
2043
  );
1864
2044
  const persistedMessages = getPersistedMessages(snapshot);
1865
2045
  if (persistedMessages != null) {