@illuma-ai/agents 1.1.25 → 1.3.0
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 +20 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/spawnPath.cjs +104 -0
- package/dist/cjs/common/spawnPath.cjs.map +1 -0
- package/dist/cjs/graphs/Graph.cjs +87 -31
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/HandoffRegistry.cjs +143 -0
- package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -0
- package/dist/cjs/graphs/MultiAgentGraph.cjs +587 -184
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/graphs/phases/flushLoop.cjs +214 -0
- package/dist/cjs/graphs/phases/flushLoop.cjs.map +1 -0
- package/dist/cjs/graphs/phases/memoryFlushPhase.cjs +102 -0
- package/dist/cjs/graphs/phases/memoryFlushPhase.cjs.map +1 -0
- package/dist/cjs/llm/bedrock/index.cjs +4 -3
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +115 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/memory/citations.cjs +69 -0
- package/dist/cjs/memory/citations.cjs.map +1 -0
- package/dist/cjs/memory/compositeBackend.cjs +60 -0
- package/dist/cjs/memory/compositeBackend.cjs.map +1 -0
- package/dist/cjs/memory/constants.cjs +232 -0
- package/dist/cjs/memory/constants.cjs.map +1 -0
- package/dist/cjs/memory/embeddings.cjs +151 -0
- package/dist/cjs/memory/embeddings.cjs.map +1 -0
- package/dist/cjs/memory/factory.cjs +95 -0
- package/dist/cjs/memory/factory.cjs.map +1 -0
- package/dist/cjs/memory/migrate.cjs +81 -0
- package/dist/cjs/memory/migrate.cjs.map +1 -0
- package/dist/cjs/memory/mmr.cjs +138 -0
- package/dist/cjs/memory/mmr.cjs.map +1 -0
- package/dist/cjs/memory/paths.cjs +217 -0
- package/dist/cjs/memory/paths.cjs.map +1 -0
- package/dist/cjs/memory/pgvectorStore.cjs +225 -0
- package/dist/cjs/memory/pgvectorStore.cjs.map +1 -0
- package/dist/cjs/memory/recallTracking.cjs +98 -0
- package/dist/cjs/memory/recallTracking.cjs.map +1 -0
- package/dist/cjs/memory/schema.sql +51 -0
- package/dist/cjs/memory/temporalDecay.cjs +118 -0
- package/dist/cjs/memory/temporalDecay.cjs.map +1 -0
- package/dist/cjs/nodes/ApprovalGateNode.cjs +1 -1
- package/dist/cjs/nodes/ApprovalGateNode.cjs.map +1 -1
- package/dist/cjs/prompts/memoryFlushPrompt.cjs +49 -0
- package/dist/cjs/prompts/memoryFlushPrompt.cjs.map +1 -0
- package/dist/cjs/run.cjs +16 -3
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +4 -4
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/AskUser.cjs +6 -1
- package/dist/cjs/tools/AskUser.cjs.map +1 -1
- package/dist/cjs/tools/BrowserTools.cjs +1 -1
- package/dist/cjs/tools/BrowserTools.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +127 -10
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/approval/constants.cjs +2 -2
- package/dist/cjs/tools/approval/constants.cjs.map +1 -1
- package/dist/cjs/tools/memory/index.cjs +58 -0
- package/dist/cjs/tools/memory/index.cjs.map +1 -0
- package/dist/cjs/tools/memory/memoryAppendTool.cjs +69 -0
- package/dist/cjs/tools/memory/memoryAppendTool.cjs.map +1 -0
- package/dist/cjs/tools/memory/memoryGetTool.cjs +49 -0
- package/dist/cjs/tools/memory/memoryGetTool.cjs.map +1 -0
- package/dist/cjs/tools/memory/memorySearchTool.cjs +65 -0
- package/dist/cjs/tools/memory/memorySearchTool.cjs.map +1 -0
- package/dist/cjs/tools/memory/shared.cjs +106 -0
- package/dist/cjs/tools/memory/shared.cjs.map +1 -0
- package/dist/cjs/types/graph.cjs.map +1 -1
- package/dist/cjs/utils/childAgentContext.cjs +242 -0
- package/dist/cjs/utils/childAgentContext.cjs.map +1 -0
- package/dist/cjs/utils/events.cjs +36 -4
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/cjs/utils/finishReasons.cjs +44 -0
- package/dist/cjs/utils/finishReasons.cjs.map +1 -0
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/logging.cjs +34 -0
- package/dist/cjs/utils/logging.cjs.map +1 -0
- package/dist/cjs/utils/toolCallNormalization.cjs +250 -0
- package/dist/cjs/utils/toolCallNormalization.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +20 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/spawnPath.mjs +95 -0
- package/dist/esm/common/spawnPath.mjs.map +1 -0
- package/dist/esm/graphs/Graph.mjs +87 -31
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/HandoffRegistry.mjs +141 -0
- package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -0
- package/dist/esm/graphs/MultiAgentGraph.mjs +587 -184
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/graphs/phases/flushLoop.mjs +209 -0
- package/dist/esm/graphs/phases/flushLoop.mjs.map +1 -0
- package/dist/esm/graphs/phases/memoryFlushPhase.mjs +99 -0
- package/dist/esm/graphs/phases/memoryFlushPhase.mjs.map +1 -0
- package/dist/esm/llm/bedrock/index.mjs +4 -3
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/main.mjs +21 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/memory/citations.mjs +64 -0
- package/dist/esm/memory/citations.mjs.map +1 -0
- package/dist/esm/memory/compositeBackend.mjs +58 -0
- package/dist/esm/memory/compositeBackend.mjs.map +1 -0
- package/dist/esm/memory/constants.mjs +198 -0
- package/dist/esm/memory/constants.mjs.map +1 -0
- package/dist/esm/memory/embeddings.mjs +148 -0
- package/dist/esm/memory/embeddings.mjs.map +1 -0
- package/dist/esm/memory/factory.mjs +93 -0
- package/dist/esm/memory/factory.mjs.map +1 -0
- package/dist/esm/memory/migrate.mjs +78 -0
- package/dist/esm/memory/migrate.mjs.map +1 -0
- package/dist/esm/memory/mmr.mjs +130 -0
- package/dist/esm/memory/mmr.mjs.map +1 -0
- package/dist/esm/memory/paths.mjs +207 -0
- package/dist/esm/memory/paths.mjs.map +1 -0
- package/dist/esm/memory/pgvectorStore.mjs +223 -0
- package/dist/esm/memory/pgvectorStore.mjs.map +1 -0
- package/dist/esm/memory/recallTracking.mjs +94 -0
- package/dist/esm/memory/recallTracking.mjs.map +1 -0
- package/dist/esm/memory/schema.sql +51 -0
- package/dist/esm/memory/temporalDecay.mjs +110 -0
- package/dist/esm/memory/temporalDecay.mjs.map +1 -0
- package/dist/esm/nodes/ApprovalGateNode.mjs +1 -1
- package/dist/esm/nodes/ApprovalGateNode.mjs.map +1 -1
- package/dist/esm/prompts/memoryFlushPrompt.mjs +44 -0
- package/dist/esm/prompts/memoryFlushPrompt.mjs.map +1 -0
- package/dist/esm/run.mjs +16 -3
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +4 -4
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/AskUser.mjs +6 -1
- package/dist/esm/tools/AskUser.mjs.map +1 -1
- package/dist/esm/tools/BrowserTools.mjs +1 -1
- package/dist/esm/tools/BrowserTools.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +128 -11
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/approval/constants.mjs +2 -2
- package/dist/esm/tools/approval/constants.mjs.map +1 -1
- package/dist/esm/tools/memory/index.mjs +46 -0
- package/dist/esm/tools/memory/index.mjs.map +1 -0
- package/dist/esm/tools/memory/memoryAppendTool.mjs +67 -0
- package/dist/esm/tools/memory/memoryAppendTool.mjs.map +1 -0
- package/dist/esm/tools/memory/memoryGetTool.mjs +47 -0
- package/dist/esm/tools/memory/memoryGetTool.mjs.map +1 -0
- package/dist/esm/tools/memory/memorySearchTool.mjs +63 -0
- package/dist/esm/tools/memory/memorySearchTool.mjs.map +1 -0
- package/dist/esm/tools/memory/shared.mjs +98 -0
- package/dist/esm/tools/memory/shared.mjs.map +1 -0
- package/dist/esm/types/graph.mjs.map +1 -1
- package/dist/esm/utils/childAgentContext.mjs +237 -0
- package/dist/esm/utils/childAgentContext.mjs.map +1 -0
- package/dist/esm/utils/events.mjs +36 -5
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/esm/utils/finishReasons.mjs +41 -0
- package/dist/esm/utils/finishReasons.mjs.map +1 -0
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/logging.mjs +31 -0
- package/dist/esm/utils/logging.mjs.map +1 -0
- package/dist/esm/utils/toolCallNormalization.mjs +247 -0
- package/dist/esm/utils/toolCallNormalization.mjs.map +1 -0
- package/dist/types/common/index.d.ts +1 -0
- package/dist/types/common/spawnPath.d.ts +59 -0
- package/dist/types/graphs/HandoffRegistry.d.ts +97 -0
- package/dist/types/graphs/MultiAgentGraph.d.ts +58 -18
- package/dist/types/graphs/index.d.ts +1 -0
- package/dist/types/graphs/phases/flushLoop.d.ts +106 -0
- package/dist/types/graphs/phases/memoryFlushPhase.d.ts +100 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/memory/__tests__/mockBackend.d.ts +40 -0
- package/dist/types/memory/citations.d.ts +39 -0
- package/dist/types/memory/compositeBackend.d.ts +30 -0
- package/dist/types/memory/constants.d.ts +121 -0
- package/dist/types/memory/embeddings.d.ts +15 -0
- package/dist/types/memory/factory.d.ts +23 -0
- package/dist/types/memory/index.d.ts +21 -0
- package/dist/types/memory/migrate.d.ts +14 -0
- package/dist/types/memory/mmr.d.ts +50 -0
- package/dist/types/memory/paths.d.ts +107 -0
- package/dist/types/memory/pgvectorStore.d.ts +56 -0
- package/dist/types/memory/recallTracking.d.ts +30 -0
- package/dist/types/memory/temporalDecay.d.ts +53 -0
- package/dist/types/memory/types.d.ts +182 -0
- package/dist/types/prompts/memoryFlushPrompt.d.ts +54 -0
- package/dist/types/run.d.ts +1 -0
- package/dist/types/tools/AskUser.d.ts +1 -1
- package/dist/types/tools/BrowserTools.d.ts +2 -2
- package/dist/types/tools/approval/constants.d.ts +2 -2
- package/dist/types/tools/memory/index.d.ts +39 -0
- package/dist/types/tools/memory/memoryAppendTool.d.ts +27 -0
- package/dist/types/tools/memory/memoryGetTool.d.ts +22 -0
- package/dist/types/tools/memory/memorySearchTool.d.ts +22 -0
- package/dist/types/tools/memory/shared.d.ts +106 -0
- package/dist/types/types/graph.d.ts +16 -3
- package/dist/types/utils/childAgentContext.d.ts +99 -0
- package/dist/types/utils/events.d.ts +21 -0
- package/dist/types/utils/finishReasons.d.ts +32 -0
- package/dist/types/utils/logging.d.ts +2 -0
- package/dist/types/utils/toolCallNormalization.d.ts +44 -0
- package/package.json +6 -4
- package/src/agents/AgentContext.ts +26 -3
- package/src/common/__tests__/enum.test.ts +4 -2
- package/src/common/__tests__/spawnPath.test.ts +110 -0
- package/src/common/index.ts +1 -0
- package/src/common/spawnPath.ts +101 -0
- package/src/graphs/Graph.ts +94 -43
- package/src/graphs/HandoffRegistry.ts +199 -0
- package/src/graphs/MultiAgentGraph.ts +694 -226
- package/src/graphs/__tests__/HandoffRegistry.test.ts +410 -0
- package/src/graphs/__tests__/multi-agent-delegate.test.ts +61 -16
- package/src/graphs/__tests__/multi-agent-edges.test.ts +4 -2
- package/src/graphs/__tests__/multi-agent-nested-subgraph.test.ts +221 -0
- package/src/graphs/__tests__/structured-output.integration.test.ts +212 -118
- package/src/graphs/contextManagement.e2e.test.ts +1 -1
- package/src/graphs/index.ts +1 -0
- package/src/graphs/phases/__tests__/flushLoop.test.ts +264 -0
- package/src/graphs/phases/__tests__/memoryFlushPhase.test.ts +37 -0
- package/src/graphs/phases/__tests__/runMemoryFlush.test.ts +150 -0
- package/src/graphs/phases/flushLoop.ts +303 -0
- package/src/graphs/phases/memoryFlushPhase.ts +209 -0
- package/src/index.ts +30 -1
- package/src/llm/bedrock/index.ts +4 -5
- package/src/memory/__tests__/citations.test.ts +61 -0
- package/src/memory/__tests__/compositeBackend.test.ts +79 -0
- package/src/memory/__tests__/isolation.test.ts +206 -0
- package/src/memory/__tests__/mmr.test.ts +148 -0
- package/src/memory/__tests__/mockBackend.ts +161 -0
- package/src/memory/__tests__/paths.test.ts +168 -0
- package/src/memory/__tests__/recallTracking.test.ts +96 -0
- package/src/memory/__tests__/temporalDecay.test.ts +151 -0
- package/src/memory/citations.ts +80 -0
- package/src/memory/compositeBackend.ts +99 -0
- package/src/memory/constants.ts +229 -0
- package/src/memory/embeddings.ts +188 -0
- package/src/memory/factory.ts +111 -0
- package/src/memory/index.ts +46 -0
- package/src/memory/migrate.ts +116 -0
- package/src/memory/mmr.ts +161 -0
- package/src/memory/paths.ts +258 -0
- package/src/memory/pgvectorStore.ts +324 -0
- package/src/memory/recallTracking.ts +127 -0
- package/src/memory/schema.sql +51 -0
- package/src/memory/temporalDecay.ts +134 -0
- package/src/memory/types.ts +185 -0
- package/src/nodes/ApprovalGateNode.ts +4 -10
- package/src/nodes/__tests__/ApprovalGateNode.test.ts +11 -20
- package/src/prompts/memoryFlushPrompt.ts +78 -0
- package/src/run.ts +17 -6
- package/src/scripts/test-bedrock-handoff-autonomous.ts +56 -20
- package/src/specs/agent-handoffs-bedrock.integration.test.ts +8 -5
- package/src/specs/agent-handoffs.test.ts +8 -2
- package/src/stream.ts +4 -6
- package/src/tools/AskUser.ts +7 -2
- package/src/tools/BrowserTools.ts +3 -5
- package/src/tools/ToolNode.ts +150 -13
- package/src/tools/__tests__/ToolApproval.test.ts +22 -9
- package/src/tools/approval/__tests__/constants.test.ts +4 -4
- package/src/tools/approval/constants.ts +2 -2
- package/src/tools/memory/__tests__/memoryTools.test.ts +205 -0
- package/src/tools/memory/index.ts +96 -0
- package/src/tools/memory/memoryAppendTool.ts +101 -0
- package/src/tools/memory/memoryGetTool.ts +53 -0
- package/src/tools/memory/memorySearchTool.ts +80 -0
- package/src/tools/memory/shared.ts +169 -0
- package/src/tools/search/search.test.ts +6 -1
- package/src/types/graph.ts +16 -3
- package/src/utils/__tests__/childAgentContext.test.ts +217 -0
- package/src/utils/__tests__/finishReasons.test.ts +55 -0
- package/src/utils/__tests__/toolCallNormalization.test.ts +181 -0
- package/src/utils/childAgentContext.ts +259 -0
- package/src/utils/events.ts +37 -4
- package/src/utils/finishReasons.ts +40 -0
- package/src/utils/llm.ts +0 -1
- package/src/utils/logging.ts +45 -8
- package/src/utils/toolCallNormalization.ts +271 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { HandoffRegistry } from '../HandoffRegistry';
|
|
2
|
+
import type { BaseGraphState } from '@/types';
|
|
3
|
+
|
|
4
|
+
/** Helper to create a resolved promise simulating a child agent completion */
|
|
5
|
+
function createChildPromise(
|
|
6
|
+
resultText: string,
|
|
7
|
+
delayMs = 0
|
|
8
|
+
): Promise<BaseGraphState> {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const fn = () =>
|
|
11
|
+
resolve({
|
|
12
|
+
messages: [
|
|
13
|
+
{
|
|
14
|
+
getType: () => 'ai',
|
|
15
|
+
content: resultText,
|
|
16
|
+
} as any,
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
if (delayMs > 0) {
|
|
20
|
+
setTimeout(fn, delayMs);
|
|
21
|
+
} else {
|
|
22
|
+
fn();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Helper to create a failing promise */
|
|
28
|
+
function createFailingPromise(
|
|
29
|
+
errorMsg: string,
|
|
30
|
+
delayMs = 0
|
|
31
|
+
): Promise<BaseGraphState> {
|
|
32
|
+
return new Promise((_resolve, reject) => {
|
|
33
|
+
const fn = () => reject(new Error(errorMsg));
|
|
34
|
+
if (delayMs > 0) {
|
|
35
|
+
setTimeout(fn, delayMs);
|
|
36
|
+
} else {
|
|
37
|
+
fn();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Stub extractResult — returns content string from last AI message */
|
|
43
|
+
function extractResult(messages: any[], _agentId: string): string {
|
|
44
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
45
|
+
if (
|
|
46
|
+
messages[i].getType() === 'ai' &&
|
|
47
|
+
typeof messages[i].content === 'string'
|
|
48
|
+
) {
|
|
49
|
+
return messages[i].content;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return '[no result]';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Stub truncateResult — pass through */
|
|
56
|
+
function truncateResult(text: string, _maxChars: number): string {
|
|
57
|
+
return text;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('HandoffRegistry', () => {
|
|
61
|
+
let registry: HandoffRegistry;
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
registry = new HandoffRegistry();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('spawn', () => {
|
|
68
|
+
it('should register a new handoff record', () => {
|
|
69
|
+
const promise = createChildPromise('test result');
|
|
70
|
+
|
|
71
|
+
registry.spawn({
|
|
72
|
+
id: 'agent-1',
|
|
73
|
+
name: 'Researcher',
|
|
74
|
+
task: 'Find information',
|
|
75
|
+
promise,
|
|
76
|
+
extractResult,
|
|
77
|
+
truncateResult,
|
|
78
|
+
maxResultChars: 5000,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(registry.size).toBe(1);
|
|
82
|
+
expect(registry.hasPending()).toBe(true);
|
|
83
|
+
|
|
84
|
+
const record = registry.get('agent-1');
|
|
85
|
+
expect(record).toBeDefined();
|
|
86
|
+
expect(record!.name).toBe('Researcher');
|
|
87
|
+
expect(record!.task).toBe('Find information');
|
|
88
|
+
expect(record!.status).toBe('running');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should update record on completion', async () => {
|
|
92
|
+
const promise = createChildPromise('research findings');
|
|
93
|
+
|
|
94
|
+
registry.spawn({
|
|
95
|
+
id: 'agent-1',
|
|
96
|
+
name: 'Researcher',
|
|
97
|
+
task: 'Find info',
|
|
98
|
+
promise,
|
|
99
|
+
extractResult,
|
|
100
|
+
truncateResult,
|
|
101
|
+
maxResultChars: 5000,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Wait for promise to resolve
|
|
105
|
+
await promise;
|
|
106
|
+
// Yield to let .then handler run
|
|
107
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
108
|
+
|
|
109
|
+
const record = registry.get('agent-1')!;
|
|
110
|
+
expect(record.status).toBe('completed');
|
|
111
|
+
expect(record.resultText).toBe('research findings');
|
|
112
|
+
expect(record.durationMs).toBeGreaterThanOrEqual(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should update record on failure', async () => {
|
|
116
|
+
const promise = createFailingPromise('timeout');
|
|
117
|
+
|
|
118
|
+
registry.spawn({
|
|
119
|
+
id: 'agent-1',
|
|
120
|
+
name: 'Researcher',
|
|
121
|
+
task: 'Find info',
|
|
122
|
+
promise,
|
|
123
|
+
extractResult,
|
|
124
|
+
truncateResult,
|
|
125
|
+
maxResultChars: 5000,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await promise;
|
|
130
|
+
} catch {
|
|
131
|
+
// expected
|
|
132
|
+
}
|
|
133
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
134
|
+
|
|
135
|
+
const record = registry.get('agent-1')!;
|
|
136
|
+
expect(record.status).toBe('failed');
|
|
137
|
+
expect(record.error).toBe('timeout');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should call onComplete callback on completion', async () => {
|
|
141
|
+
const onComplete = jest.fn();
|
|
142
|
+
const promise = createChildPromise('done');
|
|
143
|
+
|
|
144
|
+
registry.spawn({
|
|
145
|
+
id: 'agent-1',
|
|
146
|
+
name: 'Worker',
|
|
147
|
+
task: 'Do work',
|
|
148
|
+
promise,
|
|
149
|
+
extractResult,
|
|
150
|
+
truncateResult,
|
|
151
|
+
maxResultChars: 5000,
|
|
152
|
+
onComplete,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await promise;
|
|
156
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
157
|
+
|
|
158
|
+
expect(onComplete).toHaveBeenCalledTimes(1);
|
|
159
|
+
expect(onComplete).toHaveBeenCalledWith(
|
|
160
|
+
expect.objectContaining({
|
|
161
|
+
id: 'agent-1',
|
|
162
|
+
status: 'completed',
|
|
163
|
+
resultText: 'done',
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should call onComplete callback on failure', async () => {
|
|
169
|
+
const onComplete = jest.fn();
|
|
170
|
+
const promise = createFailingPromise('oops');
|
|
171
|
+
|
|
172
|
+
registry.spawn({
|
|
173
|
+
id: 'agent-1',
|
|
174
|
+
name: 'Worker',
|
|
175
|
+
task: 'Do work',
|
|
176
|
+
promise,
|
|
177
|
+
extractResult,
|
|
178
|
+
truncateResult,
|
|
179
|
+
maxResultChars: 5000,
|
|
180
|
+
onComplete,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
await promise;
|
|
185
|
+
} catch {
|
|
186
|
+
// expected
|
|
187
|
+
}
|
|
188
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
189
|
+
|
|
190
|
+
expect(onComplete).toHaveBeenCalledTimes(1);
|
|
191
|
+
expect(onComplete).toHaveBeenCalledWith(
|
|
192
|
+
expect.objectContaining({
|
|
193
|
+
id: 'agent-1',
|
|
194
|
+
status: 'failed',
|
|
195
|
+
error: 'oops',
|
|
196
|
+
})
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('listing methods', () => {
|
|
202
|
+
it('should list pending and completed separately', async () => {
|
|
203
|
+
const p1 = createChildPromise('result 1');
|
|
204
|
+
const p2 = createChildPromise('result 2', 100);
|
|
205
|
+
|
|
206
|
+
registry.spawn({
|
|
207
|
+
id: 'agent-1',
|
|
208
|
+
name: 'Agent 1',
|
|
209
|
+
task: 'task 1',
|
|
210
|
+
promise: p1,
|
|
211
|
+
extractResult,
|
|
212
|
+
truncateResult,
|
|
213
|
+
maxResultChars: 5000,
|
|
214
|
+
});
|
|
215
|
+
registry.spawn({
|
|
216
|
+
id: 'agent-2',
|
|
217
|
+
name: 'Agent 2',
|
|
218
|
+
task: 'task 2',
|
|
219
|
+
promise: p2,
|
|
220
|
+
extractResult,
|
|
221
|
+
truncateResult,
|
|
222
|
+
maxResultChars: 5000,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Wait for first to complete
|
|
226
|
+
await p1;
|
|
227
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
228
|
+
|
|
229
|
+
expect(registry.listCompleted()).toHaveLength(1);
|
|
230
|
+
expect(registry.listCompleted()[0].id).toBe('agent-1');
|
|
231
|
+
expect(registry.listPending()).toHaveLength(1);
|
|
232
|
+
expect(registry.listPending()[0].id).toBe('agent-2');
|
|
233
|
+
expect(registry.listAll()).toHaveLength(2);
|
|
234
|
+
|
|
235
|
+
// Wait for second
|
|
236
|
+
await p2;
|
|
237
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
238
|
+
|
|
239
|
+
expect(registry.listCompleted()).toHaveLength(2);
|
|
240
|
+
expect(registry.listPending()).toHaveLength(0);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('waitForAll', () => {
|
|
245
|
+
it('should wait for all pending handoffs', async () => {
|
|
246
|
+
const p1 = createChildPromise('result 1', 10);
|
|
247
|
+
const p2 = createChildPromise('result 2', 20);
|
|
248
|
+
|
|
249
|
+
registry.spawn({
|
|
250
|
+
id: 'agent-1',
|
|
251
|
+
name: 'Agent 1',
|
|
252
|
+
task: 'task 1',
|
|
253
|
+
promise: p1,
|
|
254
|
+
extractResult,
|
|
255
|
+
truncateResult,
|
|
256
|
+
maxResultChars: 5000,
|
|
257
|
+
});
|
|
258
|
+
registry.spawn({
|
|
259
|
+
id: 'agent-2',
|
|
260
|
+
name: 'Agent 2',
|
|
261
|
+
task: 'task 2',
|
|
262
|
+
promise: p2,
|
|
263
|
+
extractResult,
|
|
264
|
+
truncateResult,
|
|
265
|
+
maxResultChars: 5000,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const results = await registry.waitForAll();
|
|
269
|
+
|
|
270
|
+
expect(results).toHaveLength(2);
|
|
271
|
+
expect(results.every((r) => r.status === 'completed')).toBe(true);
|
|
272
|
+
expect(results[0].resultText).toBe('result 1');
|
|
273
|
+
expect(results[1].resultText).toBe('result 2');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should return immediately when no pending handoffs', async () => {
|
|
277
|
+
const results = await registry.waitForAll();
|
|
278
|
+
expect(results).toHaveLength(0);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('waitForAny', () => {
|
|
283
|
+
it('should return when any handoff completes', async () => {
|
|
284
|
+
const p1 = createChildPromise('fast result', 10);
|
|
285
|
+
const p2 = createChildPromise('slow result', 200);
|
|
286
|
+
|
|
287
|
+
registry.spawn({
|
|
288
|
+
id: 'agent-1',
|
|
289
|
+
name: 'Fast',
|
|
290
|
+
task: 'fast task',
|
|
291
|
+
promise: p1,
|
|
292
|
+
extractResult,
|
|
293
|
+
truncateResult,
|
|
294
|
+
maxResultChars: 5000,
|
|
295
|
+
});
|
|
296
|
+
registry.spawn({
|
|
297
|
+
id: 'agent-2',
|
|
298
|
+
name: 'Slow',
|
|
299
|
+
task: 'slow task',
|
|
300
|
+
promise: p2,
|
|
301
|
+
extractResult,
|
|
302
|
+
truncateResult,
|
|
303
|
+
maxResultChars: 5000,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const completed = await registry.waitForAny();
|
|
307
|
+
|
|
308
|
+
// At least the fast one should be completed
|
|
309
|
+
expect(completed.length).toBeGreaterThanOrEqual(1);
|
|
310
|
+
expect(completed.some((r) => r.id === 'agent-1')).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('clear', () => {
|
|
315
|
+
it('should clear all records', () => {
|
|
316
|
+
registry.spawn({
|
|
317
|
+
id: 'agent-1',
|
|
318
|
+
name: 'Agent',
|
|
319
|
+
task: 'task',
|
|
320
|
+
promise: createChildPromise('result'),
|
|
321
|
+
extractResult,
|
|
322
|
+
truncateResult,
|
|
323
|
+
maxResultChars: 5000,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
expect(registry.size).toBe(1);
|
|
327
|
+
registry.clear();
|
|
328
|
+
expect(registry.size).toBe(0);
|
|
329
|
+
expect(registry.hasPending()).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe('parallel spawn workflow', () => {
|
|
334
|
+
it('should handle the full orchestrator workflow: spawn → collect → synthesize', async () => {
|
|
335
|
+
// Step 1: Orchestrator spawns two agents in parallel
|
|
336
|
+
const p1 = createChildPromise('Researcher found: X=42', 10);
|
|
337
|
+
const p2 = createChildPromise('Analyst found: Y=100', 20);
|
|
338
|
+
|
|
339
|
+
registry.spawn({
|
|
340
|
+
id: 'researcher',
|
|
341
|
+
name: 'Researcher',
|
|
342
|
+
task: 'Find X',
|
|
343
|
+
promise: p1,
|
|
344
|
+
extractResult,
|
|
345
|
+
truncateResult,
|
|
346
|
+
maxResultChars: 5000,
|
|
347
|
+
});
|
|
348
|
+
registry.spawn({
|
|
349
|
+
id: 'analyst',
|
|
350
|
+
name: 'Analyst',
|
|
351
|
+
task: 'Find Y',
|
|
352
|
+
promise: p2,
|
|
353
|
+
extractResult,
|
|
354
|
+
truncateResult,
|
|
355
|
+
maxResultChars: 5000,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Step 2: Orchestrator checks status (non-blocking)
|
|
359
|
+
expect(registry.hasPending()).toBe(true);
|
|
360
|
+
const status = registry.listAll();
|
|
361
|
+
expect(status).toHaveLength(2);
|
|
362
|
+
|
|
363
|
+
// Step 3: Orchestrator collects all results
|
|
364
|
+
const results = await registry.waitForAll();
|
|
365
|
+
expect(results).toHaveLength(2);
|
|
366
|
+
expect(results.every((r) => r.status === 'completed')).toBe(true);
|
|
367
|
+
|
|
368
|
+
// Step 4: Orchestrator uses results for next delegation
|
|
369
|
+
const researchResult = results.find((r) => r.id === 'researcher')!;
|
|
370
|
+
expect(researchResult.resultText).toBe('Researcher found: X=42');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should handle sequential spawn workflow: spawn → collect → spawn with data → collect', async () => {
|
|
374
|
+
// Round 1: Spawn researcher
|
|
375
|
+
const p1 = createChildPromise('Research: The answer is 42', 10);
|
|
376
|
+
registry.spawn({
|
|
377
|
+
id: 'researcher',
|
|
378
|
+
name: 'Researcher',
|
|
379
|
+
task: 'Find the answer',
|
|
380
|
+
promise: p1,
|
|
381
|
+
extractResult,
|
|
382
|
+
truncateResult,
|
|
383
|
+
maxResultChars: 5000,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Collect round 1 results
|
|
387
|
+
const round1 = await registry.waitForAll();
|
|
388
|
+
const researchData = round1[0].resultText!;
|
|
389
|
+
expect(researchData).toContain('42');
|
|
390
|
+
|
|
391
|
+
// Round 2: Spawn emailer with real data from round 1
|
|
392
|
+
const p2 = createChildPromise('Email sent with answer: 42', 10);
|
|
393
|
+
registry.spawn({
|
|
394
|
+
id: 'emailer',
|
|
395
|
+
name: 'Email Agent',
|
|
396
|
+
task: `Send email with: ${researchData}`,
|
|
397
|
+
promise: p2,
|
|
398
|
+
extractResult,
|
|
399
|
+
truncateResult,
|
|
400
|
+
maxResultChars: 5000,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Collect round 2 results
|
|
404
|
+
const round2 = await registry.waitForAll();
|
|
405
|
+
const emailResult = round2.find((r) => r.id === 'emailer')!;
|
|
406
|
+
expect(emailResult.status).toBe('completed');
|
|
407
|
+
expect(emailResult.resultText).toContain('Email sent');
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
});
|
|
@@ -100,7 +100,9 @@ describe('extractHandoffResult', () => {
|
|
|
100
100
|
new AIMessage('found the data'),
|
|
101
101
|
new AIMessage({
|
|
102
102
|
content: '',
|
|
103
|
-
tool_calls: [
|
|
103
|
+
tool_calls: [
|
|
104
|
+
{ name: 'search', args: {}, id: 'tc1', type: 'tool_call' },
|
|
105
|
+
],
|
|
104
106
|
}),
|
|
105
107
|
new ToolMessage({ content: 'search result', tool_call_id: 'tc1' }),
|
|
106
108
|
];
|
|
@@ -109,16 +111,18 @@ describe('extractHandoffResult', () => {
|
|
|
109
111
|
});
|
|
110
112
|
|
|
111
113
|
it('returns fallback message when no AIMessage has text', () => {
|
|
112
|
-
const messages: BaseMessage[] = [
|
|
113
|
-
new HumanMessage('task'),
|
|
114
|
-
];
|
|
114
|
+
const messages: BaseMessage[] = [new HumanMessage('task')];
|
|
115
115
|
const result = MultiAgentGraph.extractHandoffResult(messages, 'researcher');
|
|
116
|
-
expect(result).toBe(
|
|
116
|
+
expect(result).toBe(
|
|
117
|
+
'[Agent "researcher" completed but produced no text output]'
|
|
118
|
+
);
|
|
117
119
|
});
|
|
118
120
|
|
|
119
121
|
it('returns fallback for empty messages array', () => {
|
|
120
122
|
const result = MultiAgentGraph.extractHandoffResult([], 'agent');
|
|
121
|
-
expect(result).toBe(
|
|
123
|
+
expect(result).toBe(
|
|
124
|
+
'[Agent "agent" completed but produced no text output]'
|
|
125
|
+
);
|
|
122
126
|
});
|
|
123
127
|
|
|
124
128
|
it('trims whitespace from extracted text', () => {
|
|
@@ -160,7 +164,8 @@ describe('truncateHandoffResult', () => {
|
|
|
160
164
|
const maxChars = 200;
|
|
161
165
|
const truncated = MultiAgentGraph.truncateHandoffResult(longText, maxChars);
|
|
162
166
|
|
|
163
|
-
const notice =
|
|
167
|
+
const notice =
|
|
168
|
+
'\n\n[... handoff output truncated — middle section omitted to fit parent context ...]\n\n';
|
|
164
169
|
const available = maxChars - notice.length;
|
|
165
170
|
const expectedHead = Math.floor(available * 0.6);
|
|
166
171
|
const expectedTail = available - expectedHead;
|
|
@@ -249,7 +254,12 @@ describe('prepareHandoffMessages', () => {
|
|
|
249
254
|
new AIMessage({
|
|
250
255
|
content: 'Let me call a tool',
|
|
251
256
|
tool_calls: [
|
|
252
|
-
{
|
|
257
|
+
{
|
|
258
|
+
name: 'lc_handoff_to_researcher',
|
|
259
|
+
args: { instructions: 'research this' },
|
|
260
|
+
id: 'tc1',
|
|
261
|
+
type: 'tool_call',
|
|
262
|
+
},
|
|
253
263
|
],
|
|
254
264
|
}),
|
|
255
265
|
// No ToolMessage for tc1 — it's orphaned
|
|
@@ -268,10 +278,19 @@ describe('prepareHandoffMessages', () => {
|
|
|
268
278
|
new AIMessage({
|
|
269
279
|
content: 'I will search for you',
|
|
270
280
|
tool_calls: [
|
|
271
|
-
{
|
|
281
|
+
{
|
|
282
|
+
name: 'web_search',
|
|
283
|
+
args: { query: 'test' },
|
|
284
|
+
id: 'tc1',
|
|
285
|
+
type: 'tool_call',
|
|
286
|
+
},
|
|
272
287
|
],
|
|
273
288
|
}),
|
|
274
|
-
new ToolMessage({
|
|
289
|
+
new ToolMessage({
|
|
290
|
+
content: 'Found 3 results about testing',
|
|
291
|
+
tool_call_id: 'tc1',
|
|
292
|
+
name: 'web_search',
|
|
293
|
+
}),
|
|
275
294
|
new AIMessage('Based on the search, here is what I found.'),
|
|
276
295
|
];
|
|
277
296
|
const result = MultiAgentGraph.prepareHandoffMessages(messages);
|
|
@@ -302,8 +321,16 @@ describe('prepareHandoffMessages', () => {
|
|
|
302
321
|
{ name: 'tool_b', args: {}, id: 'tc2', type: 'tool_call' },
|
|
303
322
|
],
|
|
304
323
|
}),
|
|
305
|
-
new ToolMessage({
|
|
306
|
-
|
|
324
|
+
new ToolMessage({
|
|
325
|
+
content: 'result a',
|
|
326
|
+
tool_call_id: 'tc1',
|
|
327
|
+
name: 'tool_a',
|
|
328
|
+
}),
|
|
329
|
+
new ToolMessage({
|
|
330
|
+
content: 'result b',
|
|
331
|
+
tool_call_id: 'tc2',
|
|
332
|
+
name: 'tool_b',
|
|
333
|
+
}),
|
|
307
334
|
new AIMessage('Final answer.'),
|
|
308
335
|
];
|
|
309
336
|
const result = MultiAgentGraph.prepareHandoffMessages(messages);
|
|
@@ -322,15 +349,29 @@ describe('prepareHandoffMessages', () => {
|
|
|
322
349
|
new AIMessage({
|
|
323
350
|
content: 'I will delegate to the researcher first.',
|
|
324
351
|
tool_calls: [
|
|
325
|
-
{
|
|
352
|
+
{
|
|
353
|
+
name: 'lc_handoff_to_researcher',
|
|
354
|
+
args: { instructions: 'research AI trends' },
|
|
355
|
+
id: 'h1',
|
|
356
|
+
type: 'tool_call',
|
|
357
|
+
},
|
|
326
358
|
],
|
|
327
359
|
}),
|
|
328
|
-
new ToolMessage({
|
|
360
|
+
new ToolMessage({
|
|
361
|
+
content: 'Research findings: AI adoption is growing rapidly...',
|
|
362
|
+
tool_call_id: 'h1',
|
|
363
|
+
name: 'lc_handoff_to_researcher',
|
|
364
|
+
}),
|
|
329
365
|
// Orchestrator's response after getting research back
|
|
330
366
|
new AIMessage({
|
|
331
367
|
content: 'Now I will delegate to the writer.',
|
|
332
368
|
tool_calls: [
|
|
333
|
-
{
|
|
369
|
+
{
|
|
370
|
+
name: 'lc_handoff_to_writer',
|
|
371
|
+
args: { instructions: 'write report based on findings' },
|
|
372
|
+
id: 'h2',
|
|
373
|
+
type: 'tool_call',
|
|
374
|
+
},
|
|
334
375
|
],
|
|
335
376
|
}),
|
|
336
377
|
// h2 is orphaned — writer hasn't returned yet (this is the current handoff)
|
|
@@ -399,7 +440,11 @@ describe('prepareHandoffMessages', () => {
|
|
|
399
440
|
{ name: 'some_tool', args: {}, id: 'tc1', type: 'tool_call' },
|
|
400
441
|
],
|
|
401
442
|
}),
|
|
402
|
-
new ToolMessage({
|
|
443
|
+
new ToolMessage({
|
|
444
|
+
content: 'tool output',
|
|
445
|
+
tool_call_id: 'tc1',
|
|
446
|
+
name: 'some_tool',
|
|
447
|
+
}),
|
|
403
448
|
];
|
|
404
449
|
const result = MultiAgentGraph.prepareHandoffMessages(messages);
|
|
405
450
|
// Compacted AI is trailing → converted to HumanMessage
|
|
@@ -163,7 +163,8 @@ describe('edge categorization', () => {
|
|
|
163
163
|
const edges: GraphEdge[] = [
|
|
164
164
|
{ from: 'supervisor', to: 'researcher', edgeType: EdgeType.HANDOFF },
|
|
165
165
|
];
|
|
166
|
-
const { sequenceEdges, transferEdges, handoffEdges } =
|
|
166
|
+
const { sequenceEdges, transferEdges, handoffEdges } =
|
|
167
|
+
categorizeEdges(edges);
|
|
167
168
|
expect(handoffEdges).toHaveLength(1);
|
|
168
169
|
expect(transferEdges).toHaveLength(0);
|
|
169
170
|
expect(sequenceEdges).toHaveLength(0);
|
|
@@ -175,7 +176,8 @@ describe('edge categorization', () => {
|
|
|
175
176
|
{ from: 'supervisor', to: 'writer', edgeType: EdgeType.TRANSFER },
|
|
176
177
|
{ from: 'supervisor', to: 'formatter', edgeType: EdgeType.SEQUENCE },
|
|
177
178
|
];
|
|
178
|
-
const { sequenceEdges, transferEdges, handoffEdges } =
|
|
179
|
+
const { sequenceEdges, transferEdges, handoffEdges } =
|
|
180
|
+
categorizeEdges(edges);
|
|
179
181
|
expect(handoffEdges).toHaveLength(1);
|
|
180
182
|
expect(transferEdges).toHaveLength(1);
|
|
181
183
|
expect(sequenceEdges).toHaveLength(1);
|