@librechat/agents 3.4.1 → 3.4.2
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/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +57 -33
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +81 -33
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs +14 -0
- package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs.map +1 -0
- package/dist/cjs/graphs/createGraph.cjs +15 -0
- package/dist/cjs/graphs/createGraph.cjs.map +1 -0
- package/dist/cjs/graphs/index.cjs +1 -0
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +1 -1
- package/dist/cjs/main.cjs +20 -11
- package/dist/cjs/run.cjs +48 -38
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/JsonlSessionStore.cjs +2 -2
- package/dist/cjs/tools/SubagentTool.cjs +4 -4
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +13 -9
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/eagerEventExecution.cjs +1 -0
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +3 -3
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +2 -2
- package/dist/cjs/tools/search/rerankers.cjs +154 -1
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +4 -1
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +601 -0
- package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +672 -343
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +3 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
- package/dist/cjs/tools/subagent/childGraphConfig.cjs +278 -0
- package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -0
- package/dist/cjs/tools/subagent/index.cjs +1 -0
- package/dist/cjs/tools/subagent/runtimeLimits.cjs +5 -0
- package/dist/cjs/tools/subagent/runtimeLimits.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +57 -33
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +81 -33
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/graphs/applyGraphRuntimeConfig.mjs +14 -0
- package/dist/esm/graphs/applyGraphRuntimeConfig.mjs.map +1 -0
- package/dist/esm/graphs/createGraph.mjs +15 -0
- package/dist/esm/graphs/createGraph.mjs.map +1 -0
- package/dist/esm/graphs/index.mjs +1 -0
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs +1 -1
- package/dist/esm/main.mjs +12 -10
- package/dist/esm/run.mjs +48 -38
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/JsonlSessionStore.mjs +2 -2
- package/dist/esm/tools/SubagentTool.mjs +4 -4
- package/dist/esm/tools/SubagentTool.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +13 -9
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/eagerEventExecution.mjs +1 -1
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +3 -3
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +2 -2
- package/dist/esm/tools/search/rerankers.mjs +154 -1
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +4 -1
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +597 -0
- package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +672 -343
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +3 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
- package/dist/esm/tools/subagent/childGraphConfig.mjs +272 -0
- package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -0
- package/dist/esm/tools/subagent/index.mjs +1 -0
- package/dist/esm/tools/subagent/runtimeLimits.mjs +5 -0
- package/dist/esm/tools/subagent/runtimeLimits.mjs.map +1 -0
- package/dist/types/agents/AgentContext.d.ts +1 -1
- package/dist/types/graphs/Graph.d.ts +7 -2
- package/dist/types/graphs/MultiAgentGraph.d.ts +7 -1
- package/dist/types/graphs/applyGraphRuntimeConfig.d.ts +3 -0
- package/dist/types/graphs/createGraph.d.ts +13 -0
- package/dist/types/graphs/graphFactory.d.ts +13 -0
- package/dist/types/graphs/index.d.ts +2 -0
- package/dist/types/run.d.ts +2 -2
- package/dist/types/tools/SubagentTool.d.ts +1 -1
- package/dist/types/tools/search/rerankers.d.ts +28 -0
- package/dist/types/tools/search/types.d.ts +38 -1
- package/dist/types/tools/subagent/SubagentExecutionRegistry.d.ts +171 -0
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +32 -37
- package/dist/types/tools/subagent/SubagentReplay.d.ts +5 -1
- package/dist/types/tools/subagent/childGraphConfig.d.ts +37 -0
- package/dist/types/tools/subagent/index.d.ts +1 -1
- package/dist/types/tools/subagent/runtimeLimits.d.ts +4 -0
- package/dist/types/types/graph.d.ts +184 -19
- package/package.json +3 -1
- package/src/agents/AgentContext.ts +1 -1
- package/src/graphs/Graph.ts +111 -60
- package/src/graphs/MultiAgentGraph.ts +149 -47
- package/src/graphs/__tests__/MultiAgentGraph.test.ts +86 -0
- package/src/graphs/__tests__/composition.smoke.test.ts +152 -0
- package/src/graphs/__tests__/createGraph.test.ts +206 -0
- package/src/graphs/applyGraphRuntimeConfig.ts +25 -0
- package/src/graphs/createGraph.ts +32 -0
- package/src/graphs/graphFactory.ts +12 -0
- package/src/graphs/index.ts +2 -0
- package/src/run.ts +50 -38
- package/src/specs/graph-subagent.live.test.ts +242 -0
- package/src/specs/graph-subagent.test.ts +322 -0
- package/src/specs/langfuse-routing.integration.test.ts +120 -1
- package/src/specs/subagent.test.ts +130 -4
- package/src/tools/SubagentTool.ts +4 -4
- package/src/tools/ToolNode.ts +22 -10
- package/src/tools/__tests__/SubagentExecutor.test.ts +198 -9
- package/src/tools/__tests__/SubagentReplay.test.ts +29 -0
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +28 -2
- package/src/tools/__tests__/subagentHooks.test.ts +90 -3
- package/src/tools/search/rag-api-reranker.test.ts +891 -0
- package/src/tools/search/rerankers.ts +260 -0
- package/src/tools/search/tool.ts +6 -0
- package/src/tools/search/types.ts +45 -1
- package/src/tools/subagent/SubagentExecutionRegistry.ts +1159 -0
- package/src/tools/subagent/SubagentExecutor.ts +1245 -563
- package/src/tools/subagent/SubagentReplay.ts +10 -1
- package/src/tools/subagent/__tests__/SubagentExecutionRegistry.test.ts +637 -0
- package/src/tools/subagent/__tests__/SubagentExecutor.lazy.test.ts +3054 -0
- package/src/tools/subagent/__tests__/childGraphConfig.test.ts +521 -0
- package/src/tools/subagent/childGraphConfig.ts +694 -0
- package/src/tools/subagent/index.ts +6 -0
- package/src/tools/subagent/runtimeLimits.ts +7 -0
- package/src/types/graph.ts +217 -19
package/src/tools/ToolNode.ts
CHANGED
|
@@ -205,6 +205,22 @@ type RunToolBatchContext<T = unknown> = {
|
|
|
205
205
|
errorOwnership?: ToolErrorOwnership;
|
|
206
206
|
};
|
|
207
207
|
|
|
208
|
+
function withSubagentReplayBatch(
|
|
209
|
+
config: RunnableConfig,
|
|
210
|
+
replayBatchKey: string | undefined
|
|
211
|
+
): RunnableConfig {
|
|
212
|
+
if (replayBatchKey == null) {
|
|
213
|
+
return config;
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
...config,
|
|
217
|
+
configurable: {
|
|
218
|
+
...config.configurable,
|
|
219
|
+
[SUBAGENT_PARENT_BATCH_CONFIG_KEY]: replayBatchKey,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
208
224
|
type SettledDirectToolResult = {
|
|
209
225
|
output: BaseMessage | Command;
|
|
210
226
|
additionalContexts: string[];
|
|
@@ -1632,15 +1648,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1632
1648
|
this.toolMap.get(call.name) as ReplayableSubagentTool | undefined
|
|
1633
1649
|
)?.[SUBAGENT_REPLAY_CONTROLLER];
|
|
1634
1650
|
const replayConfig =
|
|
1635
|
-
replayController == null
|
|
1651
|
+
replayController == null
|
|
1636
1652
|
? config
|
|
1637
|
-
:
|
|
1638
|
-
...config,
|
|
1639
|
-
configurable: {
|
|
1640
|
-
...config.configurable,
|
|
1641
|
-
[SUBAGENT_PARENT_BATCH_CONFIG_KEY]: batchContext.replayBatchKey,
|
|
1642
|
-
},
|
|
1643
|
-
};
|
|
1653
|
+
: withSubagentReplayBatch(config, batchContext.replayBatchKey);
|
|
1644
1654
|
const settledOutput = await replayController?.getSettledOutput(
|
|
1645
1655
|
call,
|
|
1646
1656
|
replayConfig
|
|
@@ -4166,8 +4176,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
4166
4176
|
SubagentResumeManifest['executions'][number]
|
|
4167
4177
|
>();
|
|
4168
4178
|
for (const controller of controllers) {
|
|
4169
|
-
const manifest =
|
|
4170
|
-
|
|
4179
|
+
const manifest = await controller.getResumeManifest?.(
|
|
4180
|
+
parentToolCallIds,
|
|
4181
|
+
withSubagentReplayBatch(config, baseContext.replayBatchKey)
|
|
4182
|
+
);
|
|
4171
4183
|
if (manifest != null) {
|
|
4172
4184
|
for (const execution of manifest.executions) {
|
|
4173
4185
|
executionsByParentCall.set(execution.parentToolCallId, execution);
|
|
@@ -4,7 +4,9 @@ import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
|
|
|
4
4
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
5
5
|
import type {
|
|
6
6
|
AgentInputs,
|
|
7
|
+
GraphSubagentConfig,
|
|
7
8
|
ResolvedSubagentConfig,
|
|
9
|
+
ResolvedSingleAgentSubagentConfig,
|
|
8
10
|
StandardGraphInput,
|
|
9
11
|
SubagentUpdateEvent,
|
|
10
12
|
SubagentUsageEvent,
|
|
@@ -14,6 +16,7 @@ import type {
|
|
|
14
16
|
import type { StandardGraph } from '@/graphs/Graph';
|
|
15
17
|
import {
|
|
16
18
|
SubagentExecutor,
|
|
19
|
+
filterGraphSubagentResult,
|
|
17
20
|
filterSubagentResult,
|
|
18
21
|
resolveSubagentConfigs,
|
|
19
22
|
buildChildInputs,
|
|
@@ -39,8 +42,8 @@ const makeChildInputs = (agentId = 'child-agent'): AgentInputs => ({
|
|
|
39
42
|
|
|
40
43
|
const makeConfig = (
|
|
41
44
|
type = 'researcher',
|
|
42
|
-
overrides: Partial<
|
|
43
|
-
):
|
|
45
|
+
overrides: Partial<ResolvedSingleAgentSubagentConfig> = {}
|
|
46
|
+
): ResolvedSingleAgentSubagentConfig => ({
|
|
44
47
|
type,
|
|
45
48
|
name: 'Test Researcher',
|
|
46
49
|
description: 'Researches things',
|
|
@@ -49,6 +52,18 @@ const makeConfig = (
|
|
|
49
52
|
});
|
|
50
53
|
|
|
51
54
|
describe('filterSubagentResult', () => {
|
|
55
|
+
it('treats a result member with no new AI message as textless completion', () => {
|
|
56
|
+
expect(
|
|
57
|
+
filterGraphSubagentResult(
|
|
58
|
+
{
|
|
59
|
+
messages: [new AIMessage('worker output')],
|
|
60
|
+
subagentResult: { agentId: 'result' },
|
|
61
|
+
},
|
|
62
|
+
'result'
|
|
63
|
+
)
|
|
64
|
+
).toBe('Task completed');
|
|
65
|
+
});
|
|
66
|
+
|
|
52
67
|
it('extracts text from last AIMessage string content', () => {
|
|
53
68
|
const messages: BaseMessage[] = [
|
|
54
69
|
new HumanMessage('task'),
|
|
@@ -220,7 +235,8 @@ describe('resolveSubagentConfigs', () => {
|
|
|
220
235
|
const parentContext = AgentContext.fromConfig(parentInputs);
|
|
221
236
|
const resolved = resolveSubagentConfigs([config], parentContext);
|
|
222
237
|
expect(resolved).toHaveLength(1);
|
|
223
|
-
|
|
238
|
+
const resolvedConfig = resolved[0];
|
|
239
|
+
expect(resolvedConfig.agentInputs.agentId).toBe('child-agent');
|
|
224
240
|
});
|
|
225
241
|
|
|
226
242
|
it('resolves self-spawn from parent _sourceInputs', () => {
|
|
@@ -233,8 +249,9 @@ describe('resolveSubagentConfigs', () => {
|
|
|
233
249
|
const parentContext = AgentContext.fromConfig(parentInputs);
|
|
234
250
|
const resolved = resolveSubagentConfigs([selfConfig], parentContext);
|
|
235
251
|
expect(resolved).toHaveLength(1);
|
|
236
|
-
|
|
237
|
-
expect(
|
|
252
|
+
const resolvedConfig = resolved[0];
|
|
253
|
+
expect(resolvedConfig.agentInputs.provider).toBe(Providers.OPENAI);
|
|
254
|
+
expect(resolvedConfig.agentInputs.instructions).toBe(
|
|
238
255
|
'You are a parent agent.'
|
|
239
256
|
);
|
|
240
257
|
});
|
|
@@ -496,6 +513,55 @@ describe('SubagentExecutor', () => {
|
|
|
496
513
|
expect(result.messages).toEqual([]);
|
|
497
514
|
});
|
|
498
515
|
|
|
516
|
+
it('fails graph HITL before factories, hooks, events, or usage can run', async () => {
|
|
517
|
+
const graphConfig: GraphSubagentConfig = {
|
|
518
|
+
kind: 'graph',
|
|
519
|
+
type: 'graph-team',
|
|
520
|
+
name: 'Graph Team',
|
|
521
|
+
description: 'Runs a graph.',
|
|
522
|
+
agents: [makeChildInputs('graph-member')],
|
|
523
|
+
edges: [],
|
|
524
|
+
entryAgentId: 'graph-member',
|
|
525
|
+
resultAgentId: 'graph-member',
|
|
526
|
+
};
|
|
527
|
+
const createChildGraph = jest.fn(makeNoopGraphFactory());
|
|
528
|
+
const createChildGraphByKind = jest.fn(
|
|
529
|
+
(): StandardGraph => makeNoopGraphFactory()()
|
|
530
|
+
);
|
|
531
|
+
const hook = jest.fn(async (): Promise<Record<string, never>> => ({}));
|
|
532
|
+
const hookRegistry = new HookRegistry();
|
|
533
|
+
hookRegistry.register('SubagentStart', { hooks: [hook] });
|
|
534
|
+
const update = jest.fn();
|
|
535
|
+
const handlerRegistry = new HandlerRegistry();
|
|
536
|
+
handlerRegistry.register(GraphEvents.ON_SUBAGENT_UPDATE, {
|
|
537
|
+
handle: update,
|
|
538
|
+
});
|
|
539
|
+
const usageSink = jest.fn();
|
|
540
|
+
const executor = createExecutor({
|
|
541
|
+
configs: new Map([[graphConfig.type, graphConfig]]),
|
|
542
|
+
humanInTheLoop: { enabled: true },
|
|
543
|
+
createChildGraph,
|
|
544
|
+
createChildGraphByKind,
|
|
545
|
+
hookRegistry,
|
|
546
|
+
parentHandlerRegistry: handlerRegistry,
|
|
547
|
+
usageSink,
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
const result = await executor.execute({
|
|
551
|
+
description: 'Do not run.',
|
|
552
|
+
subagentType: graphConfig.type,
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
expect(result.content).toBe(
|
|
556
|
+
'Error: Human-in-the-loop execution is not yet supported for graph subagents.'
|
|
557
|
+
);
|
|
558
|
+
expect(createChildGraph).not.toHaveBeenCalled();
|
|
559
|
+
expect(createChildGraphByKind).not.toHaveBeenCalled();
|
|
560
|
+
expect(hook).not.toHaveBeenCalled();
|
|
561
|
+
expect(update).not.toHaveBeenCalled();
|
|
562
|
+
expect(usageSink).not.toHaveBeenCalled();
|
|
563
|
+
});
|
|
564
|
+
|
|
499
565
|
it('fails closed when resumable execution has no parent tool call ID', async () => {
|
|
500
566
|
const createChildGraph = jest.fn(makeNoopGraphFactory());
|
|
501
567
|
const executor = createExecutor({
|
|
@@ -2558,6 +2624,107 @@ describe('SubagentExecutor', () => {
|
|
|
2558
2624
|
expect(phases[phases.length - 1]).toBe('stop');
|
|
2559
2625
|
});
|
|
2560
2626
|
|
|
2627
|
+
it('does not let a stalled observational update handler block child completion', async () => {
|
|
2628
|
+
jest.useFakeTimers();
|
|
2629
|
+
try {
|
|
2630
|
+
const registry = new HandlerRegistry();
|
|
2631
|
+
const updateHandler = jest.fn(
|
|
2632
|
+
(): Promise<void> => new Promise<void>(() => {})
|
|
2633
|
+
);
|
|
2634
|
+
registry.register(GraphEvents.ON_SUBAGENT_UPDATE, {
|
|
2635
|
+
handle: updateHandler,
|
|
2636
|
+
});
|
|
2637
|
+
const { factory } = makeStubGraphFactory({
|
|
2638
|
+
messages: [new AIMessage('ok')],
|
|
2639
|
+
});
|
|
2640
|
+
const executor = createExecutor({
|
|
2641
|
+
createChildGraph: factory,
|
|
2642
|
+
parentHandlerRegistry: registry,
|
|
2643
|
+
});
|
|
2644
|
+
|
|
2645
|
+
const execution = executor.execute({
|
|
2646
|
+
description: 'Task',
|
|
2647
|
+
subagentType: 'researcher',
|
|
2648
|
+
});
|
|
2649
|
+
const outcome = Promise.race([
|
|
2650
|
+
execution.then(({ content }) => content),
|
|
2651
|
+
new Promise<string>((resolve) =>
|
|
2652
|
+
setTimeout(() => resolve('stalled'), 20_000)
|
|
2653
|
+
),
|
|
2654
|
+
]);
|
|
2655
|
+
|
|
2656
|
+
await jest.advanceTimersByTimeAsync(20_000);
|
|
2657
|
+
|
|
2658
|
+
await expect(outcome).resolves.toBe('ok');
|
|
2659
|
+
expect(updateHandler).toHaveBeenCalledTimes(2);
|
|
2660
|
+
} finally {
|
|
2661
|
+
jest.useRealTimers();
|
|
2662
|
+
}
|
|
2663
|
+
});
|
|
2664
|
+
|
|
2665
|
+
it('does not let a stalled queued update block the terminal stop envelope', async () => {
|
|
2666
|
+
jest.useFakeTimers();
|
|
2667
|
+
try {
|
|
2668
|
+
const phases: SubagentUpdateEvent['phase'][] = [];
|
|
2669
|
+
const registry = new HandlerRegistry();
|
|
2670
|
+
registry.register(GraphEvents.ON_SUBAGENT_UPDATE, {
|
|
2671
|
+
handle: (_event, rawData): void | Promise<void> => {
|
|
2672
|
+
const update = rawData as SubagentUpdateEvent;
|
|
2673
|
+
phases.push(update.phase);
|
|
2674
|
+
if (update.phase === 'run_step') {
|
|
2675
|
+
return new Promise<void>(() => {});
|
|
2676
|
+
}
|
|
2677
|
+
},
|
|
2678
|
+
});
|
|
2679
|
+
const factory: () => StandardGraph = (): StandardGraph =>
|
|
2680
|
+
({
|
|
2681
|
+
createWorkflow: (): { invoke: jest.Mock } => ({
|
|
2682
|
+
invoke: jest.fn().mockImplementation(async (_state, options) => {
|
|
2683
|
+
const opts = options as { callbacks?: unknown[] };
|
|
2684
|
+
const forwarder = (opts.callbacks ?? [])[0] as {
|
|
2685
|
+
handleCustomEvent?: (
|
|
2686
|
+
eventName: string,
|
|
2687
|
+
data: unknown
|
|
2688
|
+
) => Promise<void> | void;
|
|
2689
|
+
};
|
|
2690
|
+
for (let index = 0; index < 80; index++) {
|
|
2691
|
+
await forwarder.handleCustomEvent?.(GraphEvents.ON_RUN_STEP, {
|
|
2692
|
+
id: `step_${index}`,
|
|
2693
|
+
type: StepTypes.MESSAGE_CREATION,
|
|
2694
|
+
agentId: 'researcher',
|
|
2695
|
+
index,
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
return { messages: [new AIMessage('ok')] };
|
|
2699
|
+
}),
|
|
2700
|
+
}),
|
|
2701
|
+
clearHeavyState: jest.fn(),
|
|
2702
|
+
}) as unknown as StandardGraph;
|
|
2703
|
+
const executor = createExecutor({
|
|
2704
|
+
createChildGraph: factory,
|
|
2705
|
+
parentHandlerRegistry: registry,
|
|
2706
|
+
});
|
|
2707
|
+
|
|
2708
|
+
const execution = executor.execute({
|
|
2709
|
+
description: 'Task',
|
|
2710
|
+
subagentType: 'researcher',
|
|
2711
|
+
});
|
|
2712
|
+
const outcome = Promise.race([
|
|
2713
|
+
execution.then(({ content }) => content),
|
|
2714
|
+
new Promise<string>((resolve) =>
|
|
2715
|
+
setTimeout(() => resolve('stalled'), 20_000)
|
|
2716
|
+
),
|
|
2717
|
+
]);
|
|
2718
|
+
|
|
2719
|
+
await jest.advanceTimersByTimeAsync(20_000);
|
|
2720
|
+
|
|
2721
|
+
await expect(outcome).resolves.toBe('ok');
|
|
2722
|
+
expect(phases).toEqual(['start', 'run_step', 'stop']);
|
|
2723
|
+
} finally {
|
|
2724
|
+
jest.useRealTimers();
|
|
2725
|
+
}
|
|
2726
|
+
});
|
|
2727
|
+
|
|
2561
2728
|
it('allowlists forwarded run step payloads before wrapping them in ON_SUBAGENT_UPDATE', async () => {
|
|
2562
2729
|
const subagentUpdates: SubagentUpdateEvent[] = [];
|
|
2563
2730
|
const registry = new HandlerRegistry();
|
|
@@ -2685,18 +2852,32 @@ describe('SubagentExecutor', () => {
|
|
|
2685
2852
|
expect(serialized).not.toContain('nested-completed-secret');
|
|
2686
2853
|
});
|
|
2687
2854
|
|
|
2688
|
-
it('
|
|
2855
|
+
it('bounds queued updates under overload while preserving lifecycle envelopes', async () => {
|
|
2856
|
+
const phases: SubagentUpdateEvent['phase'][] = [];
|
|
2689
2857
|
const completedIds: string[] = [];
|
|
2858
|
+
let releaseFirstCompleted!: () => void;
|
|
2859
|
+
const firstCompletedBlocked = new Promise<void>((resolve) => {
|
|
2860
|
+
releaseFirstCompleted = resolve;
|
|
2861
|
+
});
|
|
2862
|
+
let markAllEmitted!: () => void;
|
|
2863
|
+
const allEmitted = new Promise<void>((resolve) => {
|
|
2864
|
+
markAllEmitted = resolve;
|
|
2865
|
+
});
|
|
2866
|
+
let blockedFirstCompleted = false;
|
|
2690
2867
|
const registry = new HandlerRegistry();
|
|
2691
2868
|
registry.register(GraphEvents.ON_SUBAGENT_UPDATE, {
|
|
2692
2869
|
handle: async (_event, rawData): Promise<void> => {
|
|
2693
2870
|
const update = rawData as SubagentUpdateEvent;
|
|
2871
|
+
phases.push(update.phase);
|
|
2694
2872
|
if (update.phase === 'run_step_completed') {
|
|
2695
2873
|
const data = update.data as { result?: { id?: string } };
|
|
2696
2874
|
if (data.result?.id != null) {
|
|
2697
2875
|
completedIds.push(data.result.id);
|
|
2698
2876
|
}
|
|
2699
|
-
|
|
2877
|
+
if (!blockedFirstCompleted) {
|
|
2878
|
+
blockedFirstCompleted = true;
|
|
2879
|
+
await firstCompletedBlocked;
|
|
2880
|
+
}
|
|
2700
2881
|
}
|
|
2701
2882
|
},
|
|
2702
2883
|
});
|
|
@@ -2731,6 +2912,7 @@ describe('SubagentExecutor', () => {
|
|
|
2731
2912
|
}
|
|
2732
2913
|
);
|
|
2733
2914
|
}
|
|
2915
|
+
markAllEmitted();
|
|
2734
2916
|
return { messages: [new AIMessage('ok')] };
|
|
2735
2917
|
}),
|
|
2736
2918
|
}),
|
|
@@ -2742,14 +2924,21 @@ describe('SubagentExecutor', () => {
|
|
|
2742
2924
|
parentHandlerRegistry: registry,
|
|
2743
2925
|
});
|
|
2744
2926
|
|
|
2745
|
-
|
|
2927
|
+
const execution = executor.execute({
|
|
2746
2928
|
description: 'Task',
|
|
2747
2929
|
subagentType: 'researcher',
|
|
2748
2930
|
});
|
|
2931
|
+
await allEmitted;
|
|
2932
|
+
releaseFirstCompleted();
|
|
2933
|
+
await execution;
|
|
2749
2934
|
|
|
2750
|
-
expect(completedIds).toHaveLength(
|
|
2935
|
+
expect(completedIds).toHaveLength(65);
|
|
2751
2936
|
expect(completedIds[0]).toBe('step_0');
|
|
2937
|
+
expect(completedIds).not.toContain('step_1');
|
|
2938
|
+
expect(completedIds).toContain('step_16');
|
|
2752
2939
|
expect(completedIds[completedIds.length - 1]).toBe('step_79');
|
|
2940
|
+
expect(phases[0]).toBe('start');
|
|
2941
|
+
expect(phases[phases.length - 1]).toBe('stop');
|
|
2753
2942
|
});
|
|
2754
2943
|
|
|
2755
2944
|
it('does NOT forward ON_TOOL_EXECUTE when the parent registry has no handler (safe fallback)', async () => {
|
|
@@ -9,6 +9,8 @@ describe('SubagentReplay manifest', () => {
|
|
|
9
9
|
const execution = {
|
|
10
10
|
parentToolCallId: 'call_parent',
|
|
11
11
|
childRunId: 'child-run',
|
|
12
|
+
subagentType: 'researcher',
|
|
13
|
+
configId: 'researcher@v1',
|
|
12
14
|
approvalExecutionScope: 'child-approval-scope',
|
|
13
15
|
checkpoints: [
|
|
14
16
|
{
|
|
@@ -82,6 +84,33 @@ describe('SubagentReplay manifest', () => {
|
|
|
82
84
|
});
|
|
83
85
|
});
|
|
84
86
|
|
|
87
|
+
it('accepts legacy executions without a bound subagent type', () => {
|
|
88
|
+
const { subagentType: _subagentType, ...legacyExecution } = execution;
|
|
89
|
+
const manifest = { version: 1 as const, executions: [legacyExecution] };
|
|
90
|
+
const payload = attachSubagentResumeManifest(
|
|
91
|
+
{ type: 'tool_approval' },
|
|
92
|
+
manifest
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(getSubagentResumeManifest(payload)).toEqual(manifest);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it.each(['', 42, { type: 'researcher' }])(
|
|
99
|
+
'rejects malformed bound subagent type %#',
|
|
100
|
+
(subagentType) => {
|
|
101
|
+
const manifest = {
|
|
102
|
+
version: 1 as const,
|
|
103
|
+
executions: [{ ...execution, subagentType }],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
expect(
|
|
107
|
+
getSubagentResumeManifest({
|
|
108
|
+
__librechat_subagent_resume_manifest: manifest,
|
|
109
|
+
})
|
|
110
|
+
).toBeUndefined();
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
85
114
|
it.each([
|
|
86
115
|
['string', 'approve this child'],
|
|
87
116
|
['number', 42],
|
|
@@ -138,9 +138,21 @@ describe('direct-path HITL: resume scope', () => {
|
|
|
138
138
|
};
|
|
139
139
|
const replayableTool = directTool as StructuredToolInterface &
|
|
140
140
|
ReplayableSubagentTool;
|
|
141
|
+
const getResumeManifest = jest.fn(
|
|
142
|
+
async (
|
|
143
|
+
_parentToolCallIds?: ReadonlySet<string>,
|
|
144
|
+
_config?: RunnableConfig
|
|
145
|
+
) => manifest
|
|
146
|
+
);
|
|
147
|
+
const getSettledOutput = jest.fn(
|
|
148
|
+
async (
|
|
149
|
+
_call: ToolCall,
|
|
150
|
+
_config: RunnableConfig
|
|
151
|
+
): Promise<SettledSubagentToolOutput | undefined> => undefined
|
|
152
|
+
);
|
|
141
153
|
replayableTool[SUBAGENT_REPLAY_CONTROLLER] = {
|
|
142
|
-
getResumeManifest
|
|
143
|
-
getSettledOutput
|
|
154
|
+
getResumeManifest,
|
|
155
|
+
getSettledOutput,
|
|
144
156
|
persistSettledOutput: async () => {},
|
|
145
157
|
};
|
|
146
158
|
const node = new ToolNode({
|
|
@@ -166,6 +178,20 @@ describe('direct-path HITL: resume scope', () => {
|
|
|
166
178
|
throw new Error('expected primitive child interrupt');
|
|
167
179
|
}
|
|
168
180
|
const internalPayload = interrupted.__interrupt__[0].value;
|
|
181
|
+
const replayBatchKey =
|
|
182
|
+
getSettledOutput.mock.calls[0]?.[1].configurable?.[
|
|
183
|
+
SUBAGENT_PARENT_BATCH_CONFIG_KEY
|
|
184
|
+
];
|
|
185
|
+
expect(replayBatchKey).toEqual(expect.any(String));
|
|
186
|
+
expect(getResumeManifest).toHaveBeenCalledWith(
|
|
187
|
+
new Set(['call_primitive']),
|
|
188
|
+
expect.objectContaining({
|
|
189
|
+
configurable: expect.objectContaining({
|
|
190
|
+
thread_id: 'parent-thread',
|
|
191
|
+
[SUBAGENT_PARENT_BATCH_CONFIG_KEY]: replayBatchKey,
|
|
192
|
+
}),
|
|
193
|
+
})
|
|
194
|
+
);
|
|
169
195
|
expect(getSubagentResumeManifest(internalPayload)).toEqual(manifest);
|
|
170
196
|
expect(stripSubagentResumeManifest(internalPayload)).toBe('confirm child');
|
|
171
197
|
});
|
|
@@ -496,6 +496,60 @@ describe('Subagent hook integration (end-to-end via Run)', () => {
|
|
|
496
496
|
expect(captured!.messages.length).toBeGreaterThan(0);
|
|
497
497
|
});
|
|
498
498
|
|
|
499
|
+
it('propagates a caller-only Run signal into lazy subagent resolution', async () => {
|
|
500
|
+
const callerAbort = new AbortController();
|
|
501
|
+
let resolverSignal: AbortSignal | undefined;
|
|
502
|
+
let markResolverStarted = (): void => undefined;
|
|
503
|
+
const resolverStarted = new Promise<void>((resolve) => {
|
|
504
|
+
markResolverStarted = resolve;
|
|
505
|
+
});
|
|
506
|
+
let releaseResolver = (): void => undefined;
|
|
507
|
+
const resolverRelease = new Promise<void>((resolve) => {
|
|
508
|
+
releaseResolver = resolve;
|
|
509
|
+
});
|
|
510
|
+
const parent = createParentAgent();
|
|
511
|
+
const eagerChild = parent.subagentConfigs?.[0]?.agentInputs;
|
|
512
|
+
if (eagerChild == null) {
|
|
513
|
+
throw new Error('Expected eager child inputs.');
|
|
514
|
+
}
|
|
515
|
+
parent.subagentConfigs = [
|
|
516
|
+
{
|
|
517
|
+
type: 'researcher',
|
|
518
|
+
name: 'Researcher',
|
|
519
|
+
description: 'Researches topics',
|
|
520
|
+
configId: 'caller-signal-researcher@v1',
|
|
521
|
+
resolveAgentInputs: async (context) => {
|
|
522
|
+
resolverSignal = context.signal;
|
|
523
|
+
markResolverStarted();
|
|
524
|
+
await resolverRelease;
|
|
525
|
+
return eagerChild;
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
];
|
|
529
|
+
const run = await Run.create<t.IState>({
|
|
530
|
+
runId: `subagent-caller-signal-${Date.now()}`,
|
|
531
|
+
graphConfig: { type: 'standard', agents: [parent] },
|
|
532
|
+
returnContent: true,
|
|
533
|
+
skipCleanup: true,
|
|
534
|
+
});
|
|
535
|
+
run.Graph!.overrideTestModel(['Delegating...', 'Caller cancelled.'], 5, [
|
|
536
|
+
makeSubagentToolCall('call_caller_signal'),
|
|
537
|
+
]);
|
|
538
|
+
|
|
539
|
+
const processing = run.processStream(
|
|
540
|
+
{ messages: [new HumanMessage('cancel this delegation')] },
|
|
541
|
+
{ ...callerConfig, signal: callerAbort.signal }
|
|
542
|
+
);
|
|
543
|
+
await resolverStarted;
|
|
544
|
+
callerAbort.abort(new Error('caller cancelled'));
|
|
545
|
+
await Promise.resolve();
|
|
546
|
+
const resolverWasAborted = resolverSignal?.aborted === true;
|
|
547
|
+
releaseResolver();
|
|
548
|
+
await processing.catch(() => undefined);
|
|
549
|
+
|
|
550
|
+
expect(resolverWasAborted).toBe(true);
|
|
551
|
+
});
|
|
552
|
+
|
|
499
553
|
it('SubagentStart deny blocks subagent execution and returns blocked message', async () => {
|
|
500
554
|
const registry = new HookRegistry();
|
|
501
555
|
const denyHook: HookCallback<
|
|
@@ -1084,14 +1138,37 @@ describe('Subagent hook integration (end-to-end via Run)', () => {
|
|
|
1084
1138
|
schema: z.object({}),
|
|
1085
1139
|
}
|
|
1086
1140
|
);
|
|
1141
|
+
const resolutionContexts: t.SubagentResolveContext[] = [];
|
|
1142
|
+
const createLazyParent = (): t.AgentInputs => {
|
|
1143
|
+
const parent = createParentAgentWithPrimitiveInterruptTool(
|
|
1144
|
+
primitiveInterruptTool
|
|
1145
|
+
);
|
|
1146
|
+
const child = parent.subagentConfigs?.[0];
|
|
1147
|
+
const childInputs = child?.agentInputs;
|
|
1148
|
+
if (child == null || childInputs == null) {
|
|
1149
|
+
throw new Error('Expected a child agent configuration.');
|
|
1150
|
+
}
|
|
1151
|
+
return {
|
|
1152
|
+
...parent,
|
|
1153
|
+
subagentConfigs: [
|
|
1154
|
+
{
|
|
1155
|
+
...child,
|
|
1156
|
+
agentInputs: undefined,
|
|
1157
|
+
configId: 'primitive-interrupt-child@v1',
|
|
1158
|
+
resolveAgentInputs: async (context) => {
|
|
1159
|
+
resolutionContexts.push(context);
|
|
1160
|
+
return childInputs;
|
|
1161
|
+
},
|
|
1162
|
+
},
|
|
1163
|
+
],
|
|
1164
|
+
};
|
|
1165
|
+
};
|
|
1087
1166
|
const createRun = (currentRunId: string): Promise<Run<t.IState>> =>
|
|
1088
1167
|
Run.create<t.IState>({
|
|
1089
1168
|
runId: currentRunId,
|
|
1090
1169
|
graphConfig: {
|
|
1091
1170
|
type: 'standard',
|
|
1092
|
-
agents: [
|
|
1093
|
-
createParentAgentWithPrimitiveInterruptTool(primitiveInterruptTool),
|
|
1094
|
-
],
|
|
1171
|
+
agents: [createLazyParent()],
|
|
1095
1172
|
compileOptions: { checkpointer },
|
|
1096
1173
|
},
|
|
1097
1174
|
returnContent: true,
|
|
@@ -1140,6 +1217,16 @@ describe('Subagent hook integration (end-to-end via Run)', () => {
|
|
|
1140
1217
|
);
|
|
1141
1218
|
|
|
1142
1219
|
expect(rebuiltRun.getInterrupt()).toBeUndefined();
|
|
1220
|
+
expect(resolutionContexts).toHaveLength(2);
|
|
1221
|
+
expect(resolutionContexts[0].executionId).toBe(
|
|
1222
|
+
resolutionContexts[1].executionId
|
|
1223
|
+
);
|
|
1224
|
+
expect(resolutionContexts[0].descriptor.configId).toBe(
|
|
1225
|
+
'primitive-interrupt-child@v1'
|
|
1226
|
+
);
|
|
1227
|
+
expect(resolutionContexts[1].descriptor.configId).toBe(
|
|
1228
|
+
'primitive-interrupt-child@v1'
|
|
1229
|
+
);
|
|
1143
1230
|
expect(resumedValues).toEqual(['approved after restart']);
|
|
1144
1231
|
expect(JSON.stringify(rebuiltContent)).toContain('Final answer.');
|
|
1145
1232
|
expect(JSON.stringify(rebuiltContent)).not.toContain('host edit on resume');
|