@librechat/agents 3.8.1 → 3.8.3
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 +2 -2
- package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
- package/dist/cjs/hitl/approvalReview.cjs +89 -0
- package/dist/cjs/hitl/approvalReview.cjs.map +1 -0
- package/dist/cjs/hooks/index.cjs +2 -0
- package/dist/cjs/hooks/index.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +1 -1
- package/dist/cjs/main.cjs +5 -3
- package/dist/cjs/messages/core.cjs +1 -1
- package/dist/cjs/messages/format.cjs +1 -1
- package/dist/cjs/messages/prune.cjs +2 -2
- package/dist/cjs/run.cjs +17 -6
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +1 -1
- package/dist/cjs/stream.cjs +2 -2
- package/dist/cjs/summarization/node.cjs +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +2 -3
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +3 -2
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +63 -11
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +8 -7
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +271 -84
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +24 -9
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +2 -0
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
- package/dist/cjs/tools/toolBatchReplay.cjs +198 -0
- package/dist/cjs/tools/toolBatchReplay.cjs.map +1 -0
- package/dist/cjs/tools/toolOutputReferences.cjs +12 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/types/hitl.cjs +4 -0
- package/dist/cjs/types/hitl.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +2 -2
- package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
- package/dist/esm/hitl/approvalReview.mjs +83 -0
- package/dist/esm/hitl/approvalReview.mjs.map +1 -0
- package/dist/esm/hooks/index.mjs +2 -1
- package/dist/esm/hooks/index.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +1 -1
- package/dist/esm/main.mjs +6 -6
- package/dist/esm/messages/core.mjs +1 -1
- package/dist/esm/messages/format.mjs +1 -1
- package/dist/esm/messages/prune.mjs +2 -2
- package/dist/esm/run.mjs +18 -7
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +1 -1
- package/dist/esm/stream.mjs +2 -2
- package/dist/esm/summarization/node.mjs +1 -1
- package/dist/esm/tools/BashExecutor.mjs +2 -3
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -2
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +63 -12
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +8 -7
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +271 -84
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +24 -9
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
- package/dist/esm/tools/toolBatchReplay.mjs +186 -0
- package/dist/esm/tools/toolBatchReplay.mjs.map +1 -0
- package/dist/esm/tools/toolOutputReferences.mjs +12 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/types/hitl.mjs +4 -1
- package/dist/esm/types/hitl.mjs.map +1 -1
- package/dist/types/hitl/approvalReview.d.ts +32 -0
- package/dist/types/hooks/index.d.ts +2 -0
- package/dist/types/hooks/types.d.ts +6 -0
- package/dist/types/tools/CodeExecutor.d.ts +2 -0
- package/dist/types/tools/ToolNode.d.ts +12 -6
- package/dist/types/tools/subagent/SubagentReplay.d.ts +2 -0
- package/dist/types/tools/toolBatchReplay.d.ts +63 -0
- package/dist/types/tools/toolOutputReferences.d.ts +2 -0
- package/package.json +1 -1
- package/src/hitl/approvalReview.ts +209 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/types.ts +6 -0
- package/src/run.ts +34 -20
- package/src/tools/BashExecutor.ts +2 -2
- package/src/tools/BashProgrammaticToolCalling.ts +5 -3
- package/src/tools/CodeExecutor.ts +111 -17
- package/src/tools/ProgrammaticToolCalling.ts +10 -8
- package/src/tools/ToolNode.ts +536 -184
- package/src/tools/subagent/SubagentExecutor.ts +35 -6
- package/src/tools/subagent/SubagentReplay.ts +2 -2
- package/src/tools/toolBatchReplay.ts +423 -0
- package/src/tools/toolOutputReferences.ts +16 -0
|
@@ -88,6 +88,11 @@ import type {
|
|
|
88
88
|
ToolApprovalReplaySnapshot,
|
|
89
89
|
} from '@/hooks';
|
|
90
90
|
import type { GraphFactory } from '@/graphs/graphFactory';
|
|
91
|
+
import {
|
|
92
|
+
rebindToolBatchReplayScope,
|
|
93
|
+
rebindToolBatchReplayPayload,
|
|
94
|
+
restoreToolReplayConfig,
|
|
95
|
+
} from '@/tools/toolBatchReplay';
|
|
91
96
|
import type { StandardGraph } from '@/graphs/Graph';
|
|
92
97
|
import {
|
|
93
98
|
getSubagentApprovalExecutionScope,
|
|
@@ -1448,7 +1453,8 @@ export class SubagentExecutor {
|
|
|
1448
1453
|
await this.prepareCheckpointFork(
|
|
1449
1454
|
resumeExecution.checkpoints,
|
|
1450
1455
|
branchChildThreadId,
|
|
1451
|
-
lease
|
|
1456
|
+
lease,
|
|
1457
|
+
{ source: resumeExecution.approvalExecutionScope, target: approvalExecutionScope }
|
|
1452
1458
|
);
|
|
1453
1459
|
let previousApprovals: ToolApprovalReplaySnapshot[] = [];
|
|
1454
1460
|
let approvalsMutated = false;
|
|
@@ -1589,7 +1595,8 @@ export class SubagentExecutor {
|
|
|
1589
1595
|
private async prepareCheckpointFork(
|
|
1590
1596
|
sources: ReadonlyArray<SubagentCheckpointReference>,
|
|
1591
1597
|
targetThreadId: string,
|
|
1592
|
-
lease: SubagentExecutionPreparationLease
|
|
1598
|
+
lease: SubagentExecutionPreparationLease,
|
|
1599
|
+
approvalScope?: { source: string; target: string }
|
|
1593
1600
|
): Promise<void> {
|
|
1594
1601
|
const checkpointer = this.checkpointer;
|
|
1595
1602
|
try {
|
|
@@ -1608,7 +1615,7 @@ export class SubagentExecutor {
|
|
|
1608
1615
|
checkpointer.deleteThread(targetThreadId)
|
|
1609
1616
|
);
|
|
1610
1617
|
}
|
|
1611
|
-
await this.forkCheckpointSnapshot(sources, targetThreadId);
|
|
1618
|
+
await this.forkCheckpointSnapshot(sources, targetThreadId, approvalScope);
|
|
1612
1619
|
lease.assertOwned();
|
|
1613
1620
|
} catch (error) {
|
|
1614
1621
|
return lease.rollback(error);
|
|
@@ -1663,7 +1670,8 @@ export class SubagentExecutor {
|
|
|
1663
1670
|
/** Copies exact checkpoint lineages, including pending task writes. */
|
|
1664
1671
|
private async forkCheckpointSnapshot(
|
|
1665
1672
|
sources: ReadonlyArray<SubagentCheckpointReference>,
|
|
1666
|
-
targetThreadId: string
|
|
1673
|
+
targetThreadId: string,
|
|
1674
|
+
approvalScope?: { source: string; target: string }
|
|
1667
1675
|
): Promise<void> {
|
|
1668
1676
|
if (
|
|
1669
1677
|
this.checkpointer == null ||
|
|
@@ -1755,7 +1763,15 @@ export class SubagentExecutor {
|
|
|
1755
1763
|
writes = [];
|
|
1756
1764
|
writesByTask.set(taskId, writes);
|
|
1757
1765
|
}
|
|
1758
|
-
|
|
1766
|
+
/** Repeated unconsumed forks must persist their current owner. */
|
|
1767
|
+
const reboundValue =
|
|
1768
|
+
channel === INTERRUPT && approvalScope != null &&
|
|
1769
|
+
value != null && typeof value === 'object' && 'value' in value
|
|
1770
|
+
? { ...value, value: rebindToolBatchReplayPayload(
|
|
1771
|
+
value.value, approvalScope.source, approvalScope.target
|
|
1772
|
+
) }
|
|
1773
|
+
: value;
|
|
1774
|
+
writes.push([channel, reboundValue]);
|
|
1759
1775
|
}
|
|
1760
1776
|
for (const [taskId, writes] of writesByTask) {
|
|
1761
1777
|
await this.checkpointer.putWrites(storedConfig, writes, taskId);
|
|
@@ -2647,7 +2663,11 @@ export class SubagentExecutor {
|
|
|
2647
2663
|
}
|
|
2648
2664
|
const persistedInterrupts = getPersistedInterrupts(persistedState);
|
|
2649
2665
|
if (persistedInterrupts.length > 0) {
|
|
2650
|
-
activeChildRun.pendingInterrupts = persistedInterrupts
|
|
2666
|
+
activeChildRun.pendingInterrupts = resumeExecution == null ? persistedInterrupts :
|
|
2667
|
+
persistedInterrupts.map((pending) => ({
|
|
2668
|
+
...pending,
|
|
2669
|
+
value: rebindToolBatchReplayPayload(pending.value, resumeExecution.approvalExecutionScope, approvalExecutionScope),
|
|
2670
|
+
}));
|
|
2651
2671
|
execution.markStarted();
|
|
2652
2672
|
childAlreadyStarted = true;
|
|
2653
2673
|
} else if (persistedState.next.length > 0) {
|
|
@@ -2682,6 +2702,15 @@ export class SubagentExecutor {
|
|
|
2682
2702
|
}
|
|
2683
2703
|
let childInput: BaseGraphState | Command | null;
|
|
2684
2704
|
if (childResumeMap != null) {
|
|
2705
|
+
const pending = activeChildRun.pendingInterrupts.find((entry) =>
|
|
2706
|
+
entry.id != null && Object.prototype.hasOwnProperty.call(childResumeMap, entry.id)
|
|
2707
|
+
) ?? activeChildRun.pendingInterrupts[0];
|
|
2708
|
+
restoreToolReplayConfig(childConfigurable, pending.id, pending.value);
|
|
2709
|
+
rebindToolBatchReplayScope(
|
|
2710
|
+
childConfigurable,
|
|
2711
|
+
resumeExecution?.approvalExecutionScope ?? approvalExecutionScope,
|
|
2712
|
+
approvalExecutionScope
|
|
2713
|
+
);
|
|
2685
2714
|
childInput = new Command({ resume: childResumeMap });
|
|
2686
2715
|
} else if (recoveredInProgress) {
|
|
2687
2716
|
childInput = null;
|
|
@@ -201,7 +201,7 @@ function isDirectPathTurnReference(
|
|
|
201
201
|
return isString(entry.toolCallId) && isNonnegativeInteger(entry.turn);
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
function isToolNodeResumeState(
|
|
204
|
+
export function isToolNodeResumeState(
|
|
205
205
|
value: unknown
|
|
206
206
|
): value is SubagentToolNodeResumeState {
|
|
207
207
|
if (value == null || typeof value !== 'object') {
|
|
@@ -266,7 +266,7 @@ function isToolOutputReferenceEntry(
|
|
|
266
266
|
return isString(entry.key) && typeof entry.value === 'string';
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
function isToolOutputReferenceState(
|
|
269
|
+
export function isToolOutputReferenceState(
|
|
270
270
|
value: unknown
|
|
271
271
|
): value is ToolOutputReferenceState {
|
|
272
272
|
if (value == null || typeof value !== 'object') {
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { Command, MemorySaver, isCommand } from '@langchain/langgraph';
|
|
2
|
+
import { isBaseMessage } from '@langchain/core/messages';
|
|
3
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
4
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
5
|
+
import type { SubagentToolNodeResumeState } from '@/tools/subagent/SubagentReplay';
|
|
6
|
+
import type { ToolOutputReferenceState } from '@/tools/toolOutputReferences';
|
|
7
|
+
import {
|
|
8
|
+
isToolNodeResumeState,
|
|
9
|
+
isToolOutputReferenceState,
|
|
10
|
+
stripSubagentResumeManifest,
|
|
11
|
+
requireValidSubagentResumeManifest,
|
|
12
|
+
} from '@/tools/subagent/SubagentReplay';
|
|
13
|
+
import { isToolApprovalInterrupt } from '@/types/hitl';
|
|
14
|
+
import { TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY } from '@/hooks/types';
|
|
15
|
+
import {
|
|
16
|
+
TOOL_APPROVAL_REVIEW_CONFIG_KEY,
|
|
17
|
+
getToolApprovalReviewEvidence,
|
|
18
|
+
createToolApprovalReviewEvidence,
|
|
19
|
+
} from '@/hitl/approvalReview';
|
|
20
|
+
import {
|
|
21
|
+
attachRunStepResumeState,
|
|
22
|
+
getRunStepResumeState,
|
|
23
|
+
stripRunStepResumeState,
|
|
24
|
+
} from '@/tools/runStepResume';
|
|
25
|
+
|
|
26
|
+
export const TOOL_BATCH_REPLAY_KEY = '__librechat_tool_batch_replay';
|
|
27
|
+
const TOOL_BATCH_PAYLOAD_KEY = '__librechat_tool_batch_payload';
|
|
28
|
+
const TOOL_BATCH_WRAPPER_KEY = '__librechat_tool_batch_wrapper';
|
|
29
|
+
|
|
30
|
+
export interface SettledToolBatchResult {
|
|
31
|
+
proposal: { name: string; args: Record<string, unknown> };
|
|
32
|
+
output: BaseMessage | Command;
|
|
33
|
+
additionalContexts: string[];
|
|
34
|
+
resolvedArgs?: Record<string, unknown>;
|
|
35
|
+
completionHandled?: boolean;
|
|
36
|
+
referenceContent?: string;
|
|
37
|
+
turn?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ToolBatchReplayRecord {
|
|
41
|
+
owner: string;
|
|
42
|
+
batch: string;
|
|
43
|
+
encoding: string;
|
|
44
|
+
data: string;
|
|
45
|
+
turnState?: SubagentToolNodeResumeState;
|
|
46
|
+
referenceState?: ToolOutputReferenceState;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ToolBatchReplayState {
|
|
50
|
+
version: 1;
|
|
51
|
+
approvalOwner?: string;
|
|
52
|
+
/** Populated from the pending interrupt by Run, including question pauses. */
|
|
53
|
+
interruptId?: string;
|
|
54
|
+
records: ToolBatchReplayRecord[];
|
|
55
|
+
wrappedPayload?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Use the same message/Command codec as LangGraph checkpoints. */
|
|
59
|
+
const serializer = new MemorySaver().serde;
|
|
60
|
+
|
|
61
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
62
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The only host-to-runtime entry point for checkpoint-owned tool replay state. */
|
|
66
|
+
export function restoreToolReplayConfig(
|
|
67
|
+
configurable: Record<string, unknown>,
|
|
68
|
+
interruptId: string | undefined,
|
|
69
|
+
payload: unknown
|
|
70
|
+
): void {
|
|
71
|
+
delete configurable[TOOL_BATCH_REPLAY_KEY];
|
|
72
|
+
delete configurable[TOOL_APPROVAL_REVIEW_CONFIG_KEY];
|
|
73
|
+
const state = getToolBatchReplayState(payload);
|
|
74
|
+
const publicPayload = getPublicToolInterruptPayload(payload);
|
|
75
|
+
const review = createToolApprovalReviewEvidence(
|
|
76
|
+
interruptId,
|
|
77
|
+
publicPayload,
|
|
78
|
+
state?.approvalOwner
|
|
79
|
+
);
|
|
80
|
+
if (isToolApprovalInterrupt(publicPayload) &&
|
|
81
|
+
(review == null || (state != null && state.approvalOwner == null))) {
|
|
82
|
+
throw new Error('Invalid tool approval checkpoint');
|
|
83
|
+
}
|
|
84
|
+
if (state != null) {
|
|
85
|
+
configurable[TOOL_BATCH_REPLAY_KEY] = { ...state, interruptId };
|
|
86
|
+
}
|
|
87
|
+
if (review != null) {
|
|
88
|
+
configurable[TOOL_APPROVAL_REVIEW_CONFIG_KEY] = review;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resume maps identify the checkpointed interrupt, while the task scratchpad
|
|
94
|
+
* identifies the node that can consume it. Config survives into later graph
|
|
95
|
+
* steps, so the presence of replay evidence alone does not imply a resume.
|
|
96
|
+
* A parent replaying a checkpointed child has no local resume value.
|
|
97
|
+
*/
|
|
98
|
+
export function isToolReplayResume(
|
|
99
|
+
config: RunnableConfig,
|
|
100
|
+
interruptId: string | undefined,
|
|
101
|
+
isChildReplay: boolean
|
|
102
|
+
): boolean {
|
|
103
|
+
const resumeMap: object | undefined = config.configurable?.__pregel_resume_map;
|
|
104
|
+
if (interruptId != null && resumeMap != null &&
|
|
105
|
+
!Object.prototype.hasOwnProperty.call(resumeMap, interruptId)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (isChildReplay && resumeMap != null) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
const scratchpad: { resume?: { length: number }; nullResume?: unknown } | undefined =
|
|
112
|
+
config.configurable?.__pregel_scratchpad;
|
|
113
|
+
return scratchpad == null ||
|
|
114
|
+
(scratchpad.resume?.length ?? 0) > 0 || scratchpad.nullResume !== undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function getToolBatchReplayOwner(
|
|
118
|
+
config: RunnableConfig,
|
|
119
|
+
agentId: string
|
|
120
|
+
): string {
|
|
121
|
+
return JSON.stringify([
|
|
122
|
+
getToolBatchReplayScope(config),
|
|
123
|
+
config.configurable?.[TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY] == null
|
|
124
|
+
? (config.configurable?.checkpoint_ns ?? '')
|
|
125
|
+
: '',
|
|
126
|
+
agentId,
|
|
127
|
+
]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getToolBatchReplayScope(
|
|
131
|
+
config: RunnableConfig
|
|
132
|
+
): string | undefined {
|
|
133
|
+
const scope =
|
|
134
|
+
config.configurable?.[TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY] ??
|
|
135
|
+
config.configurable?.thread_id;
|
|
136
|
+
return typeof scope === 'string' && scope.length > 0 ? scope : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Foreign replay evidence may only transit a checkpoint-proven child path. */
|
|
140
|
+
export function isChildToolReplayOwner(
|
|
141
|
+
config: RunnableConfig,
|
|
142
|
+
owner: string,
|
|
143
|
+
trustedParentCallIds: ReadonlySet<string>
|
|
144
|
+
): boolean {
|
|
145
|
+
const manifest = requireValidSubagentResumeManifest(config.configurable);
|
|
146
|
+
if (manifest == null || trustedParentCallIds.size === 0) return false;
|
|
147
|
+
const identity: unknown = JSON.parse(owner);
|
|
148
|
+
if (!Array.isArray(identity) || identity.length !== 3 || identity[1] !== '') return false;
|
|
149
|
+
const pending = manifest.executions.filter((execution) => trustedParentCallIds.has(execution.parentToolCallId));
|
|
150
|
+
while (pending.length > 0) {
|
|
151
|
+
const execution = pending.pop()!;
|
|
152
|
+
if (execution.approvalExecutionScope === identity[0]) return true;
|
|
153
|
+
pending.push(...(execution.descendant?.executions ?? []));
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Rebind only the checkpoint-proven source execution when a child fork is created. */
|
|
159
|
+
export function rebindToolBatchReplayScope(
|
|
160
|
+
configurable: Record<string, unknown>,
|
|
161
|
+
sourceScope: string,
|
|
162
|
+
destinationScope: string
|
|
163
|
+
): void {
|
|
164
|
+
const rebind = (key: string): string => {
|
|
165
|
+
const parts: unknown = JSON.parse(key);
|
|
166
|
+
if (!Array.isArray(parts) || parts[0] !== sourceScope) {
|
|
167
|
+
return key;
|
|
168
|
+
}
|
|
169
|
+
return JSON.stringify([destinationScope, ...parts.slice(1)]);
|
|
170
|
+
};
|
|
171
|
+
const config = { configurable };
|
|
172
|
+
const review = getToolApprovalReviewEvidence(config);
|
|
173
|
+
if (review?.owner != null) {
|
|
174
|
+
configurable[TOOL_APPROVAL_REVIEW_CONFIG_KEY] = {
|
|
175
|
+
...review,
|
|
176
|
+
owner: rebind(review.owner),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const state = getToolBatchReplayState(configurable);
|
|
180
|
+
if (state != null) {
|
|
181
|
+
configurable[TOOL_BATCH_REPLAY_KEY] = {
|
|
182
|
+
...state,
|
|
183
|
+
approvalOwner:
|
|
184
|
+
state.approvalOwner == null ? undefined : rebind(state.approvalOwner),
|
|
185
|
+
records: state.records.map((record) => ({
|
|
186
|
+
...record,
|
|
187
|
+
owner: rebind(record.owner),
|
|
188
|
+
batch: rebind(record.batch),
|
|
189
|
+
})),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** A fork may be paused again before it consumes its pending interrupt. */
|
|
195
|
+
export function rebindToolBatchReplayPayload(
|
|
196
|
+
payload: unknown,
|
|
197
|
+
sourceScope: string,
|
|
198
|
+
destinationScope: string
|
|
199
|
+
): unknown {
|
|
200
|
+
const state = getToolBatchReplayState(payload);
|
|
201
|
+
const value = stripRunStepResumeState(payload);
|
|
202
|
+
if (state == null || !isRecord(value)) {
|
|
203
|
+
return payload;
|
|
204
|
+
}
|
|
205
|
+
const configurable = { [TOOL_BATCH_REPLAY_KEY]: state };
|
|
206
|
+
rebindToolBatchReplayScope(configurable, sourceScope, destinationScope);
|
|
207
|
+
const rebound = {
|
|
208
|
+
...value,
|
|
209
|
+
[TOOL_BATCH_REPLAY_KEY]: configurable[TOOL_BATCH_REPLAY_KEY],
|
|
210
|
+
};
|
|
211
|
+
const runStepState = getRunStepResumeState(payload);
|
|
212
|
+
return runStepState == null
|
|
213
|
+
? rebound
|
|
214
|
+
: attachRunStepResumeState(rebound, runStepState);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function getToolBatchReplayState(
|
|
218
|
+
payload: unknown
|
|
219
|
+
): ToolBatchReplayState | undefined {
|
|
220
|
+
const value = stripRunStepResumeState(payload);
|
|
221
|
+
if (
|
|
222
|
+
value == null ||
|
|
223
|
+
typeof value !== 'object' ||
|
|
224
|
+
!(TOOL_BATCH_REPLAY_KEY in value)
|
|
225
|
+
) {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
const candidate: unknown = value[TOOL_BATCH_REPLAY_KEY];
|
|
229
|
+
if (candidate === undefined) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
const state = candidate as Partial<ToolBatchReplayState> | null;
|
|
233
|
+
if (
|
|
234
|
+
state?.version !== 1 ||
|
|
235
|
+
(state.wrappedPayload != null &&
|
|
236
|
+
typeof state.wrappedPayload !== 'boolean') ||
|
|
237
|
+
(state.approvalOwner != null && typeof state.approvalOwner !== 'string') ||
|
|
238
|
+
(state.interruptId != null && typeof state.interruptId !== 'string') ||
|
|
239
|
+
!Array.isArray(state.records) ||
|
|
240
|
+
state.records.some((value: unknown) => {
|
|
241
|
+
if (value == null || typeof value !== 'object') {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
const record = value as Partial<ToolBatchReplayRecord>;
|
|
245
|
+
return (
|
|
246
|
+
typeof record.owner !== 'string' ||
|
|
247
|
+
typeof record.batch !== 'string' ||
|
|
248
|
+
typeof record.encoding !== 'string' ||
|
|
249
|
+
typeof record.data !== 'string' ||
|
|
250
|
+
(record.turnState != null &&
|
|
251
|
+
!isToolNodeResumeState(record.turnState)) ||
|
|
252
|
+
(record.referenceState != null &&
|
|
253
|
+
!isToolOutputReferenceState(record.referenceState))
|
|
254
|
+
);
|
|
255
|
+
})
|
|
256
|
+
) {
|
|
257
|
+
throw new Error('Invalid tool batch replay checkpoint');
|
|
258
|
+
}
|
|
259
|
+
return structuredClone(state as ToolBatchReplayState);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function stripToolBatchReplayState(payload: unknown): unknown {
|
|
263
|
+
if (
|
|
264
|
+
payload == null ||
|
|
265
|
+
typeof payload !== 'object' ||
|
|
266
|
+
!(TOOL_BATCH_REPLAY_KEY in payload)
|
|
267
|
+
) {
|
|
268
|
+
return payload;
|
|
269
|
+
}
|
|
270
|
+
const state = getToolBatchReplayState(payload);
|
|
271
|
+
const { [TOOL_BATCH_REPLAY_KEY]: _state, ...publicPayload } = payload;
|
|
272
|
+
if (
|
|
273
|
+
state?.wrappedPayload === true &&
|
|
274
|
+
TOOL_BATCH_PAYLOAD_KEY in publicPayload
|
|
275
|
+
) {
|
|
276
|
+
return publicPayload[TOOL_BATCH_PAYLOAD_KEY];
|
|
277
|
+
}
|
|
278
|
+
return publicPayload;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Decode outer execution metadata before shape-sensitive child wrappers. */
|
|
282
|
+
export function getPublicToolInterruptPayload(payload: unknown): unknown {
|
|
283
|
+
return stripSubagentResumeManifest(
|
|
284
|
+
stripToolBatchReplayState(stripRunStepResumeState(payload))
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function attachToolBatchReplayState(
|
|
289
|
+
payload: unknown,
|
|
290
|
+
owner: string,
|
|
291
|
+
batches: ReadonlyMap<string, ReadonlyMap<string, object>>,
|
|
292
|
+
turnState?: SubagentToolNodeResumeState,
|
|
293
|
+
referenceState?: ToolOutputReferenceState
|
|
294
|
+
): Promise<unknown> {
|
|
295
|
+
const publicPayload = stripRunStepResumeState(payload);
|
|
296
|
+
const previous = getToolBatchReplayState(publicPayload);
|
|
297
|
+
const records =
|
|
298
|
+
previous?.records.filter((record) => record.owner !== owner) ?? [];
|
|
299
|
+
for (const [batch, results] of batches) {
|
|
300
|
+
const [encoding, bytes] = await serializer.dumpsTyped([...results]);
|
|
301
|
+
records.push({
|
|
302
|
+
owner,
|
|
303
|
+
batch,
|
|
304
|
+
encoding,
|
|
305
|
+
data: Buffer.from(bytes).toString('base64'),
|
|
306
|
+
...(turnState == null ? {} : { turnState }),
|
|
307
|
+
...(referenceState == null ? {} : { referenceState }),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const approvalOwner =
|
|
311
|
+
previous?.approvalOwner ??
|
|
312
|
+
(isToolApprovalInterrupt(publicPayload) ? owner : undefined);
|
|
313
|
+
if (records.length === 0 && approvalOwner == null) {
|
|
314
|
+
return payload;
|
|
315
|
+
}
|
|
316
|
+
const result = {
|
|
317
|
+
...(isRecord(publicPayload)
|
|
318
|
+
? publicPayload
|
|
319
|
+
: {
|
|
320
|
+
[TOOL_BATCH_WRAPPER_KEY]: 1,
|
|
321
|
+
[TOOL_BATCH_PAYLOAD_KEY]: publicPayload,
|
|
322
|
+
}),
|
|
323
|
+
[TOOL_BATCH_REPLAY_KEY]: {
|
|
324
|
+
version: 1,
|
|
325
|
+
approvalOwner,
|
|
326
|
+
records,
|
|
327
|
+
wrappedPayload: previous?.wrappedPayload ?? !isRecord(publicPayload),
|
|
328
|
+
} satisfies ToolBatchReplayState,
|
|
329
|
+
};
|
|
330
|
+
const runStepState = getRunStepResumeState(payload);
|
|
331
|
+
return runStepState == null
|
|
332
|
+
? result
|
|
333
|
+
: attachRunStepResumeState(result, runStepState);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export async function restoreToolBatchReplayState(
|
|
337
|
+
config: RunnableConfig,
|
|
338
|
+
owner: string
|
|
339
|
+
): Promise<
|
|
340
|
+
Array<{
|
|
341
|
+
batch: string;
|
|
342
|
+
results: Array<[string, SettledToolBatchResult]>;
|
|
343
|
+
turnState?: SubagentToolNodeResumeState;
|
|
344
|
+
referenceState?: ToolOutputReferenceState;
|
|
345
|
+
}>
|
|
346
|
+
> {
|
|
347
|
+
const state = getToolBatchReplayState(config.configurable);
|
|
348
|
+
if (state == null) {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
return Promise.all(
|
|
352
|
+
state.records
|
|
353
|
+
.filter((record) => record.owner === owner)
|
|
354
|
+
.map(async (record) => {
|
|
355
|
+
const values: unknown = await serializer.loadsTyped(
|
|
356
|
+
record.encoding,
|
|
357
|
+
Buffer.from(record.data, 'base64')
|
|
358
|
+
);
|
|
359
|
+
if (!Array.isArray(values)) {
|
|
360
|
+
throw new Error('Invalid settled tool batch results');
|
|
361
|
+
}
|
|
362
|
+
const seen = new Set<string>();
|
|
363
|
+
const results = values.map(
|
|
364
|
+
(entry: unknown): [string, SettledToolBatchResult] => {
|
|
365
|
+
if (
|
|
366
|
+
!Array.isArray(entry) ||
|
|
367
|
+
entry.length !== 2 ||
|
|
368
|
+
typeof entry[0] !== 'string' ||
|
|
369
|
+
seen.has(entry[0])
|
|
370
|
+
) {
|
|
371
|
+
throw new Error('Invalid settled tool batch results');
|
|
372
|
+
}
|
|
373
|
+
const result = entry[1] as Partial<SettledToolBatchResult> | null;
|
|
374
|
+
if (
|
|
375
|
+
result == null ||
|
|
376
|
+
(!isBaseMessage(result.output) && !isCommand(result.output)) ||
|
|
377
|
+
!Array.isArray(result.additionalContexts) ||
|
|
378
|
+
result.additionalContexts.some(
|
|
379
|
+
(context) => typeof context !== 'string'
|
|
380
|
+
) ||
|
|
381
|
+
(result.completionHandled != null &&
|
|
382
|
+
typeof result.completionHandled !== 'boolean') ||
|
|
383
|
+
(result.referenceContent != null &&
|
|
384
|
+
typeof result.referenceContent !== 'string') ||
|
|
385
|
+
(result.turn != null &&
|
|
386
|
+
(!Number.isSafeInteger(result.turn) || result.turn < 0)) ||
|
|
387
|
+
!isRecord(result.proposal) ||
|
|
388
|
+
typeof result.proposal.name !== 'string' ||
|
|
389
|
+
result.proposal.name.length === 0 ||
|
|
390
|
+
!isRecord(result.proposal.args) ||
|
|
391
|
+
(result.resolvedArgs != null && !isRecord(result.resolvedArgs))
|
|
392
|
+
) {
|
|
393
|
+
throw new Error('Invalid settled tool batch results');
|
|
394
|
+
}
|
|
395
|
+
seen.add(entry[0]);
|
|
396
|
+
const output = isCommand(result.output)
|
|
397
|
+
? new Command({
|
|
398
|
+
update: result.output.update,
|
|
399
|
+
goto: result.output.goto,
|
|
400
|
+
graph: result.output.graph,
|
|
401
|
+
resume: result.output.resume,
|
|
402
|
+
})
|
|
403
|
+
: result.output;
|
|
404
|
+
return [
|
|
405
|
+
entry[0],
|
|
406
|
+
{
|
|
407
|
+
...result,
|
|
408
|
+
proposal: result.proposal,
|
|
409
|
+
output,
|
|
410
|
+
additionalContexts: result.additionalContexts,
|
|
411
|
+
},
|
|
412
|
+
];
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
return {
|
|
416
|
+
batch: record.batch,
|
|
417
|
+
results,
|
|
418
|
+
turnState: record.turnState,
|
|
419
|
+
referenceState: record.referenceState,
|
|
420
|
+
};
|
|
421
|
+
})
|
|
422
|
+
);
|
|
423
|
+
}
|
|
@@ -420,6 +420,22 @@ export class ToolOutputReferenceRegistry {
|
|
|
420
420
|
};
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
/** Restore batch inputs without rolling back graph-owned concurrent outputs. */
|
|
424
|
+
resumeBatch(runId: string, state: ToolOutputReferenceState): ToolOutputResolveView {
|
|
425
|
+
if (!this.runStates.has(runId)) {
|
|
426
|
+
this.restoreState(runId, state);
|
|
427
|
+
}
|
|
428
|
+
const bucket = this.getOrCreate(runId);
|
|
429
|
+
bucket.turnCounter = Math.max(bucket.turnCounter, state.turnCounter + 1);
|
|
430
|
+
const inputs = new ToolOutputReferenceRegistry({
|
|
431
|
+
maxOutputSize: this.maxOutputSize,
|
|
432
|
+
maxTotalSize: this.maxTotalSize,
|
|
433
|
+
maxActiveRuns: 1,
|
|
434
|
+
});
|
|
435
|
+
inputs.restoreState(runId, state);
|
|
436
|
+
return inputs.snapshot(runId);
|
|
437
|
+
}
|
|
438
|
+
|
|
423
439
|
/** Restores a checkpointed run bucket under the current resume scope. */
|
|
424
440
|
restoreState(
|
|
425
441
|
runId: string | undefined,
|