@librechat/agents 3.1.85 → 3.1.87
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/README.md +69 -0
- package/dist/cjs/agents/AgentContext.cjs +7 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/events.cjs +23 -0
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +133 -18
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +251 -53
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +1 -5
- package/dist/cjs/llm/init.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +113 -24
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +3 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +18 -5
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/openai/index.cjs +253 -0
- package/dist/cjs/openai/index.cjs.map +1 -0
- package/dist/cjs/responses/index.cjs +448 -0
- package/dist/cjs/responses/index.cjs.map +1 -0
- package/dist/cjs/run.cjs +108 -7
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +1057 -0
- package/dist/cjs/session/AgentSession.cjs.map +1 -0
- package/dist/cjs/session/JsonlSessionStore.cjs +425 -0
- package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -0
- package/dist/cjs/session/handlers.cjs +221 -0
- package/dist/cjs/session/handlers.cjs.map +1 -0
- package/dist/cjs/session/ids.cjs +22 -0
- package/dist/cjs/session/ids.cjs.map +1 -0
- package/dist/cjs/session/messageSerialization.cjs +179 -0
- package/dist/cjs/session/messageSerialization.cjs.map +1 -0
- package/dist/cjs/stream.cjs +472 -11
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +1 -1
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +177 -59
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/eagerEventExecution.cjs +113 -0
- package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -0
- package/dist/cjs/tools/handlers.cjs +1 -1
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/cjs/tools/streamedToolCallSeals.cjs +42 -0
- package/dist/cjs/tools/streamedToolCallSeals.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +7 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/events.mjs +23 -1
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +133 -18
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +251 -53
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -5
- package/dist/esm/llm/init.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +113 -25
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +4 -2
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/main.mjs +5 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/openai/index.mjs +246 -0
- package/dist/esm/openai/index.mjs.map +1 -0
- package/dist/esm/responses/index.mjs +440 -0
- package/dist/esm/responses/index.mjs.map +1 -0
- package/dist/esm/run.mjs +108 -7
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +1054 -0
- package/dist/esm/session/AgentSession.mjs.map +1 -0
- package/dist/esm/session/JsonlSessionStore.mjs +422 -0
- package/dist/esm/session/JsonlSessionStore.mjs.map +1 -0
- package/dist/esm/session/handlers.mjs +219 -0
- package/dist/esm/session/handlers.mjs.map +1 -0
- package/dist/esm/session/ids.mjs +17 -0
- package/dist/esm/session/ids.mjs.map +1 -0
- package/dist/esm/session/messageSerialization.mjs +173 -0
- package/dist/esm/session/messageSerialization.mjs.map +1 -0
- package/dist/esm/stream.mjs +473 -12
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +1 -1
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +177 -59
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/eagerEventExecution.mjs +107 -0
- package/dist/esm/tools/eagerEventExecution.mjs.map +1 -0
- package/dist/esm/tools/handlers.mjs +1 -1
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/esm/tools/streamedToolCallSeals.mjs +36 -0
- package/dist/esm/tools/streamedToolCallSeals.mjs.map +1 -0
- package/dist/types/events.d.ts +1 -0
- package/dist/types/graphs/Graph.d.ts +24 -9
- package/dist/types/index.d.ts +1 -0
- package/dist/types/llm/openai/index.d.ts +1 -0
- package/dist/types/openai/index.d.ts +75 -0
- package/dist/types/responses/index.d.ts +97 -0
- package/dist/types/run.d.ts +2 -0
- package/dist/types/session/AgentSession.d.ts +32 -0
- package/dist/types/session/JsonlSessionStore.d.ts +67 -0
- package/dist/types/session/handlers.d.ts +8 -0
- package/dist/types/session/ids.d.ts +4 -0
- package/dist/types/session/index.d.ts +5 -0
- package/dist/types/session/messageSerialization.d.ts +7 -0
- package/dist/types/session/types.d.ts +191 -0
- package/dist/types/tools/ToolNode.d.ts +12 -1
- package/dist/types/tools/eagerEventExecution.d.ts +23 -0
- package/dist/types/tools/streamedToolCallSeals.d.ts +13 -0
- package/dist/types/types/hitl.d.ts +4 -0
- package/dist/types/types/run.d.ts +11 -1
- package/dist/types/types/tools.d.ts +36 -0
- package/package.json +19 -2
- package/src/__tests__/stream.eagerEventExecution.test.ts +2458 -0
- package/src/agents/AgentContext.ts +7 -2
- package/src/agents/__tests__/AgentContext.test.ts +254 -5
- package/src/events.ts +29 -0
- package/src/graphs/Graph.ts +224 -50
- package/src/graphs/MultiAgentGraph.ts +1 -1
- package/src/graphs/__tests__/composition.smoke.test.ts +30 -0
- package/src/index.ts +3 -0
- package/src/llm/anthropic/index.ts +356 -84
- package/src/llm/anthropic/llm.spec.ts +64 -0
- package/src/llm/custom-chat-models.smoke.test.ts +175 -4
- package/src/llm/openai/contentBlocks.test.ts +35 -0
- package/src/llm/openai/deepseek.test.ts +201 -2
- package/src/llm/openai/index.ts +171 -26
- package/src/llm/openai/utils/index.ts +22 -0
- package/src/llm/openrouter/index.ts +4 -2
- package/src/openai/__tests__/openai.test.ts +337 -0
- package/src/openai/index.ts +404 -0
- package/src/responses/__tests__/responses.test.ts +652 -0
- package/src/responses/index.ts +677 -0
- package/src/run.ts +158 -8
- package/src/scripts/compare_pi_vs_ours.ts +592 -173
- package/src/scripts/session_live.ts +548 -0
- package/src/session/AgentSession.ts +1432 -0
- package/src/session/JsonlSessionStore.ts +572 -0
- package/src/session/__tests__/JsonlSessionStore.test.ts +1410 -0
- package/src/session/__tests__/handlers.test.ts +161 -0
- package/src/session/handlers.ts +272 -0
- package/src/session/ids.ts +17 -0
- package/src/session/index.ts +44 -0
- package/src/session/messageSerialization.ts +207 -0
- package/src/session/types.ts +275 -0
- package/src/specs/custom-event-await.test.ts +89 -0
- package/src/specs/summarization.test.ts +1 -1
- package/src/stream.ts +755 -48
- package/src/summarization/node.ts +1 -1
- package/src/tools/ToolNode.ts +299 -126
- package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +373 -0
- package/src/tools/__tests__/handlers.test.ts +2 -1
- package/src/tools/__tests__/hitl.test.ts +206 -110
- package/src/tools/eagerEventExecution.ts +153 -0
- package/src/tools/handlers.ts +8 -4
- package/src/tools/streamedToolCallSeals.ts +57 -0
- package/src/types/hitl.ts +4 -0
- package/src/types/run.ts +11 -0
- package/src/types/tools.ts +36 -0
- package/dist/cjs/llm/text.cjs +0 -69
- package/dist/cjs/llm/text.cjs.map +0 -1
- package/dist/esm/llm/text.mjs +0 -67
- package/dist/esm/llm/text.mjs.map +0 -1
|
@@ -37,6 +37,20 @@ import { HookRegistry } from '@/hooks';
|
|
|
37
37
|
import { Providers as providers, GraphEvents } from '@/common';
|
|
38
38
|
import { ToolNode } from '../ToolNode';
|
|
39
39
|
|
|
40
|
+
async function flushAsyncWork(): Promise<void> {
|
|
41
|
+
await Promise.resolve();
|
|
42
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
43
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
44
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
45
|
+
await Promise.resolve();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
afterEach(async () => {
|
|
49
|
+
await flushAsyncWork();
|
|
50
|
+
jest.restoreAllMocks();
|
|
51
|
+
await flushAsyncWork();
|
|
52
|
+
});
|
|
53
|
+
|
|
40
54
|
/**
|
|
41
55
|
* Schema-only tool stub. ToolNode in event-driven mode uses the schema
|
|
42
56
|
* for binding/discovery but routes execution through the host via
|
|
@@ -71,8 +85,20 @@ function mockEventDispatch(mockResults: t.ToolExecuteResult[]): void {
|
|
|
71
85
|
}
|
|
72
86
|
|
|
73
87
|
type MessagesUpdate = { messages: BaseMessage[] };
|
|
88
|
+
type InterruptStateSnapshot = {
|
|
89
|
+
config?: RunnableConfig;
|
|
90
|
+
tasks?: Array<{
|
|
91
|
+
interrupts?: Array<{ id?: string }>;
|
|
92
|
+
}>;
|
|
93
|
+
};
|
|
74
94
|
type CompiledMessagesGraph = Runnable<unknown, { messages: BaseMessage[] }> & {
|
|
75
95
|
invoke(input: unknown, config?: RunnableConfig): Promise<unknown>;
|
|
96
|
+
getState?(
|
|
97
|
+
config: RunnableConfig
|
|
98
|
+
): Promise<{ config?: RunnableConfig } | undefined>;
|
|
99
|
+
getStateHistory?(
|
|
100
|
+
config: RunnableConfig
|
|
101
|
+
): AsyncIterableIterator<InterruptStateSnapshot>;
|
|
76
102
|
};
|
|
77
103
|
|
|
78
104
|
/** Factory for a minimal `agent → tools → END` graph wrapping the ToolNode. */
|
|
@@ -80,18 +106,26 @@ function buildHITLGraph(
|
|
|
80
106
|
toolNode: ToolNode,
|
|
81
107
|
toolCalls: Array<{ id: string; name: string; args: Record<string, unknown> }>
|
|
82
108
|
): CompiledMessagesGraph {
|
|
83
|
-
|
|
109
|
+
const toolCallIds = new Set(toolCalls.map((call) => call.id));
|
|
84
110
|
const builder = new StateGraph(MessagesAnnotation)
|
|
85
|
-
.addNode('agent', (): MessagesUpdate => {
|
|
86
|
-
agentInvocations += 1;
|
|
111
|
+
.addNode('agent', (state: { messages?: BaseMessage[] }): MessagesUpdate => {
|
|
87
112
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
113
|
+
* Emit the AIMessage carrying tool_calls until this test graph
|
|
114
|
+
* actually has a matching ToolMessage in state. LangGraph usually
|
|
115
|
+
* resumes at the interrupted `tools` node, but under full-suite
|
|
116
|
+
* async callback pressure it can re-enter this tiny test graph from
|
|
117
|
+
* START while still carrying the resume value. A call-count based
|
|
118
|
+
* fake agent then returned "done" too early and made HITL resume
|
|
119
|
+
* assertions order-dependent. State is the stable contract here:
|
|
120
|
+
* no tool result means the tool node still needs work.
|
|
93
121
|
*/
|
|
94
|
-
|
|
122
|
+
const hasMatchingToolResult =
|
|
123
|
+
state.messages?.some(
|
|
124
|
+
(message): boolean =>
|
|
125
|
+
message._getType() === 'tool' &&
|
|
126
|
+
toolCallIds.has((message as ToolMessage).tool_call_id)
|
|
127
|
+
) === true;
|
|
128
|
+
if (!hasMatchingToolResult) {
|
|
95
129
|
return {
|
|
96
130
|
messages: [new AIMessage({ content: '', tool_calls: toolCalls })],
|
|
97
131
|
};
|
|
@@ -123,6 +157,52 @@ function makeHookRegistry(
|
|
|
123
157
|
return registry;
|
|
124
158
|
}
|
|
125
159
|
|
|
160
|
+
function resumeFromInterrupt<TResume>(
|
|
161
|
+
interrupted: unknown,
|
|
162
|
+
resume: TResume
|
|
163
|
+
): Command {
|
|
164
|
+
if (isInterrupted<unknown>(interrupted)) {
|
|
165
|
+
const interruptId = interrupted.__interrupt__[0]?.id;
|
|
166
|
+
if (typeof interruptId === 'string' && interruptId.length > 0) {
|
|
167
|
+
return new Command({ resume: { [interruptId]: resume } });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return new Command({ resume });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function resumeGraph<TResume>(
|
|
174
|
+
graph: CompiledMessagesGraph,
|
|
175
|
+
interrupted: unknown,
|
|
176
|
+
resume: TResume,
|
|
177
|
+
config: RunnableConfig
|
|
178
|
+
): Promise<unknown> {
|
|
179
|
+
const interruptId = isInterrupted<unknown>(interrupted)
|
|
180
|
+
? interrupted.__interrupt__[0]?.id
|
|
181
|
+
: undefined;
|
|
182
|
+
let checkpointConfig = config;
|
|
183
|
+
if (typeof interruptId === 'string' && graph.getStateHistory != null) {
|
|
184
|
+
for await (const snapshot of graph.getStateHistory(config)) {
|
|
185
|
+
const hasMatchingInterrupt =
|
|
186
|
+
snapshot.tasks?.some(
|
|
187
|
+
(task) =>
|
|
188
|
+
task.interrupts?.some(
|
|
189
|
+
(interrupt) => interrupt.id === interruptId
|
|
190
|
+
) === true
|
|
191
|
+
) === true;
|
|
192
|
+
if (hasMatchingInterrupt && snapshot.config != null) {
|
|
193
|
+
checkpointConfig = snapshot.config;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
checkpointConfig = (await graph.getState?.(config))?.config ?? config;
|
|
199
|
+
}
|
|
200
|
+
return graph.invoke(
|
|
201
|
+
resumeFromInterrupt(interrupted, resume),
|
|
202
|
+
checkpointConfig
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
126
206
|
describe('ToolNode HITL — `ask` decision raises interrupt() when humanInTheLoop is enabled', () => {
|
|
127
207
|
afterEach(() => {
|
|
128
208
|
jest.restoreAllMocks();
|
|
@@ -196,10 +276,14 @@ describe('ToolNode HITL — `ask` decision raises interrupt() when humanInTheLoo
|
|
|
196
276
|
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
197
277
|
expect(isInterrupted(interrupted)).toBe(true);
|
|
198
278
|
|
|
199
|
-
const resumed = (await
|
|
200
|
-
|
|
279
|
+
const resumed = (await resumeGraph(
|
|
280
|
+
graph,
|
|
281
|
+
interrupted,
|
|
282
|
+
[{ type: 'approve' }],
|
|
201
283
|
config
|
|
202
|
-
)) as {
|
|
284
|
+
)) as {
|
|
285
|
+
messages: BaseMessage[];
|
|
286
|
+
};
|
|
203
287
|
|
|
204
288
|
const toolMessages = resumed.messages.filter(
|
|
205
289
|
(m): m is ToolMessage => m._getType() === 'tool'
|
|
@@ -226,12 +310,12 @@ describe('ToolNode HITL — `ask` decision raises interrupt() when humanInTheLoo
|
|
|
226
310
|
]);
|
|
227
311
|
const config = { configurable: { thread_id: 'thread-hitl-reject' } };
|
|
228
312
|
|
|
229
|
-
await graph.invoke({ messages: [] }, config);
|
|
313
|
+
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
230
314
|
|
|
231
|
-
const resumed = (await
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
315
|
+
const resumed = (await resumeGraph(
|
|
316
|
+
graph,
|
|
317
|
+
interrupted,
|
|
318
|
+
[{ type: 'reject', reason: 'destructive command' }],
|
|
235
319
|
config
|
|
236
320
|
)) as { messages: BaseMessage[] };
|
|
237
321
|
|
|
@@ -279,12 +363,12 @@ describe('ToolNode HITL — `ask` decision raises interrupt() when humanInTheLoo
|
|
|
279
363
|
]);
|
|
280
364
|
const config = { configurable: { thread_id: 'thread-hitl-edit' } };
|
|
281
365
|
|
|
282
|
-
await graph.invoke({ messages: [] }, config);
|
|
366
|
+
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
283
367
|
|
|
284
|
-
await
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
368
|
+
await resumeGraph(
|
|
369
|
+
graph,
|
|
370
|
+
interrupted,
|
|
371
|
+
[{ type: 'edit', updatedInput: { command: 'patched' } }],
|
|
288
372
|
config
|
|
289
373
|
);
|
|
290
374
|
|
|
@@ -320,16 +404,16 @@ describe('ToolNode HITL — `ask` decision raises interrupt() when humanInTheLoo
|
|
|
320
404
|
]);
|
|
321
405
|
const config = { configurable: { thread_id: 'thread-hitl-respond' } };
|
|
322
406
|
|
|
323
|
-
await graph.invoke({ messages: [] }, config);
|
|
407
|
+
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
324
408
|
|
|
325
409
|
const dispatchCallsBefore = dispatchSpy.mock.calls.filter(
|
|
326
410
|
([event]) => event === 'on_tool_execute'
|
|
327
411
|
).length;
|
|
328
412
|
|
|
329
|
-
const resumed = (await
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
413
|
+
const resumed = (await resumeGraph(
|
|
414
|
+
graph,
|
|
415
|
+
interrupted,
|
|
416
|
+
[{ type: 'respond', responseText: 'no relevant results' }],
|
|
333
417
|
config
|
|
334
418
|
)) as { messages: BaseMessage[] };
|
|
335
419
|
|
|
@@ -399,10 +483,12 @@ describe('ToolNode HITL — `ask` decision raises interrupt() when humanInTheLoo
|
|
|
399
483
|
]);
|
|
400
484
|
const config = { configurable: { thread_id: 'thread-hitl-map' } };
|
|
401
485
|
|
|
402
|
-
await graph.invoke({ messages: [] }, config);
|
|
486
|
+
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
403
487
|
|
|
404
|
-
const resumed = (await
|
|
405
|
-
|
|
488
|
+
const resumed = (await resumeGraph(
|
|
489
|
+
graph,
|
|
490
|
+
interrupted,
|
|
491
|
+
{ call_1: { type: 'approve' } },
|
|
406
492
|
config
|
|
407
493
|
)) as { messages: BaseMessage[] };
|
|
408
494
|
|
|
@@ -561,10 +647,10 @@ describe('ToolNode HITL — multi-tool batches', () => {
|
|
|
561
647
|
'call_2',
|
|
562
648
|
]);
|
|
563
649
|
|
|
564
|
-
const resumed = (await
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
650
|
+
const resumed = (await resumeGraph(
|
|
651
|
+
graph,
|
|
652
|
+
interrupted,
|
|
653
|
+
[{ type: 'approve' }, { type: 'reject', reason: 'too risky' }],
|
|
568
654
|
config
|
|
569
655
|
)) as { messages: BaseMessage[] };
|
|
570
656
|
|
|
@@ -745,11 +831,12 @@ describe('Run integration — HITL fallback checkpointer + resume', () => {
|
|
|
745
831
|
],
|
|
746
832
|
});
|
|
747
833
|
|
|
834
|
+
const hexToolCallId = '0123456789abcdef0123456789abcdef';
|
|
748
835
|
const node = new ToolNode({
|
|
749
836
|
tools: [createSchemaStub('echo')],
|
|
750
837
|
eventDrivenMode: true,
|
|
751
838
|
agentId: 'agent-x',
|
|
752
|
-
toolCallStepIds: new Map([[
|
|
839
|
+
toolCallStepIds: new Map([[hexToolCallId, 'step_1']]),
|
|
753
840
|
hookRegistry: registry,
|
|
754
841
|
humanInTheLoop: { enabled: true },
|
|
755
842
|
});
|
|
@@ -762,7 +849,7 @@ describe('Run integration — HITL fallback checkpointer + resume', () => {
|
|
|
762
849
|
new AIMessage({
|
|
763
850
|
content: '',
|
|
764
851
|
tool_calls: [
|
|
765
|
-
{ id:
|
|
852
|
+
{ id: hexToolCallId, name: 'echo', args: { command: 'x' } },
|
|
766
853
|
],
|
|
767
854
|
}),
|
|
768
855
|
],
|
|
@@ -804,8 +891,10 @@ describe('Run integration — HITL fallback checkpointer + resume', () => {
|
|
|
804
891
|
expect(dispatchCount).toBe(0);
|
|
805
892
|
|
|
806
893
|
/** This is the API contract under test: Run.resume() with a
|
|
807
|
-
* decision
|
|
808
|
-
|
|
894
|
+
* tool_call_id-keyed decision map (not graph.invoke + Command).
|
|
895
|
+
* The tool_call_id intentionally looks like a LangGraph interrupt
|
|
896
|
+
* id; Run.resume must still wrap it under the real interrupt id. */
|
|
897
|
+
await run.resume({ [hexToolCallId]: { type: 'approve' } }, callerConfig);
|
|
809
898
|
|
|
810
899
|
expect(dispatchCount).toBe(1);
|
|
811
900
|
/** Resume completed naturally: interrupt cleared, no halt
|
|
@@ -1732,13 +1821,13 @@ describe('Codex review fixes', () => {
|
|
|
1732
1821
|
]);
|
|
1733
1822
|
const config = { configurable: { thread_id: 'dedup-thread' } };
|
|
1734
1823
|
|
|
1735
|
-
await graph.invoke({ messages: [] }, config);
|
|
1824
|
+
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
1736
1825
|
/** First pass: interrupt() threw, so the deferred denial side
|
|
1737
1826
|
* effects were not flushed. Zero step-completed events for the
|
|
1738
1827
|
* denied tool yet. */
|
|
1739
1828
|
expect(stepCompletedDispatches.filter((id) => id === 'call_a')).toEqual([]);
|
|
1740
1829
|
|
|
1741
|
-
await graph
|
|
1830
|
+
await resumeGraph(graph, interrupted, [{ type: 'approve' }], config);
|
|
1742
1831
|
|
|
1743
1832
|
/** After resume: the denied tool dispatches exactly once (deferred
|
|
1744
1833
|
* flush on the resume re-execution); the approved tool dispatches
|
|
@@ -1803,13 +1892,13 @@ describe('Codex review fixes', () => {
|
|
|
1803
1892
|
]);
|
|
1804
1893
|
const config = { configurable: { thread_id: 'allowed-enforce' } };
|
|
1805
1894
|
|
|
1806
|
-
await graph.invoke({ messages: [] }, config);
|
|
1895
|
+
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
1807
1896
|
|
|
1808
1897
|
/** Submit `edit` — outside the advertised allowlist. */
|
|
1809
|
-
const resumed = (await
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
}
|
|
1898
|
+
const resumed = (await resumeGraph(
|
|
1899
|
+
graph,
|
|
1900
|
+
interrupted,
|
|
1901
|
+
[{ type: 'edit', updatedInput: { command: 'malicious' } }],
|
|
1813
1902
|
config
|
|
1814
1903
|
)) as { messages: BaseMessage[] };
|
|
1815
1904
|
|
|
@@ -1875,10 +1964,10 @@ describe('Codex review fixes', () => {
|
|
|
1875
1964
|
]);
|
|
1876
1965
|
const config = { configurable: { thread_id: 'allowed-pass' } };
|
|
1877
1966
|
|
|
1878
|
-
await graph.invoke({ messages: [] }, config);
|
|
1967
|
+
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
1879
1968
|
|
|
1880
1969
|
/** Submit `approve` — explicitly in the allowlist. */
|
|
1881
|
-
await graph
|
|
1970
|
+
await resumeGraph(graph, interrupted, [{ type: 'approve' }], config);
|
|
1882
1971
|
|
|
1883
1972
|
expect(dispatchedArgs).toEqual([{ command: 'original' }]);
|
|
1884
1973
|
});
|
|
@@ -2048,7 +2137,7 @@ describe('Codex review fixes', () => {
|
|
|
2048
2137
|
command: 'redacted-command',
|
|
2049
2138
|
});
|
|
2050
2139
|
|
|
2051
|
-
await graph
|
|
2140
|
+
await resumeGraph(graph, interrupted, [{ type: 'approve' }], config);
|
|
2052
2141
|
|
|
2053
2142
|
/** And the host execution dispatches the rewritten args, not
|
|
2054
2143
|
* the original. Without the fix, the policy redaction would be
|
|
@@ -2307,15 +2396,15 @@ describe('Codex review fixes', () => {
|
|
|
2307
2396
|
]);
|
|
2308
2397
|
const config = { configurable: { thread_id: 'edit-malformed' } };
|
|
2309
2398
|
|
|
2310
|
-
await graph.invoke({ messages: [] }, config);
|
|
2399
|
+
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
2311
2400
|
|
|
2312
2401
|
/** `{ type: 'edit' }` with no updatedInput — same trust-boundary
|
|
2313
2402
|
* issue as malformed respond. Must fail closed, NOT pass undefined
|
|
2314
2403
|
* into applyInputOverride and approve a tool with garbage args. */
|
|
2315
|
-
const resumed = (await
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
}
|
|
2404
|
+
const resumed = (await resumeGraph(
|
|
2405
|
+
graph,
|
|
2406
|
+
interrupted,
|
|
2407
|
+
[{ type: 'edit' } as unknown as t.ToolApprovalDecision],
|
|
2319
2408
|
config
|
|
2320
2409
|
)) as { messages: BaseMessage[] };
|
|
2321
2410
|
|
|
@@ -2361,19 +2450,19 @@ describe('Codex review fixes', () => {
|
|
|
2361
2450
|
]);
|
|
2362
2451
|
const config = { configurable: { thread_id: 'edit-nonobject' } };
|
|
2363
2452
|
|
|
2364
|
-
await graph.invoke({ messages: [] }, config);
|
|
2453
|
+
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
2365
2454
|
|
|
2366
2455
|
/** `updatedInput: 'string'` — wire deserializer didn't enforce
|
|
2367
2456
|
* object shape; SDK must reject. */
|
|
2368
|
-
const resumed = (await
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2457
|
+
const resumed = (await resumeGraph(
|
|
2458
|
+
graph,
|
|
2459
|
+
interrupted,
|
|
2460
|
+
[
|
|
2461
|
+
{
|
|
2462
|
+
type: 'edit',
|
|
2463
|
+
updatedInput: 'not-an-object' as unknown as Record<string, unknown>,
|
|
2464
|
+
},
|
|
2465
|
+
],
|
|
2377
2466
|
config
|
|
2378
2467
|
)) as { messages: BaseMessage[] };
|
|
2379
2468
|
|
|
@@ -2410,17 +2499,17 @@ describe('Codex review fixes', () => {
|
|
|
2410
2499
|
]);
|
|
2411
2500
|
const config = { configurable: { thread_id: 'edit-array' } };
|
|
2412
2501
|
|
|
2413
|
-
await graph.invoke({ messages: [] }, config);
|
|
2502
|
+
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
2414
2503
|
|
|
2415
|
-
const resumed = (await
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2504
|
+
const resumed = (await resumeGraph(
|
|
2505
|
+
graph,
|
|
2506
|
+
interrupted,
|
|
2507
|
+
[
|
|
2508
|
+
{
|
|
2509
|
+
type: 'edit',
|
|
2510
|
+
updatedInput: [1, 2, 3] as unknown as Record<string, unknown>,
|
|
2511
|
+
},
|
|
2512
|
+
],
|
|
2424
2513
|
config
|
|
2425
2514
|
)) as { messages: BaseMessage[] };
|
|
2426
2515
|
|
|
@@ -2462,15 +2551,15 @@ describe('Codex review fixes', () => {
|
|
|
2462
2551
|
]);
|
|
2463
2552
|
const config = { configurable: { thread_id: 'respond-malformed' } };
|
|
2464
2553
|
|
|
2465
|
-
await graph.invoke({ messages: [] }, config);
|
|
2554
|
+
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
2466
2555
|
|
|
2467
2556
|
/** Submit a `respond` decision with NO responseText — wire shape
|
|
2468
2557
|
* the SDK can't honor. Must fail closed (blockEntry path), NOT
|
|
2469
2558
|
* crash truncateToolResultContent on `undefined.length`. */
|
|
2470
|
-
const resumed = (await
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
}
|
|
2559
|
+
const resumed = (await resumeGraph(
|
|
2560
|
+
graph,
|
|
2561
|
+
interrupted,
|
|
2562
|
+
[{ type: 'respond' } as unknown as t.ToolApprovalDecision],
|
|
2474
2563
|
config
|
|
2475
2564
|
)) as { messages: BaseMessage[] };
|
|
2476
2565
|
|
|
@@ -2508,19 +2597,19 @@ describe('Codex review fixes', () => {
|
|
|
2508
2597
|
]);
|
|
2509
2598
|
const config = { configurable: { thread_id: 'respond-nonstring' } };
|
|
2510
2599
|
|
|
2511
|
-
await graph.invoke({ messages: [] }, config);
|
|
2600
|
+
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
2512
2601
|
|
|
2513
2602
|
/** `responseText: 42` — wire deserializer didn't enforce string;
|
|
2514
2603
|
* SDK must reject without crashing. */
|
|
2515
|
-
const resumed = (await
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2604
|
+
const resumed = (await resumeGraph(
|
|
2605
|
+
graph,
|
|
2606
|
+
interrupted,
|
|
2607
|
+
[
|
|
2608
|
+
{
|
|
2609
|
+
type: 'respond',
|
|
2610
|
+
responseText: 42 as unknown as string,
|
|
2611
|
+
},
|
|
2612
|
+
],
|
|
2524
2613
|
config
|
|
2525
2614
|
)) as { messages: BaseMessage[] };
|
|
2526
2615
|
|
|
@@ -2571,14 +2660,14 @@ describe('Codex review fixes', () => {
|
|
|
2571
2660
|
]);
|
|
2572
2661
|
const config = { configurable: { thread_id: 'respond-truncate' } };
|
|
2573
2662
|
|
|
2574
|
-
await graph.invoke({ messages: [] }, config);
|
|
2663
|
+
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
2575
2664
|
|
|
2576
2665
|
/** 200-char response — well over the 50-char cap. */
|
|
2577
2666
|
const oversized = 'A'.repeat(200);
|
|
2578
|
-
const resumed = (await
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
}
|
|
2667
|
+
const resumed = (await resumeGraph(
|
|
2668
|
+
graph,
|
|
2669
|
+
interrupted,
|
|
2670
|
+
[{ type: 'respond', responseText: oversized }],
|
|
2582
2671
|
config
|
|
2583
2672
|
)) as { messages: BaseMessage[] };
|
|
2584
2673
|
|
|
@@ -3091,8 +3180,10 @@ describe('Codex review fixes', () => {
|
|
|
3091
3180
|
expect(payload.action_requests[0].tool_call_id).toBe('call_b');
|
|
3092
3181
|
expect(dispatchedToolNames).toEqual([]);
|
|
3093
3182
|
|
|
3094
|
-
const resumed = (await
|
|
3095
|
-
|
|
3183
|
+
const resumed = (await resumeGraph(
|
|
3184
|
+
graph,
|
|
3185
|
+
interrupted,
|
|
3186
|
+
[{ type: 'approve' }],
|
|
3096
3187
|
config
|
|
3097
3188
|
)) as { messages: BaseMessage[] };
|
|
3098
3189
|
|
|
@@ -3222,17 +3313,17 @@ describe('Codex review fixes', () => {
|
|
|
3222
3313
|
]);
|
|
3223
3314
|
const config = { configurable: { thread_id: 'mixed-respond-reject' } };
|
|
3224
3315
|
|
|
3225
|
-
await graph.invoke({ messages: [] }, config);
|
|
3316
|
+
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
3226
3317
|
/** First pass: interrupt fires before either dispatch path runs. */
|
|
3227
3318
|
expect(stepCompletedDispatches).toEqual([]);
|
|
3228
3319
|
|
|
3229
|
-
const resumed = (await
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3320
|
+
const resumed = (await resumeGraph(
|
|
3321
|
+
graph,
|
|
3322
|
+
interrupted,
|
|
3323
|
+
[
|
|
3324
|
+
{ type: 'respond', responseText: 'fake answer' },
|
|
3325
|
+
{ type: 'reject', reason: 'no thanks' },
|
|
3326
|
+
],
|
|
3236
3327
|
config
|
|
3237
3328
|
)) as { messages: BaseMessage[] };
|
|
3238
3329
|
|
|
@@ -3394,13 +3485,13 @@ describe('Codex review fixes', () => {
|
|
|
3394
3485
|
]);
|
|
3395
3486
|
const config = { configurable: { thread_id: 'unknown-decision' } };
|
|
3396
3487
|
|
|
3397
|
-
await graph.invoke({ messages: [] }, config);
|
|
3488
|
+
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
3398
3489
|
|
|
3399
3490
|
/** Host sends a typo'd / malformed decision. Must NOT silently approve. */
|
|
3400
|
-
const resumed = (await
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
}
|
|
3491
|
+
const resumed = (await resumeGraph(
|
|
3492
|
+
graph,
|
|
3493
|
+
interrupted,
|
|
3494
|
+
[{ type: 'aproved' as 'approve' }],
|
|
3404
3495
|
config
|
|
3405
3496
|
)) as { messages: BaseMessage[] };
|
|
3406
3497
|
|
|
@@ -3555,7 +3646,7 @@ describe('AskUserQuestion — interrupt + resume', () => {
|
|
|
3555
3646
|
const config = { configurable: { thread_id: 'ask-q-thread' } };
|
|
3556
3647
|
|
|
3557
3648
|
const interrupted = (await graph.invoke({ messages: [] }, config)) as {
|
|
3558
|
-
__interrupt__?: Array<{ value?: t.HumanInterruptPayload }>;
|
|
3649
|
+
__interrupt__?: Array<{ id?: string; value?: t.HumanInterruptPayload }>;
|
|
3559
3650
|
};
|
|
3560
3651
|
expect(interrupted.__interrupt__).toBeDefined();
|
|
3561
3652
|
const payload = interrupted.__interrupt__![0].value!;
|
|
@@ -3566,7 +3657,12 @@ describe('AskUserQuestion — interrupt + resume', () => {
|
|
|
3566
3657
|
expect(payload.question.options).toHaveLength(2);
|
|
3567
3658
|
|
|
3568
3659
|
const resolution: t.AskUserQuestionResolution = { answer: 'production' };
|
|
3569
|
-
await
|
|
3660
|
+
await resumeGraph(
|
|
3661
|
+
graph as unknown as CompiledMessagesGraph,
|
|
3662
|
+
interrupted,
|
|
3663
|
+
resolution,
|
|
3664
|
+
config
|
|
3665
|
+
);
|
|
3570
3666
|
|
|
3571
3667
|
expect(resumedAnswer).toBe('production');
|
|
3572
3668
|
});
|