@librechat/agents 3.4.0 → 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 +165 -57
- 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/HookRegistry.cjs +83 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +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 +34 -24
- package/dist/cjs/messages/format.cjs +40 -23
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +130 -53
- 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/session/JsonlSessionStore.cjs +2 -2
- 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/SubagentTool.cjs +4 -4
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +321 -95
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +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 +1114 -233
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +177 -0
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
- 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/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/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +165 -57
- 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/HookRegistry.mjs +83 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs +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 +21 -19
- package/dist/esm/messages/format.mjs +40 -23
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +131 -54
- 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/session/JsonlSessionStore.mjs +2 -2
- 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/SubagentTool.mjs +4 -4
- package/dist/esm/tools/SubagentTool.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +322 -96
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +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 +1114 -233
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +170 -0
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
- 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/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/agents/AgentContext.d.ts +1 -1
- package/dist/types/graphs/Graph.d.ts +22 -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/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 +6 -3
- package/dist/types/session/AgentSession.d.ts +1 -0
- package/dist/types/tools/SubagentTool.d.ts +1 -1
- package/dist/types/tools/ToolNode.d.ts +23 -15
- 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 +70 -36
- package/dist/types/tools/subagent/SubagentReplay.d.ts +85 -0
- 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/tools/toolOutputReferences.d.ts +12 -0
- package/dist/types/types/graph.d.ts +189 -24
- package/dist/types/types/hitl.d.ts +15 -0
- package/package.json +8 -2
- package/src/agents/AgentContext.ts +1 -1
- package/src/graphs/Graph.ts +349 -104
- package/src/graphs/MultiAgentGraph.ts +149 -47
- 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/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/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 +214 -56
- package/src/session/AgentSession.ts +52 -2
- package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
- 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/summarization/__tests__/node.test.ts +32 -14
- package/src/summarization/node.ts +15 -11
- package/src/tools/SubagentTool.ts +4 -4
- package/src/tools/ToolNode.ts +643 -157
- package/src/tools/__tests__/SubagentExecutor.test.ts +740 -15
- package/src/tools/__tests__/SubagentReplay.test.ts +329 -0
- package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +512 -2
- package/src/tools/__tests__/hitl.test.ts +234 -5
- package/src/tools/__tests__/subagentHooks.test.ts +1126 -61
- package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
- 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 +2232 -312
- package/src/tools/subagent/SubagentReplay.ts +584 -0
- 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/tools/toolOutputReferences.ts +40 -1
- package/src/types/graph.ts +222 -24
- package/src/types/hitl.ts +16 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
|
|
1
2
|
import { HumanMessage, getBufferString } from '@langchain/core/messages';
|
|
2
3
|
import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
|
|
3
4
|
import type { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
@@ -65,6 +66,40 @@ class CapturingChatModel extends FakeChatModel {
|
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
class GatedMessageCountChatModel extends FakeChatModel {
|
|
70
|
+
private responseIndex = 0;
|
|
71
|
+
|
|
72
|
+
constructor(
|
|
73
|
+
private readonly gatedAiMessageCount: number,
|
|
74
|
+
private readonly onGatedStart: () => void,
|
|
75
|
+
private readonly releaseGate: Promise<void>
|
|
76
|
+
) {
|
|
77
|
+
super({ responses: ['unused'] });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
override async *_streamResponseChunks(
|
|
81
|
+
messages: BaseMessage[],
|
|
82
|
+
_options: this['ParsedCallOptions'],
|
|
83
|
+
runManager?: CallbackManagerForLLMRun
|
|
84
|
+
): AsyncGenerator<ChatGenerationChunk> {
|
|
85
|
+
const aiMessageCount = messages.filter(
|
|
86
|
+
(message) => message.getType() === 'ai'
|
|
87
|
+
).length;
|
|
88
|
+
if (aiMessageCount === this.gatedAiMessageCount) {
|
|
89
|
+
this.onGatedStart();
|
|
90
|
+
await this.releaseGate;
|
|
91
|
+
}
|
|
92
|
+
const output = `response-${this.responseIndex++}`;
|
|
93
|
+
yield this._createResponseChunk(output);
|
|
94
|
+
void runManager?.handleLLMNewToken(output);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type AgentInvocation = {
|
|
99
|
+
agentId: string;
|
|
100
|
+
messages: BaseMessage[];
|
|
101
|
+
};
|
|
102
|
+
|
|
68
103
|
const expectCompiledWorkflow = (
|
|
69
104
|
workflow: t.CompiledWorkflow | t.CompiledMultiAgentWorkflow
|
|
70
105
|
): void => {
|
|
@@ -342,6 +377,123 @@ describe('LangGraph composition smoke tests', () => {
|
|
|
342
377
|
expect(graph.getParallelGroupId('final')).toBeUndefined();
|
|
343
378
|
});
|
|
344
379
|
|
|
380
|
+
it.each([
|
|
381
|
+
['without a prompt wrapper', undefined],
|
|
382
|
+
['with a prompt wrapper', 'Summarize these results:\n{results}'],
|
|
383
|
+
])('waits for every explicit fan-in source %s', async (_label, prompt) => {
|
|
384
|
+
const invocations: AgentInvocation[] = [];
|
|
385
|
+
let releaseLeft2!: () => void;
|
|
386
|
+
let markLeft2Started!: () => void;
|
|
387
|
+
const left2Release = new Promise<void>((resolve) => {
|
|
388
|
+
releaseLeft2 = resolve;
|
|
389
|
+
});
|
|
390
|
+
const left2Started = new Promise<void>((resolve) => {
|
|
391
|
+
markLeft2Started = resolve;
|
|
392
|
+
});
|
|
393
|
+
const invocationHandler = BaseCallbackHandler.fromMethods({
|
|
394
|
+
handleChatModelStart: (
|
|
395
|
+
_llm,
|
|
396
|
+
messages,
|
|
397
|
+
_runId,
|
|
398
|
+
_parentRunId,
|
|
399
|
+
_extraParams,
|
|
400
|
+
_tags,
|
|
401
|
+
metadata
|
|
402
|
+
) => {
|
|
403
|
+
const agentId = metadata?.agentId;
|
|
404
|
+
if (typeof agentId === 'string') {
|
|
405
|
+
invocations.push({ agentId, messages: [...messages[0]] });
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
const graph = new MultiAgentGraph({
|
|
410
|
+
runId: 'fan-in-waiting-edge-smoke',
|
|
411
|
+
agents: [
|
|
412
|
+
makeAgent('root'),
|
|
413
|
+
makeAgent('left'),
|
|
414
|
+
makeAgent('left2'),
|
|
415
|
+
makeAgent('right'),
|
|
416
|
+
makeAgent('final'),
|
|
417
|
+
],
|
|
418
|
+
edges: [
|
|
419
|
+
{ from: 'root', to: ['left', 'right'], edgeType: 'direct' },
|
|
420
|
+
{ from: 'left', to: 'left2', edgeType: 'direct' },
|
|
421
|
+
{
|
|
422
|
+
from: ['left2', 'right'],
|
|
423
|
+
to: 'final',
|
|
424
|
+
edgeType: 'direct',
|
|
425
|
+
prompt,
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
});
|
|
429
|
+
graph.overrideModel = new GatedMessageCountChatModel(
|
|
430
|
+
3,
|
|
431
|
+
markLeft2Started,
|
|
432
|
+
left2Release
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const invocation = graph.createWorkflow().invoke(
|
|
436
|
+
{ messages: [new HumanMessage('start')] },
|
|
437
|
+
{
|
|
438
|
+
...makeConfig('fan-in-waiting-edge-smoke'),
|
|
439
|
+
callbacks: [invocationHandler],
|
|
440
|
+
}
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
let gateTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
444
|
+
try {
|
|
445
|
+
await Promise.race([
|
|
446
|
+
left2Started,
|
|
447
|
+
new Promise<void>((_resolve, reject) => {
|
|
448
|
+
gateTimeout = setTimeout(
|
|
449
|
+
() =>
|
|
450
|
+
reject(
|
|
451
|
+
new Error(
|
|
452
|
+
`Timed out waiting for the gated branch; started: ${invocations
|
|
453
|
+
.map(({ agentId }) => agentId)
|
|
454
|
+
.join(', ')}`
|
|
455
|
+
)
|
|
456
|
+
),
|
|
457
|
+
5_000
|
|
458
|
+
);
|
|
459
|
+
}),
|
|
460
|
+
]);
|
|
461
|
+
} finally {
|
|
462
|
+
clearTimeout(gateTimeout);
|
|
463
|
+
}
|
|
464
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
465
|
+
expect(
|
|
466
|
+
invocations.filter(({ agentId }) => agentId === 'final')
|
|
467
|
+
).toHaveLength(0);
|
|
468
|
+
releaseLeft2();
|
|
469
|
+
await invocation;
|
|
470
|
+
|
|
471
|
+
const invokedAgentIds = invocations.map(({ agentId }) => agentId);
|
|
472
|
+
expect(
|
|
473
|
+
invokedAgentIds.filter((agentId) => agentId === 'final')
|
|
474
|
+
).toHaveLength(1);
|
|
475
|
+
expect(invokedAgentIds.indexOf('final')).toBeGreaterThan(
|
|
476
|
+
invokedAgentIds.indexOf('left2')
|
|
477
|
+
);
|
|
478
|
+
expect(invokedAgentIds.indexOf('final')).toBeGreaterThan(
|
|
479
|
+
invokedAgentIds.indexOf('right')
|
|
480
|
+
);
|
|
481
|
+
const finalInvocation = invocations.find(
|
|
482
|
+
({ agentId }) => agentId === 'final'
|
|
483
|
+
);
|
|
484
|
+
if (prompt == null) {
|
|
485
|
+
expect(
|
|
486
|
+
finalInvocation?.messages.filter(
|
|
487
|
+
(message) => message.getType() === 'ai'
|
|
488
|
+
)
|
|
489
|
+
).toHaveLength(4);
|
|
490
|
+
} else {
|
|
491
|
+
expect(getBufferString(finalInvocation?.messages ?? [])).toContain(
|
|
492
|
+
'response-3'
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
345
497
|
it('compiles mixed handoff and direct routing from the same agent', () => {
|
|
346
498
|
const graph = new MultiAgentGraph({
|
|
347
499
|
runId: 'mixed-routing-smoke',
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
2
|
+
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
3
|
+
import type { GraphFactory, GraphFactoryRequest } from '@/graphs/graphFactory';
|
|
4
|
+
import type * as t from '@/types';
|
|
5
|
+
import { MultiAgentGraph } from '@/graphs/MultiAgentGraph';
|
|
6
|
+
import { createFakeStreamingLLM } from '@/llm/fake';
|
|
7
|
+
import { createGraph } from '@/graphs/createGraph';
|
|
8
|
+
import { Constants, Providers } from '@/common';
|
|
9
|
+
import { StandardGraph } from '@/graphs/Graph';
|
|
10
|
+
|
|
11
|
+
const invokeConfig: RunnableConfig = {
|
|
12
|
+
configurable: { thread_id: 'graph-factory-test' },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const makeAgent = (agentId: string): t.AgentInputs => ({
|
|
16
|
+
agentId,
|
|
17
|
+
provider: Providers.OPENAI,
|
|
18
|
+
instructions: `You are ${agentId}.`,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const getSubagentTool = (
|
|
22
|
+
graph: StandardGraph,
|
|
23
|
+
agentId: string
|
|
24
|
+
): t.GenericTool => {
|
|
25
|
+
graph.createWorkflow();
|
|
26
|
+
const tools = graph.agentContexts.get(agentId)?.graphTools as
|
|
27
|
+
| t.GenericTool[]
|
|
28
|
+
| undefined;
|
|
29
|
+
const tool = tools?.find(
|
|
30
|
+
(candidate) => 'name' in candidate && candidate.name === Constants.SUBAGENT
|
|
31
|
+
);
|
|
32
|
+
if (tool == null) {
|
|
33
|
+
throw new Error(`Expected subagent tool for ${agentId}`);
|
|
34
|
+
}
|
|
35
|
+
return tool;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe('graph factory', () => {
|
|
39
|
+
it('constructs standard and multi-agent graph adapters', () => {
|
|
40
|
+
const standard = createGraph({
|
|
41
|
+
kind: 'standard',
|
|
42
|
+
input: { runId: 'standard-factory', agents: [makeAgent('standard')] },
|
|
43
|
+
});
|
|
44
|
+
const multiAgent = createGraph({
|
|
45
|
+
kind: 'multi-agent',
|
|
46
|
+
input: {
|
|
47
|
+
runId: 'multi-agent-factory',
|
|
48
|
+
agents: [makeAgent('multi')],
|
|
49
|
+
edges: [],
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(standard).toBeInstanceOf(StandardGraph);
|
|
54
|
+
expect(standard).not.toBeInstanceOf(MultiAgentGraph);
|
|
55
|
+
expect(multiAgent).toBeInstanceOf(MultiAgentGraph);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('accepts a union-typed graph factory request', () => {
|
|
59
|
+
const construct = (request: GraphFactoryRequest): StandardGraph =>
|
|
60
|
+
createGraph(request);
|
|
61
|
+
const graph = construct({
|
|
62
|
+
kind: 'standard',
|
|
63
|
+
input: { runId: 'union-factory', agents: [makeAgent('union')] },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(graph).toBeInstanceOf(StandardGraph);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('rejects invalid per-member recursion limits', () => {
|
|
70
|
+
expect(() =>
|
|
71
|
+
createGraph({
|
|
72
|
+
kind: 'multi-agent',
|
|
73
|
+
input: {
|
|
74
|
+
runId: 'invalid-member-limit',
|
|
75
|
+
agents: [makeAgent('member')],
|
|
76
|
+
edges: [],
|
|
77
|
+
memberRecursionLimit: 0,
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
).toThrow(/memberRecursionLimit must be a positive safe integer/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('keeps direct StandardGraph construction compatible with agent children', async () => {
|
|
84
|
+
const parent = {
|
|
85
|
+
...makeAgent('parent'),
|
|
86
|
+
subagentConfigs: [
|
|
87
|
+
{
|
|
88
|
+
type: 'worker',
|
|
89
|
+
name: 'Worker',
|
|
90
|
+
description: 'Completes one isolated task.',
|
|
91
|
+
agentInputs: makeAgent('worker'),
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
} satisfies t.AgentInputs;
|
|
95
|
+
const graph = new StandardGraph({
|
|
96
|
+
runId: 'direct-standard-factory',
|
|
97
|
+
agents: [parent],
|
|
98
|
+
});
|
|
99
|
+
graph.setSubagentModelOverride(
|
|
100
|
+
createFakeStreamingLLM({ responses: ['worker complete'] })
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const result = await getSubagentTool(graph, 'parent').invoke(
|
|
104
|
+
{ description: 'Complete the task.', subagent_type: 'worker' },
|
|
105
|
+
invokeConfig
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(result).toBe('worker complete');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('fails early when direct construction cannot instantiate a graph child', () => {
|
|
112
|
+
const parent = {
|
|
113
|
+
...makeAgent('parent'),
|
|
114
|
+
subagentConfigs: [
|
|
115
|
+
{
|
|
116
|
+
kind: 'graph' as const,
|
|
117
|
+
type: 'team',
|
|
118
|
+
name: 'Team',
|
|
119
|
+
description: 'Runs one member.',
|
|
120
|
+
agents: [makeAgent('member')],
|
|
121
|
+
edges: [],
|
|
122
|
+
entryAgentId: 'member',
|
|
123
|
+
resultAgentId: 'member',
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
} satisfies t.AgentInputs;
|
|
127
|
+
const graph = new StandardGraph({
|
|
128
|
+
runId: 'direct-graph-child-factory',
|
|
129
|
+
agents: [parent],
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(() => graph.createWorkflow()).toThrow(
|
|
133
|
+
/constructing the parent with createGraph\(\)/
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('propagates an injected factory through child and grandchild graphs', async () => {
|
|
138
|
+
const requests: GraphFactoryRequest[] = [];
|
|
139
|
+
const graphFactory: GraphFactory = (request) => {
|
|
140
|
+
requests.push(request);
|
|
141
|
+
if (request.kind === 'multi-agent') {
|
|
142
|
+
return new MultiAgentGraph(request.input, { graphFactory });
|
|
143
|
+
}
|
|
144
|
+
return new StandardGraph(request.input, { graphFactory });
|
|
145
|
+
};
|
|
146
|
+
const nestedToolCall: ToolCall = {
|
|
147
|
+
id: 'call_nested_specialist',
|
|
148
|
+
name: Constants.SUBAGENT,
|
|
149
|
+
args: {
|
|
150
|
+
description: 'Complete the specialist step.',
|
|
151
|
+
subagent_type: 'specialist',
|
|
152
|
+
},
|
|
153
|
+
type: 'tool_call',
|
|
154
|
+
};
|
|
155
|
+
const router = {
|
|
156
|
+
...makeAgent('router'),
|
|
157
|
+
subagentConfigs: [
|
|
158
|
+
{
|
|
159
|
+
type: 'specialist',
|
|
160
|
+
name: 'Specialist',
|
|
161
|
+
description: 'Completes the nested specialist step.',
|
|
162
|
+
agentInputs: makeAgent('specialist'),
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
} satisfies t.AgentInputs;
|
|
166
|
+
const root = {
|
|
167
|
+
...makeAgent('root'),
|
|
168
|
+
maxSubagentDepth: 2,
|
|
169
|
+
subagentConfigs: [
|
|
170
|
+
{
|
|
171
|
+
type: 'router',
|
|
172
|
+
name: 'Router',
|
|
173
|
+
description: 'Delegates one nested specialist step.',
|
|
174
|
+
allowNested: true,
|
|
175
|
+
agentInputs: router,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
} satisfies t.AgentInputs;
|
|
179
|
+
const graph = new StandardGraph(
|
|
180
|
+
{ runId: 'recursive-graph-factory', agents: [root] },
|
|
181
|
+
{ graphFactory }
|
|
182
|
+
);
|
|
183
|
+
graph.setSubagentModelOverride(
|
|
184
|
+
createFakeStreamingLLM({
|
|
185
|
+
responses: ['delegating', 'specialist complete', 'router complete'],
|
|
186
|
+
toolCalls: [nestedToolCall],
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const result = await getSubagentTool(graph, 'root').invoke(
|
|
191
|
+
{ description: 'Route the task.', subagent_type: 'router' },
|
|
192
|
+
invokeConfig
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(result).toBe('router complete');
|
|
196
|
+
expect(
|
|
197
|
+
requests.map(({ kind, input }) => ({
|
|
198
|
+
kind,
|
|
199
|
+
agentIds: input.agents.map(({ agentId }) => agentId),
|
|
200
|
+
}))
|
|
201
|
+
).toEqual([
|
|
202
|
+
{ kind: 'standard', agentIds: ['router'] },
|
|
203
|
+
{ kind: 'standard', agentIds: ['specialist'] },
|
|
204
|
+
]);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { StandardGraph } from './Graph';
|
|
2
|
+
|
|
3
|
+
export type GraphRuntimeConfig = Pick<
|
|
4
|
+
StandardGraph,
|
|
5
|
+
| 'hookRegistry'
|
|
6
|
+
| 'humanInTheLoop'
|
|
7
|
+
| 'toolOutputReferences'
|
|
8
|
+
| 'eagerEventToolExecution'
|
|
9
|
+
| 'codeSessionToolNames'
|
|
10
|
+
| 'interruptingToolNames'
|
|
11
|
+
| 'toolExecution'
|
|
12
|
+
>;
|
|
13
|
+
|
|
14
|
+
export function applyGraphRuntimeConfig(
|
|
15
|
+
graph: StandardGraph,
|
|
16
|
+
config: GraphRuntimeConfig
|
|
17
|
+
): void {
|
|
18
|
+
graph.hookRegistry = config.hookRegistry;
|
|
19
|
+
graph.humanInTheLoop = config.humanInTheLoop;
|
|
20
|
+
graph.toolOutputReferences = config.toolOutputReferences;
|
|
21
|
+
graph.eagerEventToolExecution = config.eagerEventToolExecution;
|
|
22
|
+
graph.codeSessionToolNames = config.codeSessionToolNames;
|
|
23
|
+
graph.interruptingToolNames = config.interruptingToolNames;
|
|
24
|
+
graph.toolExecution = config.toolExecution;
|
|
25
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GraphFactory,
|
|
3
|
+
GraphFactoryDependencies,
|
|
4
|
+
GraphFactoryRequest,
|
|
5
|
+
} from '@/graphs/graphFactory';
|
|
6
|
+
import type { MultiAgentGraphInput, StandardGraphInput } from '@/types';
|
|
7
|
+
import { MultiAgentGraph } from '@/graphs/MultiAgentGraph';
|
|
8
|
+
import { StandardGraph } from '@/graphs/Graph';
|
|
9
|
+
|
|
10
|
+
const createGraphByKind: GraphFactory = (request) => {
|
|
11
|
+
if (request.kind === 'multi-agent') {
|
|
12
|
+
return new MultiAgentGraph(request.input, dependencies);
|
|
13
|
+
}
|
|
14
|
+
return new StandardGraph(request.input, dependencies);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const dependencies: GraphFactoryDependencies = {
|
|
18
|
+
graphFactory: createGraphByKind,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function createGraph(request: {
|
|
22
|
+
kind: 'standard';
|
|
23
|
+
input: StandardGraphInput;
|
|
24
|
+
}): StandardGraph;
|
|
25
|
+
export function createGraph(request: {
|
|
26
|
+
kind: 'multi-agent';
|
|
27
|
+
input: MultiAgentGraphInput;
|
|
28
|
+
}): MultiAgentGraph;
|
|
29
|
+
export function createGraph(request: GraphFactoryRequest): StandardGraph;
|
|
30
|
+
export function createGraph(request: GraphFactoryRequest): StandardGraph {
|
|
31
|
+
return createGraphByKind(request);
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StandardGraph } from './Graph';
|
|
2
|
+
import type * as t from '@/types';
|
|
3
|
+
|
|
4
|
+
export type GraphFactoryRequest =
|
|
5
|
+
| { kind: 'standard'; input: t.StandardGraphInput }
|
|
6
|
+
| { kind: 'multi-agent'; input: t.MultiAgentGraphInput };
|
|
7
|
+
|
|
8
|
+
export type GraphFactory = (request: GraphFactoryRequest) => StandardGraph;
|
|
9
|
+
|
|
10
|
+
export type GraphFactoryDependencies = {
|
|
11
|
+
graphFactory: GraphFactory;
|
|
12
|
+
};
|
package/src/graphs/index.ts
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
// src/hooks/HookRegistry.ts
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
HookEvent,
|
|
4
|
+
HookMatcher,
|
|
5
|
+
ToolApprovalReplayKey,
|
|
6
|
+
ToolApprovalReplaySnapshot,
|
|
7
|
+
AggregatedHookResult,
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
function serializeApprovalKey(key: ToolApprovalReplayKey): string {
|
|
11
|
+
return JSON.stringify([key.executionScope, key.agentId, key.toolUseId]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function deserializeApprovalKey(value: string): ToolApprovalReplayKey | null {
|
|
15
|
+
const parsed: unknown = JSON.parse(value);
|
|
16
|
+
if (
|
|
17
|
+
!Array.isArray(parsed) ||
|
|
18
|
+
parsed.length !== 3 ||
|
|
19
|
+
parsed.some((part) => typeof part !== 'string')
|
|
20
|
+
) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
executionScope: parsed[0],
|
|
25
|
+
agentId: parsed[1],
|
|
26
|
+
toolUseId: parsed[2],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
3
29
|
|
|
4
30
|
/**
|
|
5
31
|
* Internal matcher storage type.
|
|
@@ -71,6 +97,11 @@ export class HookRegistry {
|
|
|
71
97
|
* O(1) insertion in hot paths, no spread-on-write.
|
|
72
98
|
*/
|
|
73
99
|
private readonly haltSignals: Map<string, HookHaltSignal> = new Map();
|
|
100
|
+
/** One-shot hook contributions retained until approval is consumed. */
|
|
101
|
+
private readonly pendingToolApprovals = new Map<
|
|
102
|
+
string,
|
|
103
|
+
Map<string, AggregatedHookResult>
|
|
104
|
+
>();
|
|
74
105
|
|
|
75
106
|
/**
|
|
76
107
|
* Register a matcher for the lifetime of this registry (= one Run).
|
|
@@ -160,6 +191,125 @@ export class HookRegistry {
|
|
|
160
191
|
*/
|
|
161
192
|
clearSession(sessionId: string): void {
|
|
162
193
|
this.sessions.delete(sessionId);
|
|
194
|
+
this.pendingToolApprovals.delete(sessionId);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Copies session-scoped policy into a rebuilt or branched Run. */
|
|
198
|
+
copySession(sourceSessionId: string, targetSessionId: string): void {
|
|
199
|
+
if (sourceSessionId === targetSessionId) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const source = this.sessions.get(sourceSessionId);
|
|
203
|
+
if (source != null) {
|
|
204
|
+
const target = this.ensureSessionBucket(targetSessionId);
|
|
205
|
+
for (const event of Object.keys(source) as HookEvent[]) {
|
|
206
|
+
const targetList = ensureList(target, event);
|
|
207
|
+
for (const matcher of readList(source, event)) {
|
|
208
|
+
if (!targetList.includes(matcher)) {
|
|
209
|
+
targetList.push(matcher);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const pending = this.pendingToolApprovals.get(sourceSessionId);
|
|
215
|
+
if (pending == null) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
let targetPending = this.pendingToolApprovals.get(targetSessionId);
|
|
219
|
+
if (targetPending == null) {
|
|
220
|
+
targetPending = new Map();
|
|
221
|
+
this.pendingToolApprovals.set(targetSessionId, targetPending);
|
|
222
|
+
}
|
|
223
|
+
for (const [toolUseId, result] of pending) {
|
|
224
|
+
if (!targetPending.has(toolUseId)) {
|
|
225
|
+
targetPending.set(toolUseId, result);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
getPendingToolApproval(
|
|
231
|
+
sessionId: string,
|
|
232
|
+
key: ToolApprovalReplayKey
|
|
233
|
+
): AggregatedHookResult | undefined {
|
|
234
|
+
return this.pendingToolApprovals
|
|
235
|
+
.get(sessionId)
|
|
236
|
+
?.get(serializeApprovalKey(key));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
setPendingToolApproval(
|
|
240
|
+
sessionId: string,
|
|
241
|
+
key: ToolApprovalReplayKey,
|
|
242
|
+
result: AggregatedHookResult
|
|
243
|
+
): void {
|
|
244
|
+
let pending = this.pendingToolApprovals.get(sessionId);
|
|
245
|
+
if (pending == null) {
|
|
246
|
+
pending = new Map();
|
|
247
|
+
this.pendingToolApprovals.set(sessionId, pending);
|
|
248
|
+
}
|
|
249
|
+
pending.set(serializeApprovalKey(key), result);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
clearPendingToolApproval(
|
|
253
|
+
sessionId: string,
|
|
254
|
+
key: ToolApprovalReplayKey
|
|
255
|
+
): void {
|
|
256
|
+
const pending = this.pendingToolApprovals.get(sessionId);
|
|
257
|
+
if (pending == null) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
pending.delete(serializeApprovalKey(key));
|
|
261
|
+
if (pending.size === 0) {
|
|
262
|
+
this.pendingToolApprovals.delete(sessionId);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
snapshotPendingToolApprovals(
|
|
267
|
+
sessionId: string,
|
|
268
|
+
executionScope: string
|
|
269
|
+
): ToolApprovalReplaySnapshot[] {
|
|
270
|
+
const pending = this.pendingToolApprovals.get(sessionId);
|
|
271
|
+
if (pending == null) {
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
const snapshots: ToolApprovalReplaySnapshot[] = [];
|
|
275
|
+
for (const [serializedKey, result] of pending) {
|
|
276
|
+
const key = deserializeApprovalKey(serializedKey);
|
|
277
|
+
if (key == null || key.executionScope !== executionScope) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
snapshots.push({ key, result });
|
|
281
|
+
}
|
|
282
|
+
return snapshots;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
restorePendingToolApprovals(
|
|
286
|
+
sessionId: string,
|
|
287
|
+
targetExecutionScope: string,
|
|
288
|
+
snapshots: ReadonlyArray<ToolApprovalReplaySnapshot>
|
|
289
|
+
): void {
|
|
290
|
+
const restored = new Map<string, AggregatedHookResult>();
|
|
291
|
+
for (const [serializedKey, result] of this.pendingToolApprovals.get(
|
|
292
|
+
sessionId
|
|
293
|
+
) ?? []) {
|
|
294
|
+
const key = deserializeApprovalKey(serializedKey);
|
|
295
|
+
if (key?.executionScope !== targetExecutionScope) {
|
|
296
|
+
restored.set(serializedKey, result);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (const snapshot of snapshots) {
|
|
300
|
+
restored.set(
|
|
301
|
+
serializeApprovalKey({
|
|
302
|
+
...snapshot.key,
|
|
303
|
+
executionScope: targetExecutionScope,
|
|
304
|
+
}),
|
|
305
|
+
snapshot.result
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (restored.size === 0) {
|
|
309
|
+
this.pendingToolApprovals.delete(sessionId);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
this.pendingToolApprovals.set(sessionId, restored);
|
|
163
313
|
}
|
|
164
314
|
|
|
165
315
|
/**
|