@librechat/agents 3.4.0 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/graphs/Graph.cjs +111 -27
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +83 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/executeHooks.cjs +60 -14
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs +2 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +3 -3
- package/dist/cjs/llm/invoke.cjs +2 -2
- package/dist/cjs/main.cjs +14 -13
- package/dist/cjs/messages/format.cjs +40 -23
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +82 -15
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +37 -2
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/stream.cjs +1 -1
- package/dist/cjs/summarization/node.cjs +6 -3
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
- package/dist/cjs/tools/ToolNode.cjs +316 -94
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +630 -78
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +175 -0
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
- package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +2 -2
- package/dist/esm/graphs/Graph.mjs +111 -27
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +83 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/executeHooks.mjs +60 -14
- package/dist/esm/hooks/executeHooks.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs +2 -1
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +2 -2
- package/dist/esm/main.mjs +11 -11
- package/dist/esm/messages/format.mjs +40 -23
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +83 -16
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +37 -2
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/stream.mjs +1 -1
- package/dist/esm/summarization/node.mjs +6 -3
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
- package/dist/esm/tools/ToolNode.mjs +317 -95
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +631 -79
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +168 -0
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
- package/dist/esm/tools/toolOutputReferences.mjs +20 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +2 -2
- package/dist/types/graphs/Graph.d.ts +16 -1
- package/dist/types/hooks/HookRegistry.d.ts +10 -1
- package/dist/types/hooks/executeHooks.d.ts +5 -1
- package/dist/types/hooks/index.d.ts +2 -2
- package/dist/types/hooks/types.d.ts +12 -0
- package/dist/types/run.d.ts +4 -1
- package/dist/types/session/AgentSession.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +23 -15
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +46 -7
- package/dist/types/tools/subagent/SubagentReplay.d.ts +81 -0
- package/dist/types/tools/toolOutputReferences.d.ts +12 -0
- package/dist/types/types/graph.d.ts +5 -5
- package/dist/types/types/hitl.d.ts +15 -0
- package/package.json +6 -2
- package/src/graphs/Graph.ts +246 -52
- package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
- package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
- package/src/hooks/HookRegistry.ts +151 -1
- package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
- package/src/hooks/__tests__/executeHooks.test.ts +160 -0
- package/src/hooks/executeHooks.ts +133 -15
- package/src/hooks/index.ts +3 -1
- package/src/hooks/types.ts +16 -0
- package/src/messages/format.ts +54 -26
- package/src/messages/formatAgentMessages.reducer.test.ts +162 -0
- package/src/messages/formatAgentMessages.steer.test.ts +16 -11
- package/src/messages/formatAgentMessages.test.ts +7 -2
- package/src/run.ts +164 -18
- package/src/session/AgentSession.ts +52 -2
- package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
- package/src/summarization/__tests__/node.test.ts +32 -14
- package/src/summarization/node.ts +15 -11
- package/src/tools/ToolNode.ts +631 -157
- package/src/tools/__tests__/SubagentExecutor.test.ts +542 -6
- package/src/tools/__tests__/SubagentReplay.test.ts +300 -0
- package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +486 -2
- package/src/tools/__tests__/hitl.test.ts +234 -5
- package/src/tools/__tests__/subagentHooks.test.ts +1040 -62
- package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
- package/src/tools/subagent/SubagentExecutor.ts +1369 -131
- package/src/tools/subagent/SubagentReplay.ts +575 -0
- package/src/tools/toolOutputReferences.ts +40 -1
- package/src/types/graph.ts +5 -5
- package/src/types/hitl.ts +16 -0
|
@@ -32,9 +32,9 @@ import type {
|
|
|
32
32
|
UserPromptSubmitHookOutput,
|
|
33
33
|
} from '@/hooks';
|
|
34
34
|
import type * as t from '@/types';
|
|
35
|
-
import { Providers as providers, GraphEvents } from '@/common';
|
|
36
|
-
import * as events from '@/utils/events';
|
|
35
|
+
import { Constants, Providers as providers, GraphEvents } from '@/common';
|
|
37
36
|
import { HookRegistry, createToolPolicyHook } from '@/hooks';
|
|
37
|
+
import * as events from '@/utils/events';
|
|
38
38
|
import { askUserQuestion } from '@/hitl';
|
|
39
39
|
import { ToolNode } from '../ToolNode';
|
|
40
40
|
|
|
@@ -1015,13 +1015,13 @@ describe('Run integration — HITL fallback checkpointer + resume', () => {
|
|
|
1015
1015
|
},
|
|
1016
1016
|
],
|
|
1017
1017
|
// Caller compileOptions without a checkpointer: HITL adds a MemorySaver
|
|
1018
|
-
// fallback
|
|
1019
|
-
// metadata (no checkpointer) onto Graph.compileOptions.
|
|
1018
|
+
// fallback while preserving the caller's remaining compile options.
|
|
1020
1019
|
compileOptions: { interruptBefore: [] },
|
|
1021
1020
|
},
|
|
1022
1021
|
humanInTheLoop: { enabled: true },
|
|
1023
1022
|
});
|
|
1024
|
-
expect(run.Graph?.compileOptions?.checkpointer).
|
|
1023
|
+
expect(run.Graph?.compileOptions?.checkpointer).toBeInstanceOf(MemorySaver);
|
|
1024
|
+
expect(run.Graph?.compileOptions?.interruptBefore).toEqual([]);
|
|
1025
1025
|
|
|
1026
1026
|
const graph = new StateGraph(MessagesAnnotation)
|
|
1027
1027
|
.addNode('noop', (): MessagesUpdate => ({ messages: [] }))
|
|
@@ -1090,6 +1090,99 @@ describe('Run integration — HITL fallback checkpointer + resume', () => {
|
|
|
1090
1090
|
expect(cmd2.goto).toEqual([]);
|
|
1091
1091
|
});
|
|
1092
1092
|
|
|
1093
|
+
it('Run.resume restores a persisted interrupt before scoping a rebuilt resume', async () => {
|
|
1094
|
+
const { Run } = await import('@/run');
|
|
1095
|
+
const { Providers } = await import('@/common');
|
|
1096
|
+
|
|
1097
|
+
const registry = new HookRegistry();
|
|
1098
|
+
const persistedMatcher = {
|
|
1099
|
+
hooks: [async (): Promise<PreToolUseHookOutput> => ({ decision: 'ask' })],
|
|
1100
|
+
};
|
|
1101
|
+
registry.registerSession(
|
|
1102
|
+
'persisted-hook-session',
|
|
1103
|
+
'PreToolUse',
|
|
1104
|
+
persistedMatcher
|
|
1105
|
+
);
|
|
1106
|
+
|
|
1107
|
+
const run = await Run.create<t.IState>({
|
|
1108
|
+
runId: 'hitl-rebuilt-scope',
|
|
1109
|
+
graphConfig: {
|
|
1110
|
+
type: 'standard',
|
|
1111
|
+
agents: [
|
|
1112
|
+
{
|
|
1113
|
+
agentId: 'a',
|
|
1114
|
+
provider: Providers.OPENAI,
|
|
1115
|
+
clientOptions: {
|
|
1116
|
+
modelName: 'gpt-4o-mini',
|
|
1117
|
+
apiKey: 'test-key',
|
|
1118
|
+
},
|
|
1119
|
+
instructions: 'noop',
|
|
1120
|
+
maxContextTokens: 8000,
|
|
1121
|
+
},
|
|
1122
|
+
],
|
|
1123
|
+
},
|
|
1124
|
+
hooks: registry,
|
|
1125
|
+
humanInTheLoop: { enabled: true },
|
|
1126
|
+
});
|
|
1127
|
+
const persistedState = {
|
|
1128
|
+
config: {
|
|
1129
|
+
configurable: {
|
|
1130
|
+
thread_id: 'durable-thread',
|
|
1131
|
+
checkpoint_id: 'interrupted-checkpoint',
|
|
1132
|
+
checkpoint_ns: '',
|
|
1133
|
+
},
|
|
1134
|
+
},
|
|
1135
|
+
tasks: [
|
|
1136
|
+
{
|
|
1137
|
+
interrupts: [
|
|
1138
|
+
{
|
|
1139
|
+
id: 'persisted-interrupt',
|
|
1140
|
+
value: {
|
|
1141
|
+
type: 'tool_approval',
|
|
1142
|
+
hook_session_id: 'persisted-hook-session',
|
|
1143
|
+
},
|
|
1144
|
+
},
|
|
1145
|
+
],
|
|
1146
|
+
},
|
|
1147
|
+
],
|
|
1148
|
+
};
|
|
1149
|
+
const getState = jest.fn(async (_config: RunnableConfig) => persistedState);
|
|
1150
|
+
run.graphRunnable = { getState } as unknown as t.CompiledStateWorkflow;
|
|
1151
|
+
const processSpy = jest
|
|
1152
|
+
.spyOn(run, 'processStream')
|
|
1153
|
+
.mockResolvedValue(undefined);
|
|
1154
|
+
const callerConfig = {
|
|
1155
|
+
version: 'v2' as const,
|
|
1156
|
+
configurable: { thread_id: 'durable-thread' },
|
|
1157
|
+
};
|
|
1158
|
+
const decision = [{ type: 'approve' as const }];
|
|
1159
|
+
|
|
1160
|
+
await run.resume(decision, callerConfig);
|
|
1161
|
+
|
|
1162
|
+
expect(getState).toHaveBeenCalledWith(callerConfig);
|
|
1163
|
+
const command = processSpy.mock.calls[0]?.[0] as Command;
|
|
1164
|
+
expect(command.resume).toEqual({ 'persisted-interrupt': decision });
|
|
1165
|
+
expect(processSpy.mock.calls[0]?.[1].configurable).toMatchObject({
|
|
1166
|
+
thread_id: 'durable-thread',
|
|
1167
|
+
checkpoint_id: 'interrupted-checkpoint',
|
|
1168
|
+
checkpoint_ns: '',
|
|
1169
|
+
});
|
|
1170
|
+
expect(run.getInterrupt()).toMatchObject({
|
|
1171
|
+
interruptId: 'persisted-interrupt',
|
|
1172
|
+
threadId: 'durable-thread',
|
|
1173
|
+
payload: {
|
|
1174
|
+
type: 'tool_approval',
|
|
1175
|
+
hook_session_id: 'persisted-hook-session',
|
|
1176
|
+
},
|
|
1177
|
+
});
|
|
1178
|
+
expect(
|
|
1179
|
+
registry.getMatchers('PreToolUse', 'persisted-hook-session')
|
|
1180
|
+
).toEqual([persistedMatcher]);
|
|
1181
|
+
expect(registry.getMatchers('PreToolUse', run.id)).toEqual([
|
|
1182
|
+
persistedMatcher,
|
|
1183
|
+
]);
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1093
1186
|
it('re-exports langgraph HITL primitives from the SDK barrel for host use', async () => {
|
|
1094
1187
|
const indexExports = await import('@/index');
|
|
1095
1188
|
expect(indexExports.MemorySaver).toBe(MemorySaver);
|
|
@@ -2194,6 +2287,81 @@ describe('Codex review fixes', () => {
|
|
|
2194
2287
|
jest.restoreAllMocks();
|
|
2195
2288
|
});
|
|
2196
2289
|
|
|
2290
|
+
it('reruns live hooks while replaying a consumed event-tool approval', async () => {
|
|
2291
|
+
let dispatchCalls = 0;
|
|
2292
|
+
jest
|
|
2293
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
2294
|
+
.mockImplementation(async (event, data) => {
|
|
2295
|
+
if (event !== 'on_tool_execute') {
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
dispatchCalls += 1;
|
|
2299
|
+
const request = data as {
|
|
2300
|
+
resolve: (results: t.ToolExecuteResult[]) => void;
|
|
2301
|
+
};
|
|
2302
|
+
request.resolve([
|
|
2303
|
+
{ toolCallId: 'call_event', content: 'ran', status: 'success' },
|
|
2304
|
+
]);
|
|
2305
|
+
});
|
|
2306
|
+
|
|
2307
|
+
const registry = new HookRegistry();
|
|
2308
|
+
let onceCalls = 0;
|
|
2309
|
+
let liveCalls = 0;
|
|
2310
|
+
registry.register('PreToolUse', {
|
|
2311
|
+
once: true,
|
|
2312
|
+
pattern: '^echo$',
|
|
2313
|
+
hooks: [
|
|
2314
|
+
async (): Promise<PreToolUseHookOutput> => {
|
|
2315
|
+
onceCalls += 1;
|
|
2316
|
+
return { decision: 'ask', reason: 'review once' };
|
|
2317
|
+
},
|
|
2318
|
+
],
|
|
2319
|
+
});
|
|
2320
|
+
registry.register('PreToolUse', {
|
|
2321
|
+
pattern: '^echo$',
|
|
2322
|
+
hooks: [
|
|
2323
|
+
async (): Promise<PreToolUseHookOutput> => {
|
|
2324
|
+
liveCalls += 1;
|
|
2325
|
+
return liveCalls === 1
|
|
2326
|
+
? { decision: 'allow' }
|
|
2327
|
+
: { decision: 'deny', reason: 'policy changed before resume' };
|
|
2328
|
+
},
|
|
2329
|
+
],
|
|
2330
|
+
});
|
|
2331
|
+
const node = new ToolNode({
|
|
2332
|
+
tools: [createSchemaStub('echo')],
|
|
2333
|
+
eventDrivenMode: true,
|
|
2334
|
+
agentId: 'agent-event-replay',
|
|
2335
|
+
toolCallStepIds: new Map([['call_event', 'step_event']]),
|
|
2336
|
+
hookRegistry: registry,
|
|
2337
|
+
humanInTheLoop: { enabled: true },
|
|
2338
|
+
});
|
|
2339
|
+
const graph = buildHITLGraph(node, [
|
|
2340
|
+
{ id: 'call_event', name: 'echo', args: { command: 'run' } },
|
|
2341
|
+
]);
|
|
2342
|
+
const config = {
|
|
2343
|
+
configurable: { thread_id: 'thread-event-hook-replay' },
|
|
2344
|
+
};
|
|
2345
|
+
|
|
2346
|
+
const interrupted = await graph.invoke({ messages: [] }, config);
|
|
2347
|
+
expect(isInterrupted(interrupted)).toBe(true);
|
|
2348
|
+
const resumed = await graph.invoke(
|
|
2349
|
+
new Command({ resume: [{ type: 'approve' }] }),
|
|
2350
|
+
config
|
|
2351
|
+
);
|
|
2352
|
+
|
|
2353
|
+
expect(onceCalls).toBe(1);
|
|
2354
|
+
expect(liveCalls).toBe(2);
|
|
2355
|
+
expect(dispatchCalls).toBe(0);
|
|
2356
|
+
const messages = (resumed as { messages: ToolMessage[] }).messages;
|
|
2357
|
+
const result = messages.find(
|
|
2358
|
+
(message) =>
|
|
2359
|
+
message instanceof ToolMessage && message.tool_call_id === 'call_event'
|
|
2360
|
+
);
|
|
2361
|
+
expect(result?.status).toBe('error');
|
|
2362
|
+
expect(String(result?.content)).toContain('policy changed before resume');
|
|
2363
|
+
});
|
|
2364
|
+
|
|
2197
2365
|
it('preserves session-scoped hooks across HITL interrupt so the policy still fires on resume', async () => {
|
|
2198
2366
|
let dispatchCalls = 0;
|
|
2199
2367
|
jest
|
|
@@ -2295,6 +2463,10 @@ describe('Codex review fixes', () => {
|
|
|
2295
2463
|
* MUST still be present — the regression was that finally cleared
|
|
2296
2464
|
* it, leaving the resume to bypass the policy entirely. */
|
|
2297
2465
|
expect(run.getInterrupt()).toBeDefined();
|
|
2466
|
+
expect(run.getInterrupt()?.payload).toMatchObject({
|
|
2467
|
+
type: 'tool_approval',
|
|
2468
|
+
hook_session_id: runId,
|
|
2469
|
+
});
|
|
2298
2470
|
expect(preCallCount).toBe(1);
|
|
2299
2471
|
expect(registry.hasHookFor('PreToolUse', runId)).toBe(true);
|
|
2300
2472
|
expect(dispatchCalls).toBe(0);
|
|
@@ -2310,6 +2482,63 @@ describe('Codex review fixes', () => {
|
|
|
2310
2482
|
expect(registry.hasHookFor('PreToolUse', runId)).toBe(false);
|
|
2311
2483
|
});
|
|
2312
2484
|
|
|
2485
|
+
it('persists the parent hook session when a direct subagent approval interrupts', async () => {
|
|
2486
|
+
const runId = 'parent-subagent-policy';
|
|
2487
|
+
const executeSubagent = jest.fn(async () => 'child result');
|
|
2488
|
+
const subagentTool = tool(executeSubagent, {
|
|
2489
|
+
name: Constants.SUBAGENT,
|
|
2490
|
+
description: 'execute a child agent',
|
|
2491
|
+
schema: z.object({
|
|
2492
|
+
description: z.string(),
|
|
2493
|
+
subagent_type: z.string(),
|
|
2494
|
+
}),
|
|
2495
|
+
}) as unknown as StructuredToolInterface;
|
|
2496
|
+
const registry = new HookRegistry();
|
|
2497
|
+
registry.registerSession(runId, 'PreToolUse', {
|
|
2498
|
+
hooks: [
|
|
2499
|
+
async (): Promise<PreToolUseHookOutput> => ({
|
|
2500
|
+
decision: 'ask',
|
|
2501
|
+
reason: 'approve child delegation',
|
|
2502
|
+
}),
|
|
2503
|
+
],
|
|
2504
|
+
});
|
|
2505
|
+
const node = new ToolNode({
|
|
2506
|
+
tools: [subagentTool],
|
|
2507
|
+
hookRegistry: registry,
|
|
2508
|
+
directToolNames: new Set([Constants.SUBAGENT]),
|
|
2509
|
+
humanInTheLoop: { enabled: true },
|
|
2510
|
+
});
|
|
2511
|
+
const graph = buildHITLGraph(node, [
|
|
2512
|
+
{
|
|
2513
|
+
id: 'call_subagent',
|
|
2514
|
+
name: Constants.SUBAGENT,
|
|
2515
|
+
args: { description: 'inspect logs', subagent_type: 'researcher' },
|
|
2516
|
+
},
|
|
2517
|
+
]);
|
|
2518
|
+
|
|
2519
|
+
const interrupted = await graph.invoke(
|
|
2520
|
+
{ messages: [] },
|
|
2521
|
+
{
|
|
2522
|
+
configurable: {
|
|
2523
|
+
thread_id: 'parent-subagent-thread',
|
|
2524
|
+
run_id: runId,
|
|
2525
|
+
},
|
|
2526
|
+
}
|
|
2527
|
+
);
|
|
2528
|
+
|
|
2529
|
+
if (!isInterrupted<t.ToolApprovalInterruptPayload>(interrupted)) {
|
|
2530
|
+
throw new Error('expected a subagent approval interrupt');
|
|
2531
|
+
}
|
|
2532
|
+
expect(interrupted.__interrupt__[0].value).toMatchObject({
|
|
2533
|
+
type: 'tool_approval',
|
|
2534
|
+
hook_session_id: runId,
|
|
2535
|
+
action_requests: [
|
|
2536
|
+
{ tool_call_id: 'call_subagent', name: Constants.SUBAGENT },
|
|
2537
|
+
],
|
|
2538
|
+
});
|
|
2539
|
+
expect(executeSubagent).not.toHaveBeenCalled();
|
|
2540
|
+
});
|
|
2541
|
+
|
|
2313
2542
|
it('denied tool in a deny+ask batch dispatches ON_RUN_STEP_COMPLETED exactly once across interrupt + resume', async () => {
|
|
2314
2543
|
const stepCompletedDispatches: string[] = [];
|
|
2315
2544
|
/** Spy on the underlying custom event dispatcher to capture every
|