@librechat/agents 3.4.0 → 3.4.1

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 (114) hide show
  1. package/dist/cjs/graphs/Graph.cjs +111 -27
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/hooks/HookRegistry.cjs +83 -0
  4. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  5. package/dist/cjs/hooks/executeHooks.cjs +60 -14
  6. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  7. package/dist/cjs/hooks/index.cjs.map +1 -1
  8. package/dist/cjs/hooks/types.cjs +2 -0
  9. package/dist/cjs/hooks/types.cjs.map +1 -1
  10. package/dist/cjs/llm/init.cjs +3 -3
  11. package/dist/cjs/llm/invoke.cjs +2 -2
  12. package/dist/cjs/main.cjs +14 -13
  13. package/dist/cjs/messages/format.cjs +40 -23
  14. package/dist/cjs/messages/format.cjs.map +1 -1
  15. package/dist/cjs/run.cjs +82 -15
  16. package/dist/cjs/run.cjs.map +1 -1
  17. package/dist/cjs/session/AgentSession.cjs +37 -2
  18. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  19. package/dist/cjs/stream.cjs +1 -1
  20. package/dist/cjs/summarization/node.cjs +6 -3
  21. package/dist/cjs/summarization/node.cjs.map +1 -1
  22. package/dist/cjs/tools/BashExecutor.cjs +1 -1
  23. package/dist/cjs/tools/CodeExecutor.cjs +1 -1
  24. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
  25. package/dist/cjs/tools/ToolNode.cjs +316 -94
  26. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  27. package/dist/cjs/tools/ToolSearch.cjs +1 -1
  28. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +630 -78
  29. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  30. package/dist/cjs/tools/subagent/SubagentReplay.cjs +175 -0
  31. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
  32. package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
  33. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  34. package/dist/cjs/utils/index.cjs +2 -2
  35. package/dist/esm/graphs/Graph.mjs +111 -27
  36. package/dist/esm/graphs/Graph.mjs.map +1 -1
  37. package/dist/esm/hooks/HookRegistry.mjs +83 -0
  38. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  39. package/dist/esm/hooks/executeHooks.mjs +60 -14
  40. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  41. package/dist/esm/hooks/index.mjs.map +1 -1
  42. package/dist/esm/hooks/types.mjs +2 -1
  43. package/dist/esm/hooks/types.mjs.map +1 -1
  44. package/dist/esm/llm/init.mjs +1 -1
  45. package/dist/esm/llm/invoke.mjs +2 -2
  46. package/dist/esm/main.mjs +11 -11
  47. package/dist/esm/messages/format.mjs +40 -23
  48. package/dist/esm/messages/format.mjs.map +1 -1
  49. package/dist/esm/run.mjs +83 -16
  50. package/dist/esm/run.mjs.map +1 -1
  51. package/dist/esm/session/AgentSession.mjs +37 -2
  52. package/dist/esm/session/AgentSession.mjs.map +1 -1
  53. package/dist/esm/stream.mjs +1 -1
  54. package/dist/esm/summarization/node.mjs +6 -3
  55. package/dist/esm/summarization/node.mjs.map +1 -1
  56. package/dist/esm/tools/BashExecutor.mjs +1 -1
  57. package/dist/esm/tools/CodeExecutor.mjs +1 -1
  58. package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
  59. package/dist/esm/tools/ToolNode.mjs +317 -95
  60. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  61. package/dist/esm/tools/ToolSearch.mjs +1 -1
  62. package/dist/esm/tools/subagent/SubagentExecutor.mjs +631 -79
  63. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  64. package/dist/esm/tools/subagent/SubagentReplay.mjs +168 -0
  65. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
  66. package/dist/esm/tools/toolOutputReferences.mjs +20 -0
  67. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  68. package/dist/esm/utils/index.mjs +2 -2
  69. package/dist/types/graphs/Graph.d.ts +16 -1
  70. package/dist/types/hooks/HookRegistry.d.ts +10 -1
  71. package/dist/types/hooks/executeHooks.d.ts +5 -1
  72. package/dist/types/hooks/index.d.ts +2 -2
  73. package/dist/types/hooks/types.d.ts +12 -0
  74. package/dist/types/run.d.ts +4 -1
  75. package/dist/types/session/AgentSession.d.ts +1 -0
  76. package/dist/types/tools/ToolNode.d.ts +23 -15
  77. package/dist/types/tools/subagent/SubagentExecutor.d.ts +46 -7
  78. package/dist/types/tools/subagent/SubagentReplay.d.ts +81 -0
  79. package/dist/types/tools/toolOutputReferences.d.ts +12 -0
  80. package/dist/types/types/graph.d.ts +5 -5
  81. package/dist/types/types/hitl.d.ts +15 -0
  82. package/package.json +6 -2
  83. package/src/graphs/Graph.ts +246 -52
  84. package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
  85. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
  86. package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
  87. package/src/hooks/HookRegistry.ts +151 -1
  88. package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
  89. package/src/hooks/__tests__/executeHooks.test.ts +160 -0
  90. package/src/hooks/executeHooks.ts +133 -15
  91. package/src/hooks/index.ts +3 -1
  92. package/src/hooks/types.ts +16 -0
  93. package/src/messages/format.ts +54 -26
  94. package/src/messages/formatAgentMessages.reducer.test.ts +162 -0
  95. package/src/messages/formatAgentMessages.steer.test.ts +16 -11
  96. package/src/messages/formatAgentMessages.test.ts +7 -2
  97. package/src/run.ts +164 -18
  98. package/src/session/AgentSession.ts +52 -2
  99. package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
  100. package/src/summarization/__tests__/node.test.ts +32 -14
  101. package/src/summarization/node.ts +15 -11
  102. package/src/tools/ToolNode.ts +631 -157
  103. package/src/tools/__tests__/SubagentExecutor.test.ts +542 -6
  104. package/src/tools/__tests__/SubagentReplay.test.ts +300 -0
  105. package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
  106. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +486 -2
  107. package/src/tools/__tests__/hitl.test.ts +234 -5
  108. package/src/tools/__tests__/subagentHooks.test.ts +1040 -62
  109. package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
  110. package/src/tools/subagent/SubagentExecutor.ts +1369 -131
  111. package/src/tools/subagent/SubagentReplay.ts +575 -0
  112. package/src/tools/toolOutputReferences.ts +40 -1
  113. package/src/types/graph.ts +5 -5
  114. package/src/types/hitl.ts +16 -0
