@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.
- package/dist/cjs/graphs/Graph.cjs +111 -27
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +83 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/executeHooks.cjs +60 -14
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs +2 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +3 -3
- package/dist/cjs/llm/invoke.cjs +2 -2
- package/dist/cjs/main.cjs +14 -13
- package/dist/cjs/messages/format.cjs +40 -23
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +82 -15
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +37 -2
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/stream.cjs +1 -1
- package/dist/cjs/summarization/node.cjs +6 -3
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
- package/dist/cjs/tools/ToolNode.cjs +316 -94
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +630 -78
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +175 -0
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
- package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +2 -2
- package/dist/esm/graphs/Graph.mjs +111 -27
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +83 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/executeHooks.mjs +60 -14
- package/dist/esm/hooks/executeHooks.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs +2 -1
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +2 -2
- package/dist/esm/main.mjs +11 -11
- package/dist/esm/messages/format.mjs +40 -23
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +83 -16
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +37 -2
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/stream.mjs +1 -1
- package/dist/esm/summarization/node.mjs +6 -3
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
- package/dist/esm/tools/ToolNode.mjs +317 -95
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +631 -79
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +168 -0
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
- package/dist/esm/tools/toolOutputReferences.mjs +20 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +2 -2
- package/dist/types/graphs/Graph.d.ts +16 -1
- package/dist/types/hooks/HookRegistry.d.ts +10 -1
- package/dist/types/hooks/executeHooks.d.ts +5 -1
- package/dist/types/hooks/index.d.ts +2 -2
- package/dist/types/hooks/types.d.ts +12 -0
- package/dist/types/run.d.ts +4 -1
- package/dist/types/session/AgentSession.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +23 -15
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +46 -7
- package/dist/types/tools/subagent/SubagentReplay.d.ts +81 -0
- package/dist/types/tools/toolOutputReferences.d.ts +12 -0
- package/dist/types/types/graph.d.ts +5 -5
- package/dist/types/types/hitl.d.ts +15 -0
- package/package.json +6 -2
- package/src/graphs/Graph.ts +246 -52
- package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
- package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
- package/src/hooks/HookRegistry.ts +151 -1
- package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
- package/src/hooks/__tests__/executeHooks.test.ts +160 -0
- package/src/hooks/executeHooks.ts +133 -15
- package/src/hooks/index.ts +3 -1
- package/src/hooks/types.ts +16 -0
- package/src/messages/format.ts +54 -26
- package/src/messages/formatAgentMessages.reducer.test.ts +162 -0
- package/src/messages/formatAgentMessages.steer.test.ts +16 -11
- package/src/messages/formatAgentMessages.test.ts +7 -2
- package/src/run.ts +164 -18
- package/src/session/AgentSession.ts +52 -2
- package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
- package/src/summarization/__tests__/node.test.ts +32 -14
- package/src/summarization/node.ts +15 -11
- package/src/tools/ToolNode.ts +631 -157
- package/src/tools/__tests__/SubagentExecutor.test.ts +542 -6
- package/src/tools/__tests__/SubagentReplay.test.ts +300 -0
- package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +486 -2
- package/src/tools/__tests__/hitl.test.ts +234 -5
- package/src/tools/__tests__/subagentHooks.test.ts +1040 -62
- package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
- package/src/tools/subagent/SubagentExecutor.ts +1369 -131
- package/src/tools/subagent/SubagentReplay.ts +575 -0
- package/src/tools/toolOutputReferences.ts +40 -1
- package/src/types/graph.ts +5 -5
- package/src/types/hitl.ts +16 -0
package/src/graphs/Graph.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
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
|
-
*
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
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).
|
|
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
|
+
});
|