@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
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
2
|
+
import type {
|
|
3
|
+
ToolApprovalInterruptPayload,
|
|
4
|
+
ToolApprovalRequest,
|
|
5
|
+
ToolApprovalReviewConfig,
|
|
6
|
+
} from '@/types/hitl';
|
|
7
|
+
import { stableStringify } from '@/tools/eagerEventExecution';
|
|
8
|
+
import { isToolApprovalInterrupt } from '@/types/hitl';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Private resume-only config entry populated from the checkpointed interrupt.
|
|
12
|
+
* Hosts must never need to construct or inspect this value.
|
|
13
|
+
*/
|
|
14
|
+
export const TOOL_APPROVAL_REVIEW_CONFIG_KEY =
|
|
15
|
+
'__librechat_tool_approval_review';
|
|
16
|
+
|
|
17
|
+
export interface ToolApprovalReviewEvidence {
|
|
18
|
+
interruptId: string;
|
|
19
|
+
payload: ToolApprovalInterruptPayload;
|
|
20
|
+
owner?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ReviewedToolApproval {
|
|
24
|
+
request: ToolApprovalRequest;
|
|
25
|
+
reviewConfig: ToolApprovalReviewConfig;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const APPROVAL_DECISIONS = new Set(['approve', 'reject', 'edit', 'respond']);
|
|
29
|
+
|
|
30
|
+
/** Detach approval payloads from host- or transport-owned object graphs. */
|
|
31
|
+
export function cloneToolApprovalInterruptPayload<T>(payload: T): T {
|
|
32
|
+
return isToolApprovalInterrupt(payload) ? structuredClone(payload) : payload;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
36
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hasValidApprovalShape(payload: ToolApprovalInterruptPayload): boolean {
|
|
40
|
+
if (
|
|
41
|
+
!Array.isArray(payload.action_requests) ||
|
|
42
|
+
!Array.isArray(payload.review_configs) ||
|
|
43
|
+
payload.action_requests.length !== payload.review_configs.length
|
|
44
|
+
) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const toolCallIds = new Set<string>();
|
|
49
|
+
return payload.action_requests.every((request, index) => {
|
|
50
|
+
const reviewConfig = payload.review_configs[index];
|
|
51
|
+
if (!isRecord(request) || !isRecord(reviewConfig)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const toolCallId = request.tool_call_id;
|
|
55
|
+
const toolName = request.name;
|
|
56
|
+
const allowedDecisions = reviewConfig.allowed_decisions;
|
|
57
|
+
if (
|
|
58
|
+
typeof toolCallId !== 'string' ||
|
|
59
|
+
toolCallId.length === 0 ||
|
|
60
|
+
toolCallIds.has(toolCallId) ||
|
|
61
|
+
typeof toolName !== 'string' ||
|
|
62
|
+
toolName.length === 0 ||
|
|
63
|
+
!isRecord(request.arguments) ||
|
|
64
|
+
(request.description != null &&
|
|
65
|
+
typeof request.description !== 'string') ||
|
|
66
|
+
reviewConfig.tool_call_id !== toolCallId ||
|
|
67
|
+
reviewConfig.action_name !== toolName ||
|
|
68
|
+
!Array.isArray(allowedDecisions) ||
|
|
69
|
+
!allowedDecisions.every(
|
|
70
|
+
(decision) =>
|
|
71
|
+
typeof decision === 'string' && APPROVAL_DECISIONS.has(decision)
|
|
72
|
+
)
|
|
73
|
+
) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
toolCallIds.add(toolCallId);
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Build trusted review evidence from the interrupt restored by `Run`. */
|
|
82
|
+
export function createToolApprovalReviewEvidence(
|
|
83
|
+
interruptId: string | undefined,
|
|
84
|
+
payload: unknown,
|
|
85
|
+
owner?: string
|
|
86
|
+
): ToolApprovalReviewEvidence | undefined {
|
|
87
|
+
if (
|
|
88
|
+
typeof interruptId !== 'string' ||
|
|
89
|
+
interruptId.length === 0 ||
|
|
90
|
+
!isToolApprovalInterrupt(payload) ||
|
|
91
|
+
!hasValidApprovalShape(payload)
|
|
92
|
+
) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
interruptId,
|
|
97
|
+
payload: cloneToolApprovalInterruptPayload(payload),
|
|
98
|
+
...(owner == null ? {} : { owner }),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Read only well-shaped evidence from a ToolNode's runnable config. */
|
|
103
|
+
export function getToolApprovalReviewEvidence(
|
|
104
|
+
config: RunnableConfig,
|
|
105
|
+
owner?: string
|
|
106
|
+
): ToolApprovalReviewEvidence | undefined {
|
|
107
|
+
const candidate = config.configurable?.[TOOL_APPROVAL_REVIEW_CONFIG_KEY];
|
|
108
|
+
if (candidate == null || typeof candidate !== 'object') {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const {
|
|
112
|
+
interruptId,
|
|
113
|
+
payload,
|
|
114
|
+
owner: evidenceOwner,
|
|
115
|
+
} = candidate as {
|
|
116
|
+
interruptId?: unknown;
|
|
117
|
+
payload?: unknown;
|
|
118
|
+
owner?: unknown;
|
|
119
|
+
};
|
|
120
|
+
if (
|
|
121
|
+
evidenceOwner != null &&
|
|
122
|
+
(typeof evidenceOwner !== 'string' ||
|
|
123
|
+
(owner != null && owner !== evidenceOwner))
|
|
124
|
+
) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
return createToolApprovalReviewEvidence(
|
|
128
|
+
typeof interruptId === 'string' ? interruptId : undefined,
|
|
129
|
+
payload,
|
|
130
|
+
typeof evidenceOwner === 'string' ? evidenceOwner : undefined
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getReviewedToolApproval(
|
|
135
|
+
payload: ToolApprovalInterruptPayload | undefined,
|
|
136
|
+
toolCallId: string | undefined
|
|
137
|
+
): ReviewedToolApproval | undefined {
|
|
138
|
+
if (payload == null || toolCallId == null || toolCallId === '') {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
const request = payload.action_requests.find(
|
|
142
|
+
(candidate) => candidate.tool_call_id === toolCallId
|
|
143
|
+
);
|
|
144
|
+
const reviewConfig = payload.review_configs.find(
|
|
145
|
+
(candidate) => candidate.tool_call_id === toolCallId
|
|
146
|
+
);
|
|
147
|
+
if (request == null || reviewConfig == null) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
return { request, reviewConfig };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function decisionsEqual(
|
|
154
|
+
left: ReadonlyArray<string>,
|
|
155
|
+
right: ReadonlyArray<string>
|
|
156
|
+
): boolean {
|
|
157
|
+
if (left.length !== right.length) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
const sortedLeft = [...left].sort();
|
|
161
|
+
const sortedRight = [...right].sort();
|
|
162
|
+
return sortedLeft.every((value, index) => value === sortedRight[index]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Bind a resume decision to the exact proposal the reviewer saw. Description
|
|
167
|
+
* text is deliberately excluded: it explains policy but cannot change the
|
|
168
|
+
* side effect. Tool identity, normalized arguments and available decisions
|
|
169
|
+
* are execution-authoritative.
|
|
170
|
+
*/
|
|
171
|
+
export function toolApprovalProposalMatches(
|
|
172
|
+
current: ReviewedToolApproval,
|
|
173
|
+
reviewed: ReviewedToolApproval
|
|
174
|
+
): boolean {
|
|
175
|
+
return (
|
|
176
|
+
current.request.tool_call_id === reviewed.request.tool_call_id &&
|
|
177
|
+
current.request.name === reviewed.request.name &&
|
|
178
|
+
current.reviewConfig.tool_call_id === reviewed.reviewConfig.tool_call_id &&
|
|
179
|
+
current.reviewConfig.action_name === reviewed.reviewConfig.action_name &&
|
|
180
|
+
stableStringify(current.request.arguments) ===
|
|
181
|
+
stableStringify(reviewed.request.arguments) &&
|
|
182
|
+
decisionsEqual(
|
|
183
|
+
current.reviewConfig.allowed_decisions,
|
|
184
|
+
reviewed.reviewConfig.allowed_decisions
|
|
185
|
+
)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Preserve batch order as part of the decision-to-request binding. */
|
|
190
|
+
export function toolApprovalPayloadMatches(
|
|
191
|
+
current: ToolApprovalInterruptPayload,
|
|
192
|
+
reviewed: ToolApprovalInterruptPayload
|
|
193
|
+
): boolean {
|
|
194
|
+
if (
|
|
195
|
+
current.action_requests.length !== reviewed.action_requests.length ||
|
|
196
|
+
current.review_configs.length !== reviewed.review_configs.length
|
|
197
|
+
) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return current.action_requests.every((request, index) => {
|
|
201
|
+
const currentReviewConfig = current.review_configs[index];
|
|
202
|
+
const reviewedRequest = reviewed.action_requests[index];
|
|
203
|
+
const reviewedReviewConfig = reviewed.review_configs[index];
|
|
204
|
+
return toolApprovalProposalMatches(
|
|
205
|
+
{ request, reviewConfig: currentReviewConfig },
|
|
206
|
+
{ request: reviewedRequest, reviewConfig: reviewedReviewConfig }
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -116,3 +116,6 @@ export type {
|
|
|
116
116
|
PostCompactHookOutput,
|
|
117
117
|
} from './types';
|
|
118
118
|
export type { ExecuteHooksOptions } from './executeHooks';
|
|
119
|
+
|
|
120
|
+
/** Hosts may opt into stable generation scopes across every ToolNode interrupt type. */
|
|
121
|
+
export const TOOL_APPROVAL_EXECUTION_SCOPE_CAPABLE = true;
|
package/src/hooks/types.ts
CHANGED
|
@@ -28,6 +28,12 @@ export const HOOK_EVENTS = [
|
|
|
28
28
|
'PostCompact',
|
|
29
29
|
] as const;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Host-owned generation identity, stable across every resume and rebuilt Run.
|
|
33
|
+
* A new generation must use a new scope, even when it reuses a thread or response
|
|
34
|
+
* id. Explicit scoping keeps LangGraph task namespaces out of approval owners;
|
|
35
|
+
* the executing agent id remains part of the owner and cannot change on resume.
|
|
36
|
+
*/
|
|
31
37
|
export const TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY =
|
|
32
38
|
'__librechat_tool_approval_execution_scope';
|
|
33
39
|
|
package/src/run.ts
CHANGED
|
@@ -22,7 +22,6 @@ import type {
|
|
|
22
22
|
} from '@langchain/core/messages';
|
|
23
23
|
import type { StringPromptValue } from '@langchain/core/prompt_values';
|
|
24
24
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
25
|
-
import { isFadingTier } from '@/messages/fading';
|
|
26
25
|
import type { AggregatedHookResult, HookRegistry } from '@/hooks';
|
|
27
26
|
import type { MultiAgentGraph } from '@/graphs/MultiAgentGraph';
|
|
28
27
|
import type { StandardGraph } from '@/graphs/Graph';
|
|
@@ -40,7 +39,6 @@ import {
|
|
|
40
39
|
} from '@/common';
|
|
41
40
|
import {
|
|
42
41
|
requireValidSubagentResumeManifest,
|
|
43
|
-
stripSubagentResumeManifest,
|
|
44
42
|
SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY,
|
|
45
43
|
SUBAGENT_RESUME_MANIFEST_CONFIG_KEY,
|
|
46
44
|
} from '@/tools/subagent/SubagentReplay';
|
|
@@ -70,6 +68,16 @@ import {
|
|
|
70
68
|
resolveLangfuseConfig,
|
|
71
69
|
resolveToolOutputTracingConfig,
|
|
72
70
|
} from '@/langfuseConfig';
|
|
71
|
+
import {
|
|
72
|
+
cloneToolApprovalInterruptPayload,
|
|
73
|
+
TOOL_APPROVAL_REVIEW_CONFIG_KEY,
|
|
74
|
+
} from '@/hitl/approvalReview';
|
|
75
|
+
import {
|
|
76
|
+
TOOL_BATCH_REPLAY_KEY,
|
|
77
|
+
restoreToolReplayConfig,
|
|
78
|
+
stripToolBatchReplayState,
|
|
79
|
+
getPublicToolInterruptPayload,
|
|
80
|
+
} from '@/tools/toolBatchReplay';
|
|
73
81
|
import {
|
|
74
82
|
resolveLangfuseDestinationKey,
|
|
75
83
|
resolveLangfuseTraceAnchorParent,
|
|
@@ -104,6 +112,7 @@ import { seedRunInitialSessions } from '@/utils/toolSessions';
|
|
|
104
112
|
import { getTraceIdSeed } from '@/langfuseRuntimeContext';
|
|
105
113
|
import { resolveClientOptionsModel } from '@/llm/request';
|
|
106
114
|
import { createGraph } from '@/graphs/createGraph';
|
|
115
|
+
import { isFadingTier } from '@/messages/fading';
|
|
107
116
|
import { resolveMaxSeals } from '@/llm/preempt';
|
|
108
117
|
import { isBuiltRuntime } from '@/lazyRequire';
|
|
109
118
|
import { initializeModel } from '@/llm/init';
|
|
@@ -169,9 +178,7 @@ function materializeStopContinuation(
|
|
|
169
178
|
return messages;
|
|
170
179
|
}
|
|
171
180
|
|
|
172
|
-
function advanceCheckpointCursor(
|
|
173
|
-
config: t.RunStreamConfig
|
|
174
|
-
): t.RunStreamConfig {
|
|
181
|
+
function advanceCheckpointCursor(config: t.RunStreamConfig): t.RunStreamConfig {
|
|
175
182
|
const configurable = { ...config.configurable };
|
|
176
183
|
delete configurable.checkpoint_id;
|
|
177
184
|
delete configurable.checkpoint_map;
|
|
@@ -186,9 +193,7 @@ function assertFinalAdmissionSucceeded(result: AggregatedHookResult): void {
|
|
|
186
193
|
result.errors.length > 0
|
|
187
194
|
? result.errors.join('; ')
|
|
188
195
|
: 'one or more internal finalizers failed';
|
|
189
|
-
throw new Error(
|
|
190
|
-
`StopFinalize terminal admission failed: ${detail}`
|
|
191
|
-
);
|
|
196
|
+
throw new Error(`StopFinalize terminal admission failed: ${detail}`);
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
const CUSTOM_GRAPH_EVENTS = new Set<string>([
|
|
@@ -268,9 +273,7 @@ function isLangGraphResumeMapForInterrupt(
|
|
|
268
273
|
}
|
|
269
274
|
|
|
270
275
|
function getInterruptHookSessionId(payload: unknown): string | undefined {
|
|
271
|
-
const publicPayload =
|
|
272
|
-
stripRunStepResumeState(payload)
|
|
273
|
-
);
|
|
276
|
+
const publicPayload = getPublicToolInterruptPayload(payload);
|
|
274
277
|
if (
|
|
275
278
|
publicPayload == null ||
|
|
276
279
|
typeof publicPayload !== 'object' ||
|
|
@@ -423,7 +426,7 @@ function overwriteResumeRunStepState(
|
|
|
423
426
|
const update = Array.isArray(command.update)
|
|
424
427
|
? [
|
|
425
428
|
...command.update.filter(([key]) => key !== 'runStepState'),
|
|
426
|
-
|
|
429
|
+
['runStepState', overwrite] as [string, unknown],
|
|
427
430
|
]
|
|
428
431
|
: { ...(command.update ?? {}), runStepState: overwrite };
|
|
429
432
|
const resumed = new Command({
|
|
@@ -1266,6 +1269,8 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1266
1269
|
recursionLimit,
|
|
1267
1270
|
configurable: { ...callerConfig.configurable },
|
|
1268
1271
|
};
|
|
1272
|
+
delete config.configurable?.[TOOL_APPROVAL_REVIEW_CONFIG_KEY];
|
|
1273
|
+
delete config.configurable?.[TOOL_BATCH_REPLAY_KEY];
|
|
1269
1274
|
if (!isResume) {
|
|
1270
1275
|
delete config.configurable?.[SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY];
|
|
1271
1276
|
delete config.configurable?.[SUBAGENT_RESUME_MANIFEST_CONFIG_KEY];
|
|
@@ -1276,6 +1281,10 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1276
1281
|
config,
|
|
1277
1282
|
(inputs as Command).update
|
|
1278
1283
|
);
|
|
1284
|
+
const publicPayload = stripRunStepResumeState(this._interrupt?.payload);
|
|
1285
|
+
if (config.configurable != null) {
|
|
1286
|
+
restoreToolReplayConfig(config.configurable, this._interrupt?.interruptId, publicPayload);
|
|
1287
|
+
}
|
|
1279
1288
|
if (graph.getStopContinuationExecutionId() === '') {
|
|
1280
1289
|
graph.startStopContinuationExecution(nanoid());
|
|
1281
1290
|
overwriteLegacyResumeState = this.hasCheckpointer;
|
|
@@ -1514,7 +1523,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1514
1523
|
this._interrupt = {
|
|
1515
1524
|
interruptId: first.id ?? '',
|
|
1516
1525
|
threadId,
|
|
1517
|
-
payload: first.value,
|
|
1526
|
+
payload: cloneToolApprovalInterruptPayload(first.value),
|
|
1518
1527
|
};
|
|
1519
1528
|
}
|
|
1520
1529
|
}
|
|
@@ -1558,7 +1567,8 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1558
1567
|
stopReason = OUTPUT_TRUNCATED_HALT_REASON;
|
|
1559
1568
|
}
|
|
1560
1569
|
|
|
1561
|
-
const stopMessages =
|
|
1570
|
+
const stopMessages =
|
|
1571
|
+
graph.getRunMessages() ?? stateInputs?.messages ?? [];
|
|
1562
1572
|
const stopContinuationCount = graph.getStopContinuationCount();
|
|
1563
1573
|
const continuationBudgetRemaining = Math.max(
|
|
1564
1574
|
0,
|
|
@@ -1881,8 +1891,8 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1881
1891
|
}
|
|
1882
1892
|
return {
|
|
1883
1893
|
...this._interrupt,
|
|
1884
|
-
payload:
|
|
1885
|
-
|
|
1894
|
+
payload: cloneToolApprovalInterruptPayload(
|
|
1895
|
+
getPublicToolInterruptPayload(this._interrupt.payload)
|
|
1886
1896
|
),
|
|
1887
1897
|
} as t.RunInterruptResult<TPayload>;
|
|
1888
1898
|
}
|
|
@@ -2004,16 +2014,20 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
2004
2014
|
): Promise<t.RunStreamConfig> {
|
|
2005
2015
|
await this.restoreInterruptFromCheckpoint(callerConfig, resumeUpdate);
|
|
2006
2016
|
const interrupt = this._interrupt;
|
|
2007
|
-
const
|
|
2008
|
-
|
|
2009
|
-
|
|
2017
|
+
const replayPayload = stripRunStepResumeState(interrupt?.payload);
|
|
2018
|
+
const resumeManifest = requireValidSubagentResumeManifest(replayPayload) ??
|
|
2019
|
+
requireValidSubagentResumeManifest(stripToolBatchReplayState(replayPayload));
|
|
2010
2020
|
const resumeConfigurable = { ...callerConfig.configurable };
|
|
2021
|
+
delete resumeConfigurable[TOOL_APPROVAL_REVIEW_CONFIG_KEY];
|
|
2022
|
+
delete resumeConfigurable[TOOL_BATCH_REPLAY_KEY];
|
|
2011
2023
|
delete resumeConfigurable[SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY];
|
|
2012
2024
|
delete resumeConfigurable[SUBAGENT_RESUME_MANIFEST_CONFIG_KEY];
|
|
2013
2025
|
resumeConfigurable[SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY] = nanoid();
|
|
2014
2026
|
if (resumeManifest != null) {
|
|
2015
2027
|
resumeConfigurable[SUBAGENT_RESUME_MANIFEST_CONFIG_KEY] = resumeManifest;
|
|
2016
2028
|
}
|
|
2029
|
+
const publicPayload = stripRunStepResumeState(interrupt?.payload);
|
|
2030
|
+
restoreToolReplayConfig(resumeConfigurable, interrupt?.interruptId, publicPayload);
|
|
2017
2031
|
const manifestConfig = {
|
|
2018
2032
|
...callerConfig,
|
|
2019
2033
|
configurable: resumeConfigurable,
|
|
@@ -2130,7 +2144,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
2130
2144
|
const threadId = callerConfig.configurable?.thread_id;
|
|
2131
2145
|
this._interrupt = {
|
|
2132
2146
|
interruptId: persistedInterrupt.id,
|
|
2133
|
-
payload: persistedInterrupt.value,
|
|
2147
|
+
payload: cloneToolApprovalInterruptPayload(persistedInterrupt.value),
|
|
2134
2148
|
...(typeof threadId === 'string' ? { threadId } : {}),
|
|
2135
2149
|
...(typeof checkpointId === 'string' ? { checkpointId } : {}),
|
|
2136
2150
|
...(typeof checkpointNs === 'string' ? { checkpointNs } : {}),
|
|
@@ -279,7 +279,7 @@ function createBashExecutionTool(
|
|
|
279
279
|
};
|
|
280
280
|
|
|
281
281
|
const proxyAgent = resolveFetchProxyAgent(execEndpoint);
|
|
282
|
-
if (proxyAgent) {
|
|
282
|
+
if (proxyAgent != null) {
|
|
283
283
|
fetchOptions.agent = proxyAgent;
|
|
284
284
|
}
|
|
285
285
|
const response = await fetch(execEndpoint, fetchOptions);
|
|
@@ -343,7 +343,7 @@ function createBashExecutionTool(
|
|
|
343
343
|
normalizeCodeApiRequestError(error).message,
|
|
344
344
|
command
|
|
345
345
|
);
|
|
346
|
-
throw new
|
|
346
|
+
throw new CodeApiRequestError(`Execution error:\n\n${messageWithReminder}`);
|
|
347
347
|
}
|
|
348
348
|
},
|
|
349
349
|
{
|
|
@@ -542,9 +542,11 @@ export function createBashProgrammaticToolCallingTool(
|
|
|
542
542
|
(error as Error).message,
|
|
543
543
|
code
|
|
544
544
|
);
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
545
|
+
const message = `Bash programmatic execution failed: ${messageWithReminder}`;
|
|
546
|
+
if (error instanceof CodeApiRequestError) {
|
|
547
|
+
throw new CodeApiRequestError(message);
|
|
548
|
+
}
|
|
549
|
+
throw new Error(message, { cause: error });
|
|
548
550
|
}
|
|
549
551
|
},
|
|
550
552
|
{
|
|
@@ -70,6 +70,7 @@ export function appendFailedExecutionFileReminder(
|
|
|
70
70
|
code: string
|
|
71
71
|
): string {
|
|
72
72
|
if (
|
|
73
|
+
output.includes(CODE_API_CAPABILITY_ERROR_MESSAGE) ||
|
|
73
74
|
!MNT_DATA_PATH_PATTERN.test(code) ||
|
|
74
75
|
output.includes(FAILED_EXECUTION_FILE_REMINDER)
|
|
75
76
|
) {
|
|
@@ -157,10 +158,67 @@ const SAFE_CODE_API_EXECUTION_ERROR_DETAILS: Readonly<
|
|
|
157
158
|
'stdout length exceeded': 'Execution output exceeded the size limit.',
|
|
158
159
|
};
|
|
159
160
|
|
|
161
|
+
export const CODE_API_CAPABILITY_ERROR_MESSAGE =
|
|
162
|
+
'Code execution is not supported by the selected environment.';
|
|
163
|
+
const CODE_API_CAPABILITY_ERROR_GUIDANCE =
|
|
164
|
+
'This is a permanent capability mismatch. Do not retry this tool in this environment; select a compatible environment or use an available workspace tool.';
|
|
165
|
+
|
|
166
|
+
const CODE_API_CAPABILITY_LIMITATIONS: Readonly<
|
|
167
|
+
Partial<Record<string, string>>
|
|
168
|
+
> = {
|
|
169
|
+
bridge_worker_mismatch:
|
|
170
|
+
'The selected worker does not support the requested execution capabilities (such as the stateful workspace required by programmatic tool calling).',
|
|
171
|
+
execution_profile_mismatch:
|
|
172
|
+
'The selected environment does not provide the requested execution profile.',
|
|
173
|
+
capability_mismatch:
|
|
174
|
+
'The selected environment lacks a required execution capability.',
|
|
175
|
+
unsupported_capability:
|
|
176
|
+
'The selected environment lacks a required execution capability.',
|
|
177
|
+
stateful_workspace_unsupported:
|
|
178
|
+
'The selected worker does not provide a stateful workspace.',
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
function getCodeApiCapabilityErrorMessage(
|
|
182
|
+
responseBody: string
|
|
183
|
+
): string | undefined {
|
|
184
|
+
try {
|
|
185
|
+
const parsed = JSON.parse(responseBody) as {
|
|
186
|
+
error?: string;
|
|
187
|
+
code?: string;
|
|
188
|
+
message?: string;
|
|
189
|
+
} | null;
|
|
190
|
+
const code = parsed?.code ?? parsed?.error;
|
|
191
|
+
if (typeof code !== 'string') return undefined;
|
|
192
|
+
const normalizedCode = code.toLowerCase();
|
|
193
|
+
if (!Object.hasOwn(CODE_API_CAPABILITY_LIMITATIONS, normalizedCode))
|
|
194
|
+
return undefined;
|
|
195
|
+
let limitation = CODE_API_CAPABILITY_LIMITATIONS[normalizedCode];
|
|
196
|
+
if (limitation == null) return undefined;
|
|
197
|
+
if (
|
|
198
|
+
normalizedCode === 'bridge_worker_mismatch' &&
|
|
199
|
+
typeof parsed?.message === 'string' &&
|
|
200
|
+
/^Bridge worker [A-Za-z0-9._:-]+ does not provide a stateful workspace$/.test(
|
|
201
|
+
parsed.message
|
|
202
|
+
)
|
|
203
|
+
) {
|
|
204
|
+
limitation =
|
|
205
|
+
CODE_API_CAPABILITY_LIMITATIONS.stateful_workspace_unsupported;
|
|
206
|
+
}
|
|
207
|
+
return `${CODE_API_CAPABILITY_ERROR_MESSAGE} ${limitation} ${CODE_API_CAPABILITY_ERROR_GUIDANCE}`;
|
|
208
|
+
} catch {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
160
213
|
export class CodeApiRequestError extends Error {
|
|
214
|
+
readonly retryable: boolean;
|
|
215
|
+
|
|
161
216
|
constructor(message = CODE_API_UNAVAILABLE_ERROR_MESSAGE) {
|
|
162
217
|
super(message);
|
|
163
218
|
this.name = 'CodeApiRequestError';
|
|
219
|
+
this.retryable =
|
|
220
|
+
message.includes(CODE_API_UNAVAILABLE_ERROR_MESSAGE) ||
|
|
221
|
+
message.includes('Code execution is temporarily rate-limited.');
|
|
164
222
|
}
|
|
165
223
|
}
|
|
166
224
|
|
|
@@ -277,11 +335,7 @@ type CodeApiErrorResponse = {
|
|
|
277
335
|
body?: NodeJS.ReadableStream | null;
|
|
278
336
|
};
|
|
279
337
|
|
|
280
|
-
/**
|
|
281
|
-
* Only the 429 branch reads the body, and node-fetch keeps the stream and its
|
|
282
|
-
* socket alive until something does. Repeated backend failures would otherwise
|
|
283
|
-
* accumulate connections holding payloads this module deliberately discards.
|
|
284
|
-
*/
|
|
338
|
+
/** Releases unread or incomplete error responses after bounded classification. */
|
|
285
339
|
function discardResponseBody(response: CodeApiErrorResponse): void {
|
|
286
340
|
const body = response.body;
|
|
287
341
|
if (body == null || !('destroy' in body)) {
|
|
@@ -298,6 +352,48 @@ function discardResponseBody(response: CodeApiErrorResponse): void {
|
|
|
298
352
|
}
|
|
299
353
|
}
|
|
300
354
|
|
|
355
|
+
const MAX_CODE_API_ERROR_BODY_BYTES = 64 * 1024;
|
|
356
|
+
const CODE_API_ERROR_BODY_TIMEOUT_MS = 1_000;
|
|
357
|
+
|
|
358
|
+
/** Reads only small error envelopes, with a deadline for stalled upstreams. */
|
|
359
|
+
async function readCodeApiErrorBody(
|
|
360
|
+
response: CodeApiErrorResponse
|
|
361
|
+
): Promise<string> {
|
|
362
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
363
|
+
try {
|
|
364
|
+
const read = async (): Promise<string> => {
|
|
365
|
+
if (response.body != null && Symbol.asyncIterator in response.body) {
|
|
366
|
+
let bytes = 0;
|
|
367
|
+
const chunks: Buffer[] = [];
|
|
368
|
+
for await (const chunk of response.body as AsyncIterable<
|
|
369
|
+
Buffer | string
|
|
370
|
+
>) {
|
|
371
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
372
|
+
bytes += buffer.length;
|
|
373
|
+
if (bytes > MAX_CODE_API_ERROR_BODY_BYTES) return '';
|
|
374
|
+
chunks.push(buffer);
|
|
375
|
+
}
|
|
376
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
377
|
+
}
|
|
378
|
+
const body = await response.text();
|
|
379
|
+
return Buffer.byteLength(body) <= MAX_CODE_API_ERROR_BODY_BYTES
|
|
380
|
+
? body
|
|
381
|
+
: '';
|
|
382
|
+
};
|
|
383
|
+
return await Promise.race([
|
|
384
|
+
read(),
|
|
385
|
+
new Promise<string>((resolve) => {
|
|
386
|
+
timer = setTimeout(() => resolve(''), CODE_API_ERROR_BODY_TIMEOUT_MS);
|
|
387
|
+
}),
|
|
388
|
+
]);
|
|
389
|
+
} catch {
|
|
390
|
+
return '';
|
|
391
|
+
} finally {
|
|
392
|
+
clearTimeout(timer);
|
|
393
|
+
discardResponseBody(response);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
301
397
|
export async function buildCodeApiHttpErrorMessage(
|
|
302
398
|
method: CodeApiMethod,
|
|
303
399
|
_endpoint: string,
|
|
@@ -305,8 +401,8 @@ export async function buildCodeApiHttpErrorMessage(
|
|
|
305
401
|
options?: { recoverable?: boolean; profile?: t.CodeApiExecutionProfile }
|
|
306
402
|
): Promise<string> {
|
|
307
403
|
/* Logged before the body is touched. A non-OK response can leave a chunked
|
|
308
|
-
body open,
|
|
309
|
-
|
|
404
|
+
body open, so logging first preserves the diagnostic even if the bounded
|
|
405
|
+
classification read times out.
|
|
310
406
|
The body itself is never logged — it is upstream free text that can echo
|
|
311
407
|
the header that was sent — and the endpoint is host-configured, so the
|
|
312
408
|
backend is named by the profile this module chose rather than by its
|
|
@@ -325,16 +421,12 @@ export async function buildCodeApiHttpErrorMessage(
|
|
|
325
421
|
}
|
|
326
422
|
);
|
|
327
423
|
}
|
|
328
|
-
|
|
329
|
-
|
|
424
|
+
const responseBody = await readCodeApiErrorBody(response);
|
|
425
|
+
const capabilityError = getCodeApiCapabilityErrorMessage(responseBody);
|
|
426
|
+
if (capabilityError != null) {
|
|
427
|
+
return capabilityError;
|
|
330
428
|
}
|
|
331
429
|
if (response.status === 429) {
|
|
332
|
-
let responseBody = '';
|
|
333
|
-
try {
|
|
334
|
-
responseBody = await response.text();
|
|
335
|
-
} catch {
|
|
336
|
-
responseBody = '';
|
|
337
|
-
}
|
|
338
430
|
const retryAfterSeconds = getRetryAfterSeconds(responseBody);
|
|
339
431
|
return retryAfterSeconds != null
|
|
340
432
|
? `Code execution is temporarily rate-limited. Retry after ${retryAfterSeconds} seconds.`
|
|
@@ -543,7 +635,7 @@ function createCodeExecutionTool(
|
|
|
543
635
|
};
|
|
544
636
|
|
|
545
637
|
const proxyAgent = resolveFetchProxyAgent(execEndpoint);
|
|
546
|
-
if (proxyAgent) {
|
|
638
|
+
if (proxyAgent != null) {
|
|
547
639
|
fetchOptions.agent = proxyAgent;
|
|
548
640
|
}
|
|
549
641
|
const response = await fetch(execEndpoint, fetchOptions);
|
|
@@ -610,7 +702,9 @@ function createCodeExecutionTool(
|
|
|
610
702
|
normalizeCodeApiRequestError(error).message,
|
|
611
703
|
code
|
|
612
704
|
);
|
|
613
|
-
throw new
|
|
705
|
+
throw new CodeApiRequestError(
|
|
706
|
+
`Execution error:\n\n${messageWithReminder}`
|
|
707
|
+
);
|
|
614
708
|
}
|
|
615
709
|
},
|
|
616
710
|
{
|
|
@@ -469,7 +469,7 @@ export async function fetchSessionFiles(
|
|
|
469
469
|
};
|
|
470
470
|
|
|
471
471
|
const proxyAgent = resolveFetchProxyAgent(filesEndpoint, proxy);
|
|
472
|
-
if (proxyAgent) {
|
|
472
|
+
if (proxyAgent != null) {
|
|
473
473
|
fetchOptions.agent = proxyAgent;
|
|
474
474
|
}
|
|
475
475
|
|
|
@@ -531,7 +531,7 @@ export async function makeRequest(
|
|
|
531
531
|
};
|
|
532
532
|
|
|
533
533
|
const proxyAgent = resolveFetchProxyAgent(endpoint, proxy);
|
|
534
|
-
if (proxyAgent) {
|
|
534
|
+
if (proxyAgent != null) {
|
|
535
535
|
fetchOptions.agent = proxyAgent;
|
|
536
536
|
}
|
|
537
537
|
|
|
@@ -689,7 +689,7 @@ type ToolInputSchemaKind = {
|
|
|
689
689
|
function detectSchemaKind(schema: unknown): ToolInputSchemaKind {
|
|
690
690
|
const kind: ToolInputSchemaKind = { object: false, string: false };
|
|
691
691
|
|
|
692
|
-
if (
|
|
692
|
+
if (schema == null || typeof schema !== 'object') {
|
|
693
693
|
return kind;
|
|
694
694
|
}
|
|
695
695
|
|
|
@@ -704,7 +704,7 @@ function detectSchemaKind(schema: unknown): ToolInputSchemaKind {
|
|
|
704
704
|
}
|
|
705
705
|
|
|
706
706
|
const zodDef = (schema as { _def?: unknown })._def;
|
|
707
|
-
if (
|
|
707
|
+
if (zodDef == null || typeof zodDef !== 'object') {
|
|
708
708
|
return kind;
|
|
709
709
|
}
|
|
710
710
|
|
|
@@ -726,7 +726,7 @@ function detectSchemaKind(schema: unknown): ToolInputSchemaKind {
|
|
|
726
726
|
type?: unknown;
|
|
727
727
|
}
|
|
728
728
|
).innerType ?? (zodDef as { schema?: unknown }).schema;
|
|
729
|
-
if (innerSchema) {
|
|
729
|
+
if (innerSchema != null) {
|
|
730
730
|
const innerKind = detectSchemaKind(innerSchema);
|
|
731
731
|
kind.object ||= innerKind.object;
|
|
732
732
|
kind.string ||= innerKind.string;
|
|
@@ -1221,9 +1221,11 @@ export function createProgrammaticToolCallingTool(
|
|
|
1221
1221
|
(error as Error).message,
|
|
1222
1222
|
code
|
|
1223
1223
|
);
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1224
|
+
const message = `Programmatic execution failed: ${messageWithReminder}`;
|
|
1225
|
+
if (error instanceof CodeApiRequestError) {
|
|
1226
|
+
throw new CodeApiRequestError(message);
|
|
1227
|
+
}
|
|
1228
|
+
throw new Error(message, { cause: error });
|
|
1227
1229
|
}
|
|
1228
1230
|
},
|
|
1229
1231
|
{
|