@@ -17,6 +17,13 @@ import type {
17
17
  MessageContent,
18
18
  } from '@langchain/core/messages';
19
19
  import type { ToolCall } from '@langchain/core/messages/tool';
20
+ import type {
21
+ ReplayableSubagentTool,
22
+ SubagentGraphResumeState,
23
+ SubagentResumeManifest,
24
+ SubagentToolNodeResumeState,
25
+ SettledSubagentToolOutput,
26
+ } from '@/tools/subagent/SubagentReplay';
20
27
  import type {
21
28
  ResolvedStreamLimits,
22
29
  RunBreakerScope,
@@ -60,14 +67,6 @@ import {
60
67
  appendPredecessorHandoffCue,
61
68
  removePredecessorHandoffCue,
62
69
  } from '@/messages';
63
- import {
64
- attemptInvoke,
65
- tryFallbackProviders,
66
- getFallbackErrorContext,
67
- getFallbackOverflowCandidates,
68
- projectMessagesForProvider,
69
- resolveServingModelId,
70
- } from '@/llm/invoke';
71
70
  import {
72
71
  resetIfNotEmpty,
73
72
  isAnthropicLike,
@@ -79,6 +78,21 @@ import {
79
78
  joinKeys,
80
79
  sleep,
81
80
  } from '@/utils';
81
+ import {
82
+ attemptInvoke,
83
+ tryFallbackProviders,
84
+ getFallbackErrorContext,
85
+ getFallbackOverflowCandidates,
86
+ projectMessagesForProvider,
87
+ resolveServingModelId,
88
+ } from '@/llm/invoke';
89
+ import {
90
+ resolveStreamLimits,
91
+ StreamLimitExceededError,
92
+ sweepStaleStreamLimitEntries,
93
+ STREAM_LIMIT_EPOCH_KEY,
94
+ RUN_BREAKER_SCOPE_CONFIG_KEY,
95
+ } from '@/llm/streamLimits';
82
96
  import {
83
97
  Constants,
84
98
  GraphNodeKeys,
@@ -122,17 +136,11 @@ import {
122
136
  findCallback,
123
137
  type CallbackEntry,
124
138
  } from '@/utils/callbacks';
125
- import {
126
- resolveStreamLimits,
127
- StreamLimitExceededError,
128
- sweepStaleStreamLimitEntries,
129
- STREAM_LIMIT_EPOCH_KEY,
130
- RUN_BREAKER_SCOPE_CONFIG_KEY,
131
- } from '@/llm/streamLimits';
132
139
  import { partitionAndMarkOpenRouterToolCache } from '@/llm/openrouter/toolCache';
133
140
  import { ToolNode as CustomToolNode, toolsCondition } from '@/tools/ToolNode';
134
141
  import { shouldTraceToolNodeForLangfuse } from '@/langfuseToolOutputTracing';
135
142
  import { createLocalCodingToolBundle } from '@/tools/local/LocalCodingTools';
143
+ import { SUBAGENT_REPLAY_CONTROLLER } from '@/tools/subagent/SubagentReplay';
136
144
  import { SubagentExecutor, resolveSubagentConfigs } from '@/tools/subagent';
137
145
  import { partitionAndMarkBedrockToolCache } from '@/llm/bedrock/toolCache';
138
146
  import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
@@ -630,6 +638,10 @@ export abstract class Graph<
630
638
  _TNodeName extends string = string,
631
639
  > {
632
640
  abstract resetValues(keepContent?: boolean, checkpointScope?: string): void;
641
+ restoreCheckpointMessages(
642
+ _messages: BaseMessage[],
643
+ _pendingMessages?: BaseMessage[]
644
+ ): void {}
633
645
  abstract initializeTools({
634
646
  currentTools,
635
647
  currentToolMap,
@@ -848,11 +860,18 @@ export abstract class Graph<
848
860
  // Flush each compiled ToolNode's direct-path turn cache so it
849
861
  // doesn't leak across Runs (Codex P2 #33). The cache survives
850
862
  // `run()` re-entry by design (resume-stable), but end-of-Run
851
- // is the right point to reset it.
863
+ // is the right point to reset it. Retain the registrations because
864
+ // the compiled workflow can be reused for later Runs; compilation
865
+ // will not register these instances again.
852
866
  for (const node of this._compiledToolNodes) {
853
867
  node.clearDirectPathTurns();
854
868
  }
855
- this._compiledToolNodes.clear();
869
+ // Subagent executors are likewise compiled once and reused. Clear
870
+ // their per-Run state without dropping the registrations needed by
871
+ // subsequent cleanup cycles.
872
+ for (const executor of this._subagentExecutors) {
873
+ executor.clearHeavyState();
874
+ }
856
875
  this.sessions.clear();
857
876
  }
858
877
 
@@ -904,10 +923,137 @@ export abstract class Graph<
904
923
  */
905
924
  protected registerCompiledToolNode(node: {
906
925
  clearDirectPathTurns(): void;
926
+ createSubagentResumeState(): SubagentToolNodeResumeState;
927
+ restoreSubagentResumeState(state: SubagentToolNodeResumeState): void;
907
928
  }): void {
908
929
  this._compiledToolNodes.add(node);
909
930
  }
910
931
 
932
+ protected registerSubagentExecutor(executor: SubagentExecutor): void {
933
+ this._subagentExecutors.add(executor);
934
+ }
935
+
936
+ protected resetSubagentCheckpointThreadIds(): void {
937
+ for (const executor of this._subagentExecutors) {
938
+ executor.resetCheckpointThreadIds();
939
+ }
940
+ }
941
+
942
+ getChildCheckpointThreadIds(): string[] {
943
+ const threadIds = new Set<string>();
944
+ for (const executor of this._subagentExecutors) {
945
+ for (const threadId of executor.getChildCheckpointThreadIds()) {
946
+ threadIds.add(threadId);
947
+ }
948
+ }
949
+ return [...threadIds];
950
+ }
951
+
952
+ createSubagentResumeState(runId: string): SubagentGraphResumeState {
953
+ return {
954
+ toolCallSteps: [...this.toolCallStepIds].map(([toolCallId, stepId]) => ({
955
+ toolCallId,
956
+ stepId,
957
+ })),
958
+ toolSessions: [...this.sessions].map(([toolName, context]) => ({
959
+ toolName,
960
+ context: {
961
+ ...context,
962
+ ...(context.files == null
963
+ ? {}
964
+ : { files: context.files.map((file) => ({ ...file })) }),
965
+ },
966
+ })),
967
+ toolNodes: [...this._compiledToolNodes].map((node) =>
968
+ node.createSubagentResumeState()
969
+ ),
970
+ eagerToolUsage: [
971
+ {
972
+ agentId: '',
973
+ toolUsageCounts: [...this.eagerEventToolUsageCount].map(
974
+ ([toolName, count]) => ({ toolName, count })
975
+ ),
976
+ },
977
+ ...[...this.eagerEventToolUsageCountsByAgentId].map(
978
+ ([agentId, usageCounts]) => ({
979
+ agentId,
980
+ toolUsageCounts: [...usageCounts].map(([toolName, count]) => ({
981
+ toolName,
982
+ count,
983
+ })),
984
+ })
985
+ ),
986
+ ],
987
+ eagerToolSuppressions: [...this.eagerEventToolSuppressions],
988
+ ...(this._toolOutputRegistry == null
989
+ ? {}
990
+ : {
991
+ toolOutputReferences: this._toolOutputRegistry.snapshotState(runId),
992
+ }),
993
+ };
994
+ }
995
+
996
+ restoreSubagentResumeState(
997
+ state: SubagentGraphResumeState,
998
+ runId: string
999
+ ): void {
1000
+ const toolNodesByKey = new Map(
1001
+ [...this._compiledToolNodes].map((node) => {
1002
+ const nodeState = node.createSubagentResumeState();
1003
+ return [nodeState.stateKey, node] as const;
1004
+ })
1005
+ );
1006
+ if (toolNodesByKey.size !== state.toolNodes.length) {
1007
+ throw new Error('Cannot restore changed subagent tool topology.');
1008
+ }
1009
+ for (const nodeState of state.toolNodes) {
1010
+ if (!toolNodesByKey.has(nodeState.stateKey)) {
1011
+ throw new Error(
1012
+ `Cannot restore subagent tool state for "${nodeState.stateKey}".`
1013
+ );
1014
+ }
1015
+ }
1016
+ const registry =
1017
+ state.toolOutputReferences == null
1018
+ ? undefined
1019
+ : this.getOrCreateToolOutputRegistry();
1020
+ if (state.toolOutputReferences != null && registry == null) {
1021
+ throw new Error('Cannot restore disabled tool output references.');
1022
+ }
1023
+
1024
+ this.toolCallStepIds.clear();
1025
+ for (const { toolCallId, stepId } of state.toolCallSteps) {
1026
+ this.toolCallStepIds.set(toolCallId, stepId);
1027
+ }
1028
+ this.sessions.clear();
1029
+ for (const { toolName, context } of state.toolSessions) {
1030
+ this.sessions.set(toolName, {
1031
+ ...context,
1032
+ ...(context.files == null
1033
+ ? {}
1034
+ : { files: context.files.map((file) => ({ ...file })) }),
1035
+ });
1036
+ }
1037
+ for (const nodeState of state.toolNodes) {
1038
+ const node = toolNodesByKey.get(nodeState.stateKey)!;
1039
+ node.restoreSubagentResumeState(nodeState);
1040
+ }
1041
+ this.clearEagerEventToolUsageCounts();
1042
+ for (const usageState of state.eagerToolUsage) {
1043
+ const usageCounts = this.getEagerEventToolUsageCount(usageState.agentId);
1044
+ for (const { toolName, count } of usageState.toolUsageCounts) {
1045
+ usageCounts.set(toolName, count);
1046
+ }
1047
+ }
1048
+ this.eagerEventToolSuppressions.clear();
1049
+ for (const toolName of state.eagerToolSuppressions) {
1050
+ this.eagerEventToolSuppressions.add(toolName);
1051
+ }
1052
+ if (state.toolOutputReferences != null && registry != null) {
1053
+ registry.restoreState(runId, state.toolOutputReferences);
1054
+ }
1055
+ }
1056
+
911
1057
  /**
912
1058
  * Returns the shared `ToolOutputReferenceRegistry` for this run,
913
1059
  * constructing it on first access. Returns `undefined` when the
@@ -957,7 +1103,10 @@ export abstract class Graph<
957
1103
  */
958
1104
  private _compiledToolNodes: Set<{
959
1105
  clearDirectPathTurns(): void;
1106
+ createSubagentResumeState(): SubagentToolNodeResumeState;
1107
+ restoreSubagentResumeState(state: SubagentToolNodeResumeState): void;
960
1108
  }> = new Set();
1109
+ private _subagentExecutors = new Set<SubagentExecutor>();
961
1110
  public getOrCreateFileCheckpointer(): t.LocalFileCheckpointer | undefined {
962
1111
  // Return the cached instance unconditionally if one exists. The
963
1112
  // toolExecution check below decides whether to *create* a new
@@ -1004,6 +1153,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1004
1153
  /** Whether the workflow was actually compiled with a checkpointer. */
1005
1154
  hasCompiledCheckpointer: boolean = false;
1006
1155
  messages: BaseMessage[] = [];
1156
+ /** Whether a rebuilt resume seeded the message baseline from its checkpoint. */
1157
+ private hasRestoredCheckpointMessages = false;
1007
1158
  /** Cached run messages preserved before clearHeavyState() so getRunMessages() works after cleanup. */
1008
1159
  private cachedRunMessages?: BaseMessage[];
1009
1160
  /** Per-agent discovery snapshots preserved before contexts are reset on cleanup. */
@@ -1058,8 +1209,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1058
1209
  * generation key + chunk index (see `resolveGenerationKey`). Per-run
1059
1210
  * accumulation state, cleared by both reset paths.
1060
1211
  */
1061
- streamedToolCallArgTallies: Map<string, StreamedToolCallArgTally> =
1062
- new Map();
1212
+ streamedToolCallArgTallies: Map<string, StreamedToolCallArgTally> = new Map();
1063
1213
  /** Streamed chunk events per model generation, keyed by generation key. */
1064
1214
  streamDeltaEventCounts: Map<string, StreamDeltaEventTally> = new Map();
1065
1215
  /** Per-chunk-object, per-generation charge balances (lazily created; see
@@ -1213,7 +1363,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1213
1363
  /* Init */
1214
1364
 
1215
1365
  resetValues(keepContent?: boolean, checkpointScope?: string): void {
1366
+ this.resetSubagentCheckpointThreadIds();
1216
1367
  this.messages = [];
1368
+ this.hasRestoredCheckpointMessages = false;
1217
1369
  this.cachedRunMessages = undefined;
1218
1370
  this.cachedDiscoveredTools = undefined;
1219
1371
  this.config = resetIfNotEmpty(this.config, undefined);
@@ -1310,6 +1462,23 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1310
1462
  : undefined;
1311
1463
  }
1312
1464
 
1465
+ /** Seeds the sidecar message view that checkpoint restoration bypasses. */
1466
+ override restoreCheckpointMessages(
1467
+ messages: BaseMessage[],
1468
+ pendingMessages?: BaseMessage[]
1469
+ ): void {
1470
+ if (this.messages.length > 0) {
1471
+ return;
1472
+ }
1473
+ this.messages =
1474
+ pendingMessages == null
1475
+ ? [...messages]
1476
+ : messagesStateReducer(messages, pendingMessages);
1477
+ this.startIndex = this.messages.length;
1478
+ this.hasRestoredCheckpointMessages = true;
1479
+ this.cachedRunMessages = undefined;
1480
+ }
1481
+
1313
1482
  override clearHeavyState(): void {
1314
1483
  this.cachedRunMessages = this.messages.slice(this.startIndex);
1315
1484
  this.cachedDiscoveredTools = new Map(
@@ -1798,6 +1967,15 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1798
1967
  runLangfuse: this.langfuse,
1799
1968
  agentLangfuse: agentContext?.langfuse,
1800
1969
  });
1970
+ const interruptingToolNames = new Set(this.interruptingToolNames ?? []);
1971
+ if (
1972
+ this.humanInTheLoop?.enabled === true &&
1973
+ (agentContext?.subagentConfigs?.length ?? 0) > 0
1974
+ ) {
1975
+ interruptingToolNames.add(Constants.SUBAGENT);
1976
+ }
1977
+ const effectiveInterruptingToolNames =
1978
+ interruptingToolNames.size > 0 ? interruptingToolNames : undefined;
1801
1979
 
1802
1980
  if (eventDrivenMode) {
1803
1981
  const schemaTools = createSchemaOnlyTools(toolDefinitions);
@@ -1849,11 +2027,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1849
2027
  eagerEventToolSuppressions: this.eagerEventToolSuppressions,
1850
2028
  toolExecution: this.toolExecution,
1851
2029
  directToolNames: directToolNames.size > 0 ? directToolNames : undefined,
1852
- interruptingToolNames:
1853
- this.interruptingToolNames != null &&
1854
- this.interruptingToolNames.length > 0
1855
- ? new Set(this.interruptingToolNames)
1856
- : undefined,
2030
+ interruptingToolNames: effectiveInterruptingToolNames,
1857
2031
  maxContextTokens: agentContext?.maxContextTokens,
1858
2032
  maxToolResultChars: agentContext?.maxToolResultChars,
1859
2033
  toolOutputRegistry: this.getOrCreateToolOutputRegistry(),
@@ -1916,11 +2090,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1916
2090
  sessions: this.sessions,
1917
2091
  toolExecution: this.toolExecution,
1918
2092
  codeSessionToolNames: this.codeSessionToolNames,
1919
- interruptingToolNames:
1920
- this.interruptingToolNames != null &&
1921
- this.interruptingToolNames.length > 0
1922
- ? new Set(this.interruptingToolNames)
1923
- : undefined,
2093
+ interruptingToolNames: effectiveInterruptingToolNames,
1924
2094
  hookRegistry: this.hookRegistry,
1925
2095
  humanInTheLoop: this.humanInTheLoop,
1926
2096
  maxContextTokens: agentContext?.maxContextTokens,
@@ -3377,7 +3547,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3377
3547
  * would start new provider work after the run-wide breaker fired.
3378
3548
  * Rethrow the breaker's own stream-limit reason instead. */
3379
3549
  {
3380
- const trippedReason = this.resolveTrippedBreakerReason(attemptBreaker.signal);
3550
+ const trippedReason = this.resolveTrippedBreakerReason(
3551
+ attemptBreaker.signal
3552
+ );
3381
3553
  if (trippedReason != null) {
3382
3554
  throw trippedReason;
3383
3555
  }
@@ -3624,7 +3796,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3624
3796
  }
3625
3797
  {
3626
3798
  /** Same sibling-abort translation guard as the primary catch. */
3627
- const trippedReason = this.resolveTrippedBreakerReason(attemptBreaker.signal);
3799
+ const trippedReason = this.resolveTrippedBreakerReason(
3800
+ attemptBreaker.signal
3801
+ );
3628
3802
  if (trippedReason != null) {
3629
3803
  throw trippedReason;
3630
3804
  }
@@ -4061,6 +4235,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4061
4235
  tokenCounter: agentContext.tokenCounter,
4062
4236
  usageSink: this.subagentUsageSink,
4063
4237
  streamLimits: this.streamLimits,
4238
+ humanInTheLoop: this.humanInTheLoop,
4239
+ checkpointer: this.compileOptions?.checkpointer,
4064
4240
  maxDepth: effectiveSubagentDepth,
4065
4241
  createChildGraph: (input): StandardGraph => {
4066
4242
  const childGraph = new StandardGraph(input);
@@ -4072,12 +4248,12 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4072
4248
  getParentHandlerRegistry()
4073
4249
  );
4074
4250
  childGraph.hookRegistry = this.hookRegistry;
4075
- /**
4076
- * Do not propagate `humanInTheLoop` into the child graph yet:
4077
- * nested subagent interrupts need a stable child checkpoint and
4078
- * resume bridge. Child hooks still fire; `ask` decisions fail
4079
- * closed inside the subagent until that flow is implemented.
4080
- */
4251
+ childGraph.humanInTheLoop = this.humanInTheLoop;
4252
+ if (this.humanInTheLoop?.enabled === true) {
4253
+ childGraph.compileOptions = {
4254
+ checkpointer: this.compileOptions?.checkpointer,
4255
+ };
4256
+ }
4081
4257
  childGraph.toolOutputReferences = this.toolOutputReferences;
4082
4258
  childGraph.eagerEventToolExecution = this.eagerEventToolExecution;
4083
4259
  childGraph.codeSessionToolNames = this.codeSessionToolNames;
@@ -4099,6 +4275,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4099
4275
  return childGraph;
4100
4276
  },
4101
4277
  });
4278
+ this.registerSubagentExecutor(executor);
4102
4279
 
4103
4280
  const subagentTool = tool(async (rawInput, config) => {
4104
4281
  const input = rawInput as {
@@ -4113,20 +4290,22 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4113
4290
  const subagentType =
4114
4291
  typeof input.subagent_type === 'string' ? input.subagent_type : '';
4115
4292
  const threadId = config.configurable?.thread_id as string | undefined;
4116
- /**
4117
- * When the tool is dispatched from an LLM's `tool_call`, LangChain
4118
- * threads the originating `ToolCall` onto the RunnableConfig as
4119
- * `config.toolCall` (see `ToolRunnableConfig` in
4120
- * `@langchain/core/tools` internal but stable since ≥0.3.x).
4121
- * Surfacing its id lets hosts correlate `SubagentUpdateEvent`s
4122
- * back to the parent's `tool_call_id` deterministically — no
4123
- * temporal heuristics needed. If a future LangChain version
4124
- * changes the threading, the type-guarded read falls back to
4125
- * `undefined` and the correlation degrades gracefully.
4126
- */
4127
- const toolCall = (config as { toolCall?: { id?: string } }).toolCall;
4128
- const parentToolCallId =
4129
- typeof toolCall?.id === 'string' ? toolCall.id : undefined;
4293
+ /** Surface the parent call id so child checkpoints, interrupts, and
4294
+ * update events remain correlated across replay and resume. */
4295
+ const toolRuntime = config as {
4296
+ toolCallId?: string;
4297
+ toolCall?: { id?: string };
4298
+ };
4299
+ const toolCall = toolRuntime.toolCall;
4300
+ let parentToolCallId: string | undefined;
4301
+ if (
4302
+ typeof toolRuntime.toolCallId === 'string' &&
4303
+ toolRuntime.toolCallId !== ''
4304
+ ) {
4305
+ parentToolCallId = toolRuntime.toolCallId;
4306
+ } else if (typeof toolCall?.id === 'string' && toolCall.id !== '') {
4307
+ parentToolCallId = toolCall.id;
4308
+ }
4130
4309
  /** The parent tool batch's entry-captured scope (stamped by
4131
4310
  * ToolNode before PreToolUse hooks) — binds this child to the
4132
4311
  * run that dispatched it, not to whatever controller a reset
@@ -4152,6 +4331,21 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4152
4331
  });
4153
4332
  return result.content;
4154
4333
  }, buildSubagentToolParams(resolvedConfigs));
4334
+ const replayableSubagentTool = subagentTool as typeof subagentTool &
4335
+ ReplayableSubagentTool;
4336
+ replayableSubagentTool[SUBAGENT_REPLAY_CONTROLLER] = {
4337
+ getResumeManifest: (
4338
+ parentToolCallIds
4339
+ ): Promise<SubagentResumeManifest | undefined> =>
4340
+ executor.getResumeManifest(parentToolCallIds),
4341
+ getSettledOutput: (
4342
+ call,
4343
+ config
4344
+ ): Promise<SettledSubagentToolOutput | undefined> =>
4345
+ executor.getSettledToolOutput(call, config),
4346
+ persistSettledOutput: (call, config, output): Promise<void> =>
4347
+ executor.persistSettledToolOutput(call, config, output),
4348
+ };
4155
4349
 
4156
4350
  if (!agentContext.graphTools) {
4157
4351
  agentContext.graphTools = [];
@@ -4399,7 +4593,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4399
4593
  const StateAnnotation = Annotation.Root({
4400
4594
  messages: Annotation<BaseMessage[]>({
4401
4595
  reducer: (a, b) => {
4402
- if (!this.messages.length) {
4596
+ if (!this.messages.length && !this.hasRestoredCheckpointMessages) {
4403
4597
  this.startIndex = a.length + b.length;
4404
4598
  }
4405
4599
  const result = messagesStateReducer(a, b);
@@ -1,9 +1,9 @@
1
- import { describe, it, expect } from '@jest/globals';
1
+ import { describe, it, expect, jest } from '@jest/globals';
2
2
  import type { RunnableConfig } from '@langchain/core/runnables';
3
3
  import type * as t from '@/types';
4
4
  import { StreamLimitExceededError } from '@/llm/streamLimits';
5
- import { Providers } from '@/common';
6
5
  import { StandardGraph } from '../Graph';
6
+ import { Providers } from '@/common';
7
7
 
8
8
  const makeAgent = (agentId: string): t.AgentInputs => ({
9
9
  agentId,
@@ -25,7 +25,51 @@ const makeTrip = (): StreamLimitExceededError =>
25
25
  toolName: 'db_query',
26
26
  });
27
27
 
28
+ type CompiledRegistrationInternals = {
29
+ _compiledToolNodes: Set<{ clearDirectPathTurns(): void }>;
30
+ _subagentExecutors: Set<{
31
+ clearHeavyState(): void;
32
+ resetCheckpointThreadIds(): void;
33
+ }>;
34
+ };
35
+
28
36
  describe('run breaker lifecycle', () => {
37
+ it('retains compiled cleanup registrations across graph reuse', () => {
38
+ const graph = makeGraph();
39
+ const internals = graph as unknown as CompiledRegistrationInternals;
40
+ const clearDirectPathTurns = jest.fn();
41
+ const clearSubagentState = jest.fn();
42
+ const resetCheckpointThreadIds = jest.fn();
43
+ internals._compiledToolNodes.add({ clearDirectPathTurns });
44
+ internals._subagentExecutors.add({
45
+ clearHeavyState: clearSubagentState,
46
+ resetCheckpointThreadIds,
47
+ });
48
+
49
+ graph.clearHeavyState();
50
+ graph.clearHeavyState();
51
+
52
+ expect(clearDirectPathTurns).toHaveBeenCalledTimes(2);
53
+ expect(clearSubagentState).toHaveBeenCalledTimes(2);
54
+ expect(internals._compiledToolNodes.size).toBe(1);
55
+ expect(internals._subagentExecutors.size).toBe(1);
56
+ });
57
+
58
+ it('resets subagent checkpoint ownership at each fresh run start', () => {
59
+ const graph = makeGraph();
60
+ const internals = graph as unknown as CompiledRegistrationInternals;
61
+ const resetCheckpointThreadIds = jest.fn();
62
+ internals._subagentExecutors.add({
63
+ clearHeavyState: jest.fn(),
64
+ resetCheckpointThreadIds,
65
+ });
66
+
67
+ graph.resetValues();
68
+ graph.resetValues();
69
+
70
+ expect(resetCheckpointThreadIds).toHaveBeenCalledTimes(2);
71
+ });
72
+
29
73
  it('replaces the breaker at every run start, even when un-aborted', () => {
30
74
  const graph = makeGraph();
31
75
  const previous = graph.breakerAbort;
@@ -115,9 +159,7 @@ describe('run breaker lifecycle', () => {
115
159
  model: model as unknown as t.ChatModel,
116
160
  messages: [new HumanMessage('hi')],
117
161
  provider: Providers.OPENAI,
118
- context: graph as Parameters<
119
- typeof attemptInvoke
120
- >[0]['context'],
162
+ context: graph as Parameters<typeof attemptInvoke>[0]['context'],
121
163
  onChunk: () => {
122
164
  /* producer loop enforcement is under test */
123
165
  },
@@ -225,10 +267,7 @@ describe('run breaker lifecycle', () => {
225
267
  /** With a live breaker the entry guard must not fire; the node then
226
268
  * fails later, on the missing config, proving it got past the guard. */
227
269
  await expect(
228
- node(
229
- { messages: [] } as unknown as t.AgentSubgraphState,
230
- undefined
231
- )
270
+ node({ messages: [] } as unknown as t.AgentSubgraphState, undefined)
232
271
  ).rejects.toThrow('No config provided');
233
272
  });
234
273
  });
@@ -1338,7 +1338,7 @@ describe('context overflow recovery', () => {
1338
1338
  if (agentContext == null) {
1339
1339
  throw new Error('Expected default agent context');
1340
1340
  }
1341
- expect(run.Graph.compileOptions?.checkpointer).toBeUndefined();
1341
+ expect(run.Graph.compileOptions?.checkpointer).toBeInstanceOf(MemorySaver);
1342
1342
  expect(run.Graph.hasCompiledCheckpointer).toBe(true);
1343
1343
  run.Graph.resetValues(undefined, 'checkpoint-scope');
1344
1344
  agentContext.preserveOriginalToolContent(new Map([[2, 'full output']]));
@@ -0,0 +1,80 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import type { SubagentToolNodeResumeState } from '@/tools/subagent/SubagentReplay';
3
+ import { StandardGraph } from '../Graph';
4
+ import { Providers } from '@/common';
5
+
6
+ type ResumeToolNode = {
7
+ createSubagentResumeState(): SubagentToolNodeResumeState;
8
+ restoreSubagentResumeState(state: SubagentToolNodeResumeState): void;
9
+ };
10
+
11
+ const makeGraph = (runId: string): StandardGraph =>
12
+ new StandardGraph({
13
+ runId,
14
+ agents: [
15
+ {
16
+ agentId: 'child-agent',
17
+ provider: Providers.OPENAI,
18
+ instructions: 'Test child state restoration.',
19
+ },
20
+ ],
21
+ });
22
+
23
+ const initializeToolNode = (graph: StandardGraph): ResumeToolNode => {
24
+ const agentContext = graph.agentContexts.get('child-agent');
25
+ return graph.initializeTools({
26
+ currentTools: [],
27
+ agentContext,
28
+ }) as unknown as ResumeToolNode;
29
+ };
30
+
31
+ describe('subagent graph resume state', () => {
32
+ it('restores tool identity, turns, references, and sessions into a branch', () => {
33
+ const source = makeGraph('source');
34
+ source.toolOutputReferences = { enabled: true };
35
+ const sourceNode = initializeToolNode(source);
36
+ source.toolCallStepIds.set('call_tool', 'step_tool');
37
+ source.sessions.set('execute_code', {
38
+ session_id: 'sandbox-session',
39
+ lastUpdated: 42,
40
+ });
41
+ source
42
+ .getOrCreateToolOutputRegistry()
43
+ ?.set('source-scope', 'tool0turn0', 'raw output');
44
+ source.getEagerEventToolUsageCount('child-agent').set('calculator', 3);
45
+ source.eagerEventToolSuppressions.add('unstable_search');
46
+ const sourceNodeState = sourceNode.createSubagentResumeState();
47
+ sourceNode.restoreSubagentResumeState({
48
+ ...sourceNodeState,
49
+ toolUsageCounts: [{ toolName: 'calculator', count: 3 }],
50
+ directPathTurns: [{ toolCallId: 'call_tool', turn: 2 }],
51
+ });
52
+
53
+ const target = makeGraph('target');
54
+ target.toolOutputReferences = { enabled: true };
55
+ const targetNode = initializeToolNode(target);
56
+ target.restoreSubagentResumeState(
57
+ source.createSubagentResumeState('source-scope'),
58
+ 'target-scope'
59
+ );
60
+
61
+ expect(target.toolCallStepIds.get('call_tool')).toBe('step_tool');
62
+ expect(target.sessions.get('execute_code')).toMatchObject({
63
+ session_id: 'sandbox-session',
64
+ lastUpdated: 42,
65
+ });
66
+ expect(
67
+ target.getOrCreateToolOutputRegistry()?.get('target-scope', 'tool0turn0')
68
+ ).toBe('raw output');
69
+ expect(targetNode.createSubagentResumeState()).toMatchObject({
70
+ toolUsageCounts: [{ toolName: 'calculator', count: 3 }],
71
+ directPathTurns: [{ toolCallId: 'call_tool', turn: 2 }],
72
+ });
73
+ expect(
74
+ target.getEagerEventToolUsageCount('child-agent').get('calculator')
75
+ ).toBe(3);
76
+ expect(target.eagerEventToolSuppressions).toEqual(
77
+ new Set(['unstable_search'])
78
+ );
79
+ });
80
+ });