@librechat/agents 3.3.13 → 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 +121 -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/anthropic/index.cjs +35 -206
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +121 -241
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/google/index.cjs +19 -12
- package/dist/cjs/llm/google/index.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +3 -3
- package/dist/cjs/llm/invoke.cjs +2 -2
- package/dist/cjs/llm/mistral/index.cjs +26 -0
- package/dist/cjs/llm/mistral/index.cjs.map +1 -0
- package/dist/cjs/llm/openai/index.cjs +82 -80
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/providers.cjs +19 -19
- package/dist/cjs/llm/providers.cjs.map +1 -1
- package/dist/cjs/llm/stream/chunkAdapters.cjs +198 -0
- package/dist/cjs/llm/stream/chunkAdapters.cjs.map +1 -0
- package/dist/cjs/llm/stream/smoother.cjs +369 -0
- package/dist/cjs/llm/stream/smoother.cjs.map +1 -0
- package/dist/cjs/llm/vertexai/index.cjs +13 -1
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +25 -16
- 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 +664 -80
- 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/cjs/utils/tokens.cjs +76 -29
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +121 -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/anthropic/index.mjs +34 -205
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +120 -240
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/google/index.mjs +19 -12
- package/dist/esm/llm/google/index.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +2 -2
- package/dist/esm/llm/mistral/index.mjs +26 -0
- package/dist/esm/llm/mistral/index.mjs.map +1 -0
- package/dist/esm/llm/openai/index.mjs +82 -80
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/providers.mjs +3 -3
- package/dist/esm/llm/providers.mjs.map +1 -1
- package/dist/esm/llm/stream/chunkAdapters.mjs +195 -0
- package/dist/esm/llm/stream/chunkAdapters.mjs.map +1 -0
- package/dist/esm/llm/stream/smoother.mjs +365 -0
- package/dist/esm/llm/stream/smoother.mjs.map +1 -0
- package/dist/esm/llm/vertexai/index.mjs +13 -1
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +13 -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 +665 -81
- 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/esm/utils/tokens.mjs +76 -30
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +19 -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/index.d.ts +3 -0
- package/dist/types/llm/google/index.d.ts +2 -0
- package/dist/types/llm/mistral/index.d.ts +11 -0
- package/dist/types/llm/openai/index.d.ts +20 -4
- package/dist/types/llm/openrouter/index.d.ts +4 -1
- package/dist/types/llm/stream/chunkAdapters.d.ts +48 -0
- package/dist/types/llm/stream/smoother.d.ts +95 -0
- package/dist/types/llm/vertexai/index.d.ts +2 -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 +49 -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/dist/types/types/llm.d.ts +21 -14
- package/dist/types/utils/tokens.d.ts +10 -0
- package/package.json +6 -2
- package/src/graphs/Graph.ts +257 -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/index.ts +8 -0
- package/src/llm/anthropic/index.ts +85 -354
- package/src/llm/bedrock/index.ts +174 -390
- package/src/llm/bedrock/llm.spec.ts +2 -0
- package/src/llm/bedrock/streamSealDispatch.test.ts +76 -0
- package/src/llm/custom-chat-models.smoke.test.ts +16 -1
- package/src/llm/google/index.ts +17 -18
- package/src/llm/google/streamSmoothing.test.ts +121 -0
- package/src/llm/mistral/index.ts +33 -0
- package/src/llm/mistral/streamSmoothing.test.ts +97 -0
- package/src/llm/openai/deepseek.test.ts +56 -0
- package/src/llm/openai/index.ts +119 -126
- package/src/llm/openrouter/index.ts +4 -1
- package/src/llm/providers.ts +3 -3
- package/src/llm/stream/chunkAdapters.test.ts +202 -0
- package/src/llm/stream/chunkAdapters.ts +317 -0
- package/src/llm/stream/reassembly.test.ts +241 -0
- package/src/llm/stream/smoother.bench.test.ts +155 -0
- package/src/llm/stream/smoother.test.ts +519 -0
- package/src/llm/stream/smoother.ts +574 -0
- package/src/llm/vertexai/index.ts +17 -1
- package/src/llm/vertexai/streamSmoothing.test.ts +109 -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/specs/subagent.test.ts +44 -0
- package/src/specs/tokens.test.ts +159 -14
- 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 +590 -7
- 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 +1438 -136
- 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
- package/src/types/llm.ts +53 -36
- package/src/utils/tokens.ts +115 -30
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { GraphInterrupt, MemorySaver } from '@langchain/langgraph';
|
|
2
3
|
import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
|
|
3
4
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
4
5
|
import type {
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
summarizeEvent,
|
|
20
21
|
} from '../subagent';
|
|
21
22
|
import { sanitizeForwardedSubagentUpdateData } from '../subagent/SubagentExecutor';
|
|
23
|
+
import { SUBAGENT_PARENT_BATCH_CONFIG_KEY } from '../subagent/SubagentReplay';
|
|
22
24
|
import { Constants, Providers, GraphEvents, StepTypes } from '@/common';
|
|
23
25
|
import { StreamLimitExceededError } from '@/llm/streamLimits';
|
|
24
26
|
import { AgentContext } from '@/agents/AgentContext';
|
|
@@ -67,6 +69,50 @@ describe('filterSubagentResult', () => {
|
|
|
67
69
|
expect(filterSubagentResult(messages)).toBe('First part.\nSecond part.');
|
|
68
70
|
});
|
|
69
71
|
|
|
72
|
+
it('prefers final text_delta blocks over earlier AI text', () => {
|
|
73
|
+
const messages: BaseMessage[] = [
|
|
74
|
+
new AIMessage({
|
|
75
|
+
content: [
|
|
76
|
+
{ type: 'text', text: 'Let me search.' },
|
|
77
|
+
{ type: 'tool_use', id: 'call_1', name: 'search', input: {} },
|
|
78
|
+
],
|
|
79
|
+
}),
|
|
80
|
+
new ToolMessage({ content: 'result', tool_call_id: 'call_1' }),
|
|
81
|
+
new AIMessage({
|
|
82
|
+
content: [
|
|
83
|
+
{ type: 'text_delta', index: 0, text: 'Streamed ' },
|
|
84
|
+
{ type: 'text_delta', index: 0, text: 'result.' },
|
|
85
|
+
],
|
|
86
|
+
}),
|
|
87
|
+
];
|
|
88
|
+
expect(filterSubagentResult(messages)).toBe('Streamed result.');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('keeps annotation-only text blocks within a text_delta sequence', () => {
|
|
92
|
+
const messages: BaseMessage[] = [
|
|
93
|
+
new AIMessage({
|
|
94
|
+
content: [
|
|
95
|
+
{ type: 'text_delta', index: 0, text: 'Cited ' },
|
|
96
|
+
{ type: 'text', index: 0, citations: [{ url: 'source' }] },
|
|
97
|
+
{ type: 'text_delta', index: 0, text: 'answer.' },
|
|
98
|
+
],
|
|
99
|
+
}),
|
|
100
|
+
];
|
|
101
|
+
expect(filterSubagentResult(messages)).toBe('Cited answer.');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('separates text_delta blocks with different indexes', () => {
|
|
105
|
+
const messages: BaseMessage[] = [
|
|
106
|
+
new AIMessage({
|
|
107
|
+
content: [
|
|
108
|
+
{ type: 'text_delta', index: 0, text: 'First.' },
|
|
109
|
+
{ type: 'text_delta', index: 1, text: 'Second.' },
|
|
110
|
+
],
|
|
111
|
+
}),
|
|
112
|
+
];
|
|
113
|
+
expect(filterSubagentResult(messages)).toBe('First.\nSecond.');
|
|
114
|
+
});
|
|
115
|
+
|
|
70
116
|
it('strips tool_use blocks from array content', () => {
|
|
71
117
|
const messages: BaseMessage[] = [
|
|
72
118
|
new AIMessage({
|
|
@@ -305,8 +351,7 @@ describe('buildChildInputs', () => {
|
|
|
305
351
|
/**
|
|
306
352
|
* Self-spawn: `resolveSubagentConfigs` fills `agentInputs` as a shallow
|
|
307
353
|
* spread of the parent's `_sourceInputs`, so the parent-scoped direct
|
|
308
|
-
* tool would leak into
|
|
309
|
-
* `No checkpointer set` — must be scrubbed.
|
|
354
|
+
* tool would leak into the child implicitly — it must be scrubbed.
|
|
310
355
|
*/
|
|
311
356
|
const selfConfig: ResolvedSubagentConfig = {
|
|
312
357
|
type: 'self',
|
|
@@ -321,9 +366,8 @@ describe('buildChildInputs', () => {
|
|
|
321
366
|
|
|
322
367
|
/**
|
|
323
368
|
* Explicit child config: a host that deliberately attaches its own
|
|
324
|
-
* in-process direct tools to a child keeps them (Codex #289 P2
|
|
325
|
-
* interrupt-capable tools
|
|
326
|
-
* direct tools are legitimate there).
|
|
369
|
+
* in-process direct tools to a child keeps them (Codex #289 P2). With
|
|
370
|
+
* HITL enabled, interrupt-capable child tools use the shared checkpointer.
|
|
327
371
|
*/
|
|
328
372
|
const explicitConfig: ResolvedSubagentConfig = {
|
|
329
373
|
type: 'researcher',
|
|
@@ -452,6 +496,232 @@ describe('SubagentExecutor', () => {
|
|
|
452
496
|
expect(result.messages).toEqual([]);
|
|
453
497
|
});
|
|
454
498
|
|
|
499
|
+
it('fails closed when resumable execution has no parent tool call ID', async () => {
|
|
500
|
+
const createChildGraph = jest.fn(makeNoopGraphFactory());
|
|
501
|
+
const executor = createExecutor({
|
|
502
|
+
humanInTheLoop: { enabled: true },
|
|
503
|
+
createChildGraph,
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const result = await executor.execute({
|
|
507
|
+
description: 'Do something',
|
|
508
|
+
subagentType: 'researcher',
|
|
509
|
+
threadId: 'durable-thread',
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
expect(result.content).toContain('requires a parent tool call ID');
|
|
513
|
+
expect(result.messages).toEqual([]);
|
|
514
|
+
expect(createChildGraph).not.toHaveBeenCalled();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('persists terminal replay outcomes without an active child workflow', async () => {
|
|
518
|
+
const checkpointer = new MemorySaver();
|
|
519
|
+
const hookRegistry = new HookRegistry();
|
|
520
|
+
hookRegistry.registerSession('original-run', 'PreToolUse', {
|
|
521
|
+
hooks: [async (): Promise<Record<string, never>> => ({})],
|
|
522
|
+
});
|
|
523
|
+
const initialExecutor = createExecutor({
|
|
524
|
+
checkpointer,
|
|
525
|
+
hookRegistry,
|
|
526
|
+
humanInTheLoop: { enabled: true },
|
|
527
|
+
parentRunId: 'original-run',
|
|
528
|
+
});
|
|
529
|
+
const call = {
|
|
530
|
+
id: 'call_terminal',
|
|
531
|
+
name: Constants.SUBAGENT,
|
|
532
|
+
args: {
|
|
533
|
+
description: 'blocked before execution',
|
|
534
|
+
subagent_type: 'researcher',
|
|
535
|
+
},
|
|
536
|
+
type: 'tool_call' as const,
|
|
537
|
+
};
|
|
538
|
+
const originalConfig = {
|
|
539
|
+
configurable: {
|
|
540
|
+
thread_id: 'durable-parent',
|
|
541
|
+
checkpoint_id: 'parent-fork',
|
|
542
|
+
run_id: 'original-run',
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
const settled = {
|
|
546
|
+
output: new ToolMessage({
|
|
547
|
+
content: 'Blocked: policy',
|
|
548
|
+
status: 'error' as const,
|
|
549
|
+
name: Constants.SUBAGENT,
|
|
550
|
+
tool_call_id: call.id,
|
|
551
|
+
}),
|
|
552
|
+
additionalContexts: ['persisted context'],
|
|
553
|
+
resolvedArgs: { description: 'rewritten' },
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
await initialExecutor.persistSettledToolOutput(
|
|
557
|
+
call,
|
|
558
|
+
originalConfig,
|
|
559
|
+
settled
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
const rebuiltExecutor = createExecutor({
|
|
563
|
+
checkpointer,
|
|
564
|
+
hookRegistry,
|
|
565
|
+
humanInTheLoop: { enabled: true },
|
|
566
|
+
parentRunId: 'rebuilt-run',
|
|
567
|
+
});
|
|
568
|
+
const restored = await rebuiltExecutor.getSettledToolOutput(call, {
|
|
569
|
+
configurable: {
|
|
570
|
+
...originalConfig.configurable,
|
|
571
|
+
run_id: 'rebuilt-run',
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
expect(restored?.output.content).toBe('Blocked: policy');
|
|
576
|
+
expect(restored?.additionalContexts).toEqual(['persisted context']);
|
|
577
|
+
expect(restored?.resolvedArgs).toEqual({ description: 'rewritten' });
|
|
578
|
+
expect(hookRegistry.hasHookFor('PreToolUse', 'original-run')).toBe(true);
|
|
579
|
+
expect(hookRegistry.hasHookFor('PreToolUse', 'rebuilt-run')).toBe(true);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('persists an ordinary child error after the active workflow is released', async () => {
|
|
583
|
+
const checkpointer = new MemorySaver();
|
|
584
|
+
const invoke = jest.fn().mockRejectedValue(new Error('child failed'));
|
|
585
|
+
const executor = createExecutor({
|
|
586
|
+
checkpointer,
|
|
587
|
+
humanInTheLoop: { enabled: true },
|
|
588
|
+
createChildGraph: (): StandardGraph =>
|
|
589
|
+
({
|
|
590
|
+
createWorkflow: () => ({
|
|
591
|
+
getState: jest.fn().mockResolvedValue({
|
|
592
|
+
values: {},
|
|
593
|
+
next: ['child-agent'],
|
|
594
|
+
tasks: [],
|
|
595
|
+
}),
|
|
596
|
+
invoke,
|
|
597
|
+
}),
|
|
598
|
+
clearHeavyState: jest.fn(),
|
|
599
|
+
}) as unknown as StandardGraph,
|
|
600
|
+
});
|
|
601
|
+
const call = {
|
|
602
|
+
id: 'call_error',
|
|
603
|
+
name: Constants.SUBAGENT,
|
|
604
|
+
args: { description: 'fail', subagent_type: 'researcher' },
|
|
605
|
+
type: 'tool_call' as const,
|
|
606
|
+
};
|
|
607
|
+
const runnableConfig = {
|
|
608
|
+
configurable: {
|
|
609
|
+
thread_id: 'durable-parent',
|
|
610
|
+
checkpoint_id: 'parent-fork',
|
|
611
|
+
run_id: 'original-run',
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
const result = await executor.execute({
|
|
615
|
+
description: 'fail',
|
|
616
|
+
subagentType: 'researcher',
|
|
617
|
+
threadId: 'durable-parent',
|
|
618
|
+
parentToolCallId: call.id,
|
|
619
|
+
parentConfigurable: runnableConfig.configurable,
|
|
620
|
+
});
|
|
621
|
+
expect(result.content).toBe('Subagent error: child failed');
|
|
622
|
+
|
|
623
|
+
await executor.persistSettledToolOutput(call, runnableConfig, {
|
|
624
|
+
output: new ToolMessage({
|
|
625
|
+
content: result.content,
|
|
626
|
+
name: Constants.SUBAGENT,
|
|
627
|
+
tool_call_id: call.id,
|
|
628
|
+
}),
|
|
629
|
+
additionalContexts: [],
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const restored = await createExecutor({
|
|
633
|
+
checkpointer,
|
|
634
|
+
humanInTheLoop: { enabled: true },
|
|
635
|
+
}).getSettledToolOutput(call, runnableConfig);
|
|
636
|
+
expect(invoke).toHaveBeenCalledTimes(1);
|
|
637
|
+
expect(restored?.output.content).toBe('Subagent error: child failed');
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('continues a recovered in-progress child without reseeding its task', async () => {
|
|
641
|
+
const invoke = jest.fn().mockResolvedValue({
|
|
642
|
+
messages: [new AIMessage('continued')],
|
|
643
|
+
});
|
|
644
|
+
const executor = createExecutor({
|
|
645
|
+
checkpointer: new MemorySaver(),
|
|
646
|
+
humanInTheLoop: { enabled: true },
|
|
647
|
+
createChildGraph: (): StandardGraph =>
|
|
648
|
+
({
|
|
649
|
+
createWorkflow: () => ({
|
|
650
|
+
getState: jest.fn().mockResolvedValue({
|
|
651
|
+
values: {},
|
|
652
|
+
next: ['child-agent'],
|
|
653
|
+
tasks: [],
|
|
654
|
+
}),
|
|
655
|
+
invoke,
|
|
656
|
+
}),
|
|
657
|
+
clearHeavyState: jest.fn(),
|
|
658
|
+
}) as unknown as StandardGraph,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const result = await executor.execute({
|
|
662
|
+
description: 'Do not duplicate this task',
|
|
663
|
+
subagentType: 'researcher',
|
|
664
|
+
threadId: 'durable-parent',
|
|
665
|
+
parentToolCallId: 'call_in_progress',
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
expect(result.content).toBe('continued');
|
|
669
|
+
expect(invoke).toHaveBeenCalledWith(null, expect.any(Object));
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('preserves an existing nested approval scope', async () => {
|
|
673
|
+
const nestedScope = {
|
|
674
|
+
run_id: 'grandchild-run',
|
|
675
|
+
agent_id: 'grandchild-agent',
|
|
676
|
+
subagent_type: 'grandchild',
|
|
677
|
+
parent_tool_call_id: 'call_grandchild',
|
|
678
|
+
};
|
|
679
|
+
const nestedInterrupt = new GraphInterrupt([
|
|
680
|
+
{
|
|
681
|
+
id: 'nested-interrupt',
|
|
682
|
+
value: {
|
|
683
|
+
type: 'tool_approval',
|
|
684
|
+
action_requests: [],
|
|
685
|
+
review_configs: [],
|
|
686
|
+
subagent: nestedScope,
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
]);
|
|
690
|
+
const executor = createExecutor({
|
|
691
|
+
humanInTheLoop: { enabled: true },
|
|
692
|
+
createChildGraph: (): StandardGraph =>
|
|
693
|
+
({
|
|
694
|
+
createWorkflow: () => ({
|
|
695
|
+
getState: jest.fn().mockResolvedValue({
|
|
696
|
+
values: {},
|
|
697
|
+
next: ['agent'],
|
|
698
|
+
tasks: [],
|
|
699
|
+
}),
|
|
700
|
+
invoke: jest.fn().mockRejectedValue(nestedInterrupt),
|
|
701
|
+
}),
|
|
702
|
+
clearHeavyState: jest.fn(),
|
|
703
|
+
}) as unknown as StandardGraph,
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
let received: GraphInterrupt | undefined;
|
|
707
|
+
try {
|
|
708
|
+
await executor.execute({
|
|
709
|
+
description: 'Delegate again',
|
|
710
|
+
subagentType: 'researcher',
|
|
711
|
+
threadId: 'durable-thread',
|
|
712
|
+
parentToolCallId: 'call_child',
|
|
713
|
+
});
|
|
714
|
+
} catch (error) {
|
|
715
|
+
if (error instanceof GraphInterrupt) {
|
|
716
|
+
received = error;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
expect(received?.interrupts[0].value).toMatchObject({
|
|
721
|
+
subagent: nestedScope,
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
|
|
455
725
|
it('rethrows a child stream-limit trip instead of converting it to a tool result', async () => {
|
|
456
726
|
const executor = createExecutor({
|
|
457
727
|
createChildGraph: (): StandardGraph =>
|
|
@@ -470,7 +740,10 @@ describe('SubagentExecutor', () => {
|
|
|
470
740
|
}) as unknown as StandardGraph,
|
|
471
741
|
});
|
|
472
742
|
await expect(
|
|
473
|
-
executor.execute({
|
|
743
|
+
executor.execute({
|
|
744
|
+
description: 'Do something',
|
|
745
|
+
subagentType: 'researcher',
|
|
746
|
+
})
|
|
474
747
|
).rejects.toBeInstanceOf(StreamLimitExceededError);
|
|
475
748
|
});
|
|
476
749
|
|
|
@@ -1346,6 +1619,7 @@ describe('SubagentExecutor', () => {
|
|
|
1346
1619
|
checkpoint_id: 'parent-checkpoint-id',
|
|
1347
1620
|
checkpoint_map: { parent: 'checkpoint' },
|
|
1348
1621
|
checkpoint_ns: 'parent-checkpoint-ns',
|
|
1622
|
+
[SUBAGENT_PARENT_BATCH_CONFIG_KEY]: 'assistant-batch',
|
|
1349
1623
|
requestBody: { messageId: 'msg-1' },
|
|
1350
1624
|
thread_id: 'parent-thread',
|
|
1351
1625
|
user: { id: 'user_abc' },
|
|
@@ -1362,11 +1636,260 @@ describe('SubagentExecutor', () => {
|
|
|
1362
1636
|
expect(configurable.checkpoint_id).toBeUndefined();
|
|
1363
1637
|
expect(configurable.checkpoint_map).toBeUndefined();
|
|
1364
1638
|
expect(configurable.checkpoint_ns).toBeUndefined();
|
|
1639
|
+
expect(configurable[SUBAGENT_PARENT_BATCH_CONFIG_KEY]).toBeUndefined();
|
|
1365
1640
|
expect(configurable.requestBody).toEqual({ messageId: 'msg-1' });
|
|
1366
1641
|
expect(configurable.thread_id).toBe('parent-thread');
|
|
1367
1642
|
expect(configurable.user).toEqual({ id: 'user_abc' });
|
|
1368
1643
|
});
|
|
1369
1644
|
|
|
1645
|
+
it('isolates child checkpoints created from different parent forks', async () => {
|
|
1646
|
+
const invokedConfigs: Record<string, unknown>[] = [];
|
|
1647
|
+
const invoke = jest
|
|
1648
|
+
.fn()
|
|
1649
|
+
.mockImplementation(
|
|
1650
|
+
async (
|
|
1651
|
+
_input: unknown,
|
|
1652
|
+
config: Record<string, unknown>
|
|
1653
|
+
): Promise<{ messages: BaseMessage[] }> => {
|
|
1654
|
+
invokedConfigs.push(config);
|
|
1655
|
+
return { messages: [new AIMessage('done')] };
|
|
1656
|
+
}
|
|
1657
|
+
);
|
|
1658
|
+
const executor = createExecutor({
|
|
1659
|
+
humanInTheLoop: { enabled: true },
|
|
1660
|
+
createChildGraph: (): StandardGraph =>
|
|
1661
|
+
({
|
|
1662
|
+
createWorkflow: () => ({
|
|
1663
|
+
getState: jest.fn().mockResolvedValue({
|
|
1664
|
+
values: {},
|
|
1665
|
+
next: [],
|
|
1666
|
+
tasks: [],
|
|
1667
|
+
}),
|
|
1668
|
+
invoke,
|
|
1669
|
+
}),
|
|
1670
|
+
clearHeavyState: jest.fn(),
|
|
1671
|
+
}) as unknown as StandardGraph,
|
|
1672
|
+
});
|
|
1673
|
+
const common = {
|
|
1674
|
+
description: 'task',
|
|
1675
|
+
subagentType: 'researcher',
|
|
1676
|
+
threadId: 'parent-thread',
|
|
1677
|
+
parentToolCallId: 'call_shared',
|
|
1678
|
+
};
|
|
1679
|
+
|
|
1680
|
+
await executor.execute({
|
|
1681
|
+
...common,
|
|
1682
|
+
parentConfigurable: {
|
|
1683
|
+
thread_id: 'parent-thread',
|
|
1684
|
+
checkpoint_id: 'fork-a',
|
|
1685
|
+
},
|
|
1686
|
+
});
|
|
1687
|
+
await executor.execute({
|
|
1688
|
+
...common,
|
|
1689
|
+
parentConfigurable: {
|
|
1690
|
+
thread_id: 'parent-thread',
|
|
1691
|
+
checkpoint_id: 'fork-b',
|
|
1692
|
+
},
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
expect(invoke).toHaveBeenCalledTimes(2);
|
|
1696
|
+
const childThreadIds = invokedConfigs.map(
|
|
1697
|
+
(config) =>
|
|
1698
|
+
(config.configurable as Record<string, unknown>).thread_id as string
|
|
1699
|
+
);
|
|
1700
|
+
expect(childThreadIds[0]).toMatch(/^subagent:/);
|
|
1701
|
+
expect(childThreadIds[1]).toMatch(/^subagent:/);
|
|
1702
|
+
expect(childThreadIds[0]).not.toBe(childThreadIds[1]);
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1705
|
+
it('isolates reused tool-call IDs across assistant batches', async () => {
|
|
1706
|
+
const invokedThreadIds: string[] = [];
|
|
1707
|
+
const executor = createExecutor({
|
|
1708
|
+
humanInTheLoop: { enabled: true },
|
|
1709
|
+
createChildGraph: (): StandardGraph =>
|
|
1710
|
+
({
|
|
1711
|
+
createWorkflow: () => ({
|
|
1712
|
+
getState: jest.fn().mockResolvedValue({
|
|
1713
|
+
values: {},
|
|
1714
|
+
next: [],
|
|
1715
|
+
tasks: [],
|
|
1716
|
+
}),
|
|
1717
|
+
invoke: jest
|
|
1718
|
+
.fn()
|
|
1719
|
+
.mockImplementation(
|
|
1720
|
+
async (
|
|
1721
|
+
_input: unknown,
|
|
1722
|
+
invokeConfig: Record<string, unknown>
|
|
1723
|
+
): Promise<{ messages: BaseMessage[] }> => {
|
|
1724
|
+
invokedThreadIds.push(
|
|
1725
|
+
(invokeConfig.configurable as Record<string, unknown>)
|
|
1726
|
+
.thread_id as string
|
|
1727
|
+
);
|
|
1728
|
+
return { messages: [new AIMessage('done')] };
|
|
1729
|
+
}
|
|
1730
|
+
),
|
|
1731
|
+
}),
|
|
1732
|
+
clearHeavyState: jest.fn(),
|
|
1733
|
+
}) as unknown as StandardGraph,
|
|
1734
|
+
});
|
|
1735
|
+
const common = {
|
|
1736
|
+
description: 'task',
|
|
1737
|
+
subagentType: 'researcher',
|
|
1738
|
+
threadId: 'parent-thread',
|
|
1739
|
+
parentToolCallId: 'call_reused',
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
await executor.execute({
|
|
1743
|
+
...common,
|
|
1744
|
+
parentConfigurable: {
|
|
1745
|
+
thread_id: 'parent-thread',
|
|
1746
|
+
[SUBAGENT_PARENT_BATCH_CONFIG_KEY]: 'assistant-batch-1',
|
|
1747
|
+
},
|
|
1748
|
+
});
|
|
1749
|
+
await executor.execute({
|
|
1750
|
+
...common,
|
|
1751
|
+
parentConfigurable: {
|
|
1752
|
+
thread_id: 'parent-thread',
|
|
1753
|
+
[SUBAGENT_PARENT_BATCH_CONFIG_KEY]: 'assistant-batch-2',
|
|
1754
|
+
},
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
expect(invokedThreadIds).toHaveLength(2);
|
|
1758
|
+
expect(invokedThreadIds[0]).not.toBe(invokedThreadIds[1]);
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
it('keeps ambiguous parent agent and tool-call components collision-free', async () => {
|
|
1762
|
+
const invokedThreadIds: string[] = [];
|
|
1763
|
+
const createChildGraph = (): StandardGraph =>
|
|
1764
|
+
({
|
|
1765
|
+
createWorkflow: () => ({
|
|
1766
|
+
getState: jest.fn().mockResolvedValue({
|
|
1767
|
+
values: {},
|
|
1768
|
+
next: [],
|
|
1769
|
+
tasks: [],
|
|
1770
|
+
}),
|
|
1771
|
+
invoke: jest
|
|
1772
|
+
.fn()
|
|
1773
|
+
.mockImplementation(
|
|
1774
|
+
async (
|
|
1775
|
+
_input: unknown,
|
|
1776
|
+
invokeConfig: Record<string, unknown>
|
|
1777
|
+
): Promise<{ messages: BaseMessage[] }> => {
|
|
1778
|
+
invokedThreadIds.push(
|
|
1779
|
+
(invokeConfig.configurable as Record<string, unknown>)
|
|
1780
|
+
.thread_id as string
|
|
1781
|
+
);
|
|
1782
|
+
return { messages: [new AIMessage('done')] };
|
|
1783
|
+
}
|
|
1784
|
+
),
|
|
1785
|
+
}),
|
|
1786
|
+
clearHeavyState: jest.fn(),
|
|
1787
|
+
}) as unknown as StandardGraph;
|
|
1788
|
+
const common = {
|
|
1789
|
+
description: 'task',
|
|
1790
|
+
subagentType: 'researcher',
|
|
1791
|
+
threadId: 'parent',
|
|
1792
|
+
parentConfigurable: { thread_id: 'parent' },
|
|
1793
|
+
};
|
|
1794
|
+
|
|
1795
|
+
await createExecutor({
|
|
1796
|
+
checkpointer: new MemorySaver(),
|
|
1797
|
+
humanInTheLoop: { enabled: true },
|
|
1798
|
+
parentAgentId: 'a_b',
|
|
1799
|
+
createChildGraph,
|
|
1800
|
+
}).execute({ ...common, parentToolCallId: 'c' });
|
|
1801
|
+
await createExecutor({
|
|
1802
|
+
checkpointer: new MemorySaver(),
|
|
1803
|
+
humanInTheLoop: { enabled: true },
|
|
1804
|
+
parentAgentId: 'a',
|
|
1805
|
+
createChildGraph,
|
|
1806
|
+
}).execute({ ...common, parentToolCallId: 'b_c' });
|
|
1807
|
+
|
|
1808
|
+
expect(invokedThreadIds).toHaveLength(2);
|
|
1809
|
+
expect(invokedThreadIds[0]).not.toBe(invokedThreadIds[1]);
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
it('retains owned child checkpoint threads after heavy-state cleanup', async () => {
|
|
1813
|
+
const executor = createExecutor({
|
|
1814
|
+
checkpointer: new MemorySaver(),
|
|
1815
|
+
humanInTheLoop: { enabled: true },
|
|
1816
|
+
createChildGraph: (): StandardGraph =>
|
|
1817
|
+
({
|
|
1818
|
+
createWorkflow: () => ({
|
|
1819
|
+
getState: jest.fn().mockResolvedValue({
|
|
1820
|
+
values: {},
|
|
1821
|
+
next: [],
|
|
1822
|
+
tasks: [],
|
|
1823
|
+
}),
|
|
1824
|
+
invoke: jest.fn().mockResolvedValue({
|
|
1825
|
+
messages: [new AIMessage('done')],
|
|
1826
|
+
}),
|
|
1827
|
+
}),
|
|
1828
|
+
clearHeavyState: jest.fn(),
|
|
1829
|
+
}) as unknown as StandardGraph,
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
await executor.execute({
|
|
1833
|
+
description: 'task',
|
|
1834
|
+
subagentType: 'researcher',
|
|
1835
|
+
threadId: 'parent',
|
|
1836
|
+
parentToolCallId: 'call_owned',
|
|
1837
|
+
});
|
|
1838
|
+
const beforeCleanup = executor.getChildCheckpointThreadIds();
|
|
1839
|
+
executor.clearHeavyState();
|
|
1840
|
+
|
|
1841
|
+
expect(beforeCleanup).toHaveLength(1);
|
|
1842
|
+
expect(executor.getChildCheckpointThreadIds()).toEqual(beforeCleanup);
|
|
1843
|
+
|
|
1844
|
+
executor.resetCheckpointThreadIds();
|
|
1845
|
+
expect(executor.getChildCheckpointThreadIds()).toEqual([]);
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
it('includes checkpoint threads from active descendant graphs', async () => {
|
|
1849
|
+
const grandchildThreadId = 'subagent:grandchild';
|
|
1850
|
+
const executor = createExecutor({
|
|
1851
|
+
checkpointer: new MemorySaver(),
|
|
1852
|
+
humanInTheLoop: { enabled: true },
|
|
1853
|
+
createChildGraph: (): StandardGraph =>
|
|
1854
|
+
({
|
|
1855
|
+
createWorkflow: () => ({
|
|
1856
|
+
getState: jest.fn().mockResolvedValue({
|
|
1857
|
+
values: {},
|
|
1858
|
+
next: ['child-agent'],
|
|
1859
|
+
tasks: [],
|
|
1860
|
+
}),
|
|
1861
|
+
invoke: jest.fn().mockRejectedValue(
|
|
1862
|
+
new GraphInterrupt([
|
|
1863
|
+
{
|
|
1864
|
+
id: 'grandchild-interrupt',
|
|
1865
|
+
value: {
|
|
1866
|
+
type: 'tool_approval',
|
|
1867
|
+
action_requests: [],
|
|
1868
|
+
review_configs: [],
|
|
1869
|
+
},
|
|
1870
|
+
},
|
|
1871
|
+
])
|
|
1872
|
+
),
|
|
1873
|
+
}),
|
|
1874
|
+
getChildCheckpointThreadIds: jest.fn(() => [grandchildThreadId]),
|
|
1875
|
+
clearHeavyState: jest.fn(),
|
|
1876
|
+
}) as unknown as StandardGraph,
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
await expect(
|
|
1880
|
+
executor.execute({
|
|
1881
|
+
description: 'nested task',
|
|
1882
|
+
subagentType: 'researcher',
|
|
1883
|
+
threadId: 'parent',
|
|
1884
|
+
parentToolCallId: 'call_child',
|
|
1885
|
+
})
|
|
1886
|
+
).rejects.toBeInstanceOf(GraphInterrupt);
|
|
1887
|
+
|
|
1888
|
+
expect(executor.getChildCheckpointThreadIds()).toEqual(
|
|
1889
|
+
expect.arrayContaining([grandchildThreadId])
|
|
1890
|
+
);
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1370
1893
|
it('does not require parentConfigurable (back-compat with hosts that omit it)', async () => {
|
|
1371
1894
|
const { factory, getInvokeConfig } = makeCapturingGraphFactory();
|
|
1372
1895
|
const executor = createExecutor({ createChildGraph: factory });
|
|
@@ -1380,7 +1903,6 @@ describe('SubagentExecutor', () => {
|
|
|
1380
1903
|
string,
|
|
1381
1904
|
unknown
|
|
1382
1905
|
>;
|
|
1383
|
-
// Only thread_id (childRunId fallback) is set when no parent context is supplied.
|
|
1384
1906
|
expect(Object.keys(configurable)).toEqual(['thread_id']);
|
|
1385
1907
|
});
|
|
1386
1908
|
});
|
|
@@ -1455,6 +1977,67 @@ describe('SubagentExecutor', () => {
|
|
|
1455
1977
|
expect(input.agentType).toBe('researcher');
|
|
1456
1978
|
});
|
|
1457
1979
|
|
|
1980
|
+
it('finishes missing child lifecycle after recovering a completed checkpoint', async () => {
|
|
1981
|
+
const stopHook = jest.fn().mockResolvedValue({});
|
|
1982
|
+
const hookRegistry = new HookRegistry();
|
|
1983
|
+
hookRegistry.register('SubagentStop', { hooks: [stopHook] });
|
|
1984
|
+
const updates: SubagentUpdateEvent[] = [];
|
|
1985
|
+
const handlerRegistry = new HandlerRegistry();
|
|
1986
|
+
handlerRegistry.register(GraphEvents.ON_SUBAGENT_UPDATE, {
|
|
1987
|
+
handle: (_event, data): void => {
|
|
1988
|
+
updates.push(data as SubagentUpdateEvent);
|
|
1989
|
+
},
|
|
1990
|
+
});
|
|
1991
|
+
const invoke = jest.fn();
|
|
1992
|
+
const updateState = jest.fn().mockResolvedValue({});
|
|
1993
|
+
const executor = createExecutor({
|
|
1994
|
+
humanInTheLoop: { enabled: true },
|
|
1995
|
+
hookRegistry,
|
|
1996
|
+
parentHandlerRegistry: handlerRegistry,
|
|
1997
|
+
createChildGraph: (): StandardGraph =>
|
|
1998
|
+
({
|
|
1999
|
+
createWorkflow: () => ({
|
|
2000
|
+
getState: jest.fn().mockResolvedValue({
|
|
2001
|
+
values: { messages: [new AIMessage('persisted result')] },
|
|
2002
|
+
next: [],
|
|
2003
|
+
tasks: [],
|
|
2004
|
+
}),
|
|
2005
|
+
invoke,
|
|
2006
|
+
updateState,
|
|
2007
|
+
}),
|
|
2008
|
+
clearHeavyState: jest.fn(),
|
|
2009
|
+
}) as unknown as StandardGraph,
|
|
2010
|
+
});
|
|
2011
|
+
|
|
2012
|
+
const result = await executor.execute({
|
|
2013
|
+
description: 'recover me',
|
|
2014
|
+
subagentType: 'researcher',
|
|
2015
|
+
threadId: 'durable-thread',
|
|
2016
|
+
parentToolCallId: 'call_recovered',
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
expect(result.content).toBe('persisted result');
|
|
2020
|
+
expect(invoke).not.toHaveBeenCalled();
|
|
2021
|
+
expect(stopHook).toHaveBeenCalledTimes(1);
|
|
2022
|
+
expect(updates.filter((update) => update.phase === 'stop')).toHaveLength(
|
|
2023
|
+
1
|
|
2024
|
+
);
|
|
2025
|
+
expect(updateState).toHaveBeenCalledTimes(1);
|
|
2026
|
+
expect(updateState.mock.calls[0][1]).toMatchObject({
|
|
2027
|
+
messages: [
|
|
2028
|
+
{
|
|
2029
|
+
additional_kwargs: {
|
|
2030
|
+
__librechat_subagent_checkpoint: {
|
|
2031
|
+
version: 1,
|
|
2032
|
+
parentToolCallId: 'call_recovered',
|
|
2033
|
+
lifecycleComplete: true,
|
|
2034
|
+
},
|
|
2035
|
+
},
|
|
2036
|
+
},
|
|
2037
|
+
],
|
|
2038
|
+
});
|
|
2039
|
+
});
|
|
2040
|
+
|
|
1458
2041
|
it('SubagentStart deny blocks execution', async () => {
|
|
1459
2042
|
const registry = new HookRegistry();
|
|
1460
2043
|
registry.register('SubagentStart', {
|