@illuma-ai/agents 1.1.15 → 1.1.17
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/common/enum.cjs +15 -13
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +173 -150
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -2
- package/dist/cjs/types/graph.cjs.map +1 -1
- package/dist/esm/common/enum.mjs +14 -12
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +174 -151
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/types/graph.mjs.map +1 -1
- package/dist/types/common/enum.d.ts +13 -11
- package/dist/types/graphs/MultiAgentGraph.d.ts +38 -36
- package/dist/types/types/graph.d.ts +22 -7
- package/package.json +1 -1
- package/src/common/__tests__/enum.test.ts +14 -6
- package/src/common/enum.ts +13 -11
- package/src/graphs/MultiAgentGraph.ts +190 -152
- package/src/graphs/__tests__/multi-agent-delegate.test.ts +44 -44
- package/src/graphs/__tests__/multi-agent-edges.test.ts +83 -85
- package/src/scripts/multi-agent-chain.js +1 -1
- package/src/scripts/multi-agent-chain.ts +1 -1
- package/src/scripts/multi-agent-document-review-chain.js +1 -1
- package/src/scripts/multi-agent-document-review-chain.ts +1 -1
- package/src/scripts/multi-agent-hybrid-flow.js +3 -3
- package/src/scripts/multi-agent-hybrid-flow.ts +3 -3
- package/src/scripts/multi-agent-parallel.js +2 -2
- package/src/scripts/multi-agent-parallel.ts +2 -2
- package/src/scripts/multi-agent-sequence.js +2 -2
- package/src/scripts/multi-agent-sequence.ts +2 -2
- package/src/scripts/multi-agent-supervisor.js +5 -5
- package/src/scripts/multi-agent-supervisor.ts +5 -5
- package/src/scripts/poc-multi-agent-comprehensive.ts +7 -7
- package/src/scripts/sequential-full-metadata-test.js +1 -1
- package/src/scripts/sequential-full-metadata-test.ts +1 -1
- package/src/scripts/test-custom-prompt-key.js +3 -3
- package/src/scripts/test-custom-prompt-key.ts +3 -3
- package/src/scripts/test-handoff-input.js +1 -1
- package/src/scripts/test-handoff-input.ts +1 -1
- package/src/scripts/test-handoff-preamble.js +1 -1
- package/src/scripts/test-handoff-preamble.ts +1 -1
- package/src/scripts/test-handoff-steering.js +3 -3
- package/src/scripts/test-handoff-steering.ts +3 -3
- package/src/scripts/test-multi-agent-list-handoff.js +1 -1
- package/src/scripts/test-multi-agent-list-handoff.ts +1 -1
- package/src/scripts/test-parallel-agent-labeling.js +2 -2
- package/src/scripts/test-parallel-agent-labeling.ts +2 -2
- package/src/scripts/test-parallel-handoffs.js +2 -2
- package/src/scripts/test-parallel-handoffs.ts +2 -2
- package/src/scripts/test-thinking-handoff-bedrock.js +1 -1
- package/src/scripts/test-thinking-handoff-bedrock.ts +1 -1
- package/src/scripts/test-thinking-handoff.js +1 -1
- package/src/scripts/test-thinking-handoff.ts +1 -1
- package/src/scripts/test-thinking-to-thinking-handoff-bedrock.js +1 -1
- package/src/scripts/test-thinking-to-thinking-handoff-bedrock.ts +1 -1
- package/src/scripts/test-tool-before-handoff-role-order.js +1 -1
- package/src/scripts/test-tool-before-handoff-role-order.ts +1 -1
- package/src/scripts/test-tools-before-handoff.js +1 -1
- package/src/scripts/test-tools-before-handoff.ts +1 -1
- package/src/specs/agent-handoffs-bedrock.integration.test.ts +6 -6
- package/src/specs/agent-handoffs.test.ts +35 -35
- package/src/specs/thinking-handoff.test.ts +9 -9
- package/src/types/graph.ts +23 -7
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit tests for the
|
|
2
|
+
* Unit tests for the handoff pattern (supervisor-delegate) in MultiAgentGraph.
|
|
3
3
|
*
|
|
4
4
|
* Tests cover:
|
|
5
|
-
* - Result extraction from child agent messages (
|
|
6
|
-
* - Result truncation for parent context protection (
|
|
5
|
+
* - Result extraction from child agent messages (extractHandoffResult)
|
|
6
|
+
* - Result truncation for parent context protection (truncateHandoffResult)
|
|
7
7
|
* - Constants and naming conventions
|
|
8
8
|
*/
|
|
9
9
|
import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
|
|
@@ -11,8 +11,8 @@ import type { BaseMessage } from '@langchain/core/messages';
|
|
|
11
11
|
import {
|
|
12
12
|
Constants,
|
|
13
13
|
EdgeType,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
DEFAULT_HANDOFF_MAX_RESULT_CHARS,
|
|
15
|
+
HANDOFF_TIMEOUT_MS,
|
|
16
16
|
} from '@/common';
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -24,38 +24,38 @@ import { MultiAgentGraph } from '../MultiAgentGraph';
|
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
// Constants
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
|
-
describe('
|
|
28
|
-
it('
|
|
29
|
-
expect(Constants.
|
|
27
|
+
describe('handoff constants', () => {
|
|
28
|
+
it('LC_HANDOFF_TO_ prefix matches expected pattern', () => {
|
|
29
|
+
expect(Constants.LC_HANDOFF_TO_).toBe('lc_handoff_to_');
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
it('
|
|
33
|
-
expect(Constants.
|
|
32
|
+
it('LC_HANDOFF_TO_ is distinct from LC_TRANSFER_TO_', () => {
|
|
33
|
+
expect(Constants.LC_HANDOFF_TO_).not.toBe(Constants.LC_TRANSFER_TO_);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
it('
|
|
37
|
-
expect(
|
|
36
|
+
it('DEFAULT_HANDOFF_MAX_RESULT_CHARS is 32768', () => {
|
|
37
|
+
expect(DEFAULT_HANDOFF_MAX_RESULT_CHARS).toBe(32768);
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
it('
|
|
41
|
-
expect(
|
|
40
|
+
it('HANDOFF_TIMEOUT_MS is 5 minutes', () => {
|
|
41
|
+
expect(HANDOFF_TIMEOUT_MS).toBe(300_000);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
it('EdgeType.
|
|
45
|
-
expect(EdgeType.
|
|
44
|
+
it('EdgeType.HANDOFF has correct value', () => {
|
|
45
|
+
expect(EdgeType.HANDOFF).toBe('handoff');
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
// ---------------------------------------------------------------------------
|
|
50
|
-
//
|
|
50
|
+
// extractHandoffResult
|
|
51
51
|
// ---------------------------------------------------------------------------
|
|
52
|
-
describe('
|
|
52
|
+
describe('extractHandoffResult', () => {
|
|
53
53
|
it('extracts text from last AIMessage with string content', () => {
|
|
54
54
|
const messages: BaseMessage[] = [
|
|
55
55
|
new HumanMessage('find sales data'),
|
|
56
56
|
new AIMessage('Here are the sales figures for Q1...'),
|
|
57
57
|
];
|
|
58
|
-
const result = MultiAgentGraph.
|
|
58
|
+
const result = MultiAgentGraph.extractHandoffResult(messages, 'researcher');
|
|
59
59
|
expect(result).toBe('Here are the sales figures for Q1...');
|
|
60
60
|
});
|
|
61
61
|
|
|
@@ -69,7 +69,7 @@ describe('extractDelegateResult', () => {
|
|
|
69
69
|
],
|
|
70
70
|
}),
|
|
71
71
|
];
|
|
72
|
-
const result = MultiAgentGraph.
|
|
72
|
+
const result = MultiAgentGraph.extractHandoffResult(messages, 'analyst');
|
|
73
73
|
expect(result).toBe('Analysis shows growth.\nRevenue up 15%.');
|
|
74
74
|
});
|
|
75
75
|
|
|
@@ -80,7 +80,7 @@ describe('extractDelegateResult', () => {
|
|
|
80
80
|
new HumanMessage('continue'),
|
|
81
81
|
new AIMessage('final result here'),
|
|
82
82
|
];
|
|
83
|
-
const result = MultiAgentGraph.
|
|
83
|
+
const result = MultiAgentGraph.extractHandoffResult(messages, 'agent');
|
|
84
84
|
expect(result).toBe('final result here');
|
|
85
85
|
});
|
|
86
86
|
|
|
@@ -90,7 +90,7 @@ describe('extractDelegateResult', () => {
|
|
|
90
90
|
new AIMessage('good result'),
|
|
91
91
|
new AIMessage(''),
|
|
92
92
|
];
|
|
93
|
-
const result = MultiAgentGraph.
|
|
93
|
+
const result = MultiAgentGraph.extractHandoffResult(messages, 'agent');
|
|
94
94
|
expect(result).toBe('good result');
|
|
95
95
|
});
|
|
96
96
|
|
|
@@ -104,7 +104,7 @@ describe('extractDelegateResult', () => {
|
|
|
104
104
|
}),
|
|
105
105
|
new ToolMessage({ content: 'search result', tool_call_id: 'tc1' }),
|
|
106
106
|
];
|
|
107
|
-
const result = MultiAgentGraph.
|
|
107
|
+
const result = MultiAgentGraph.extractHandoffResult(messages, 'agent');
|
|
108
108
|
expect(result).toBe('found the data');
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -112,12 +112,12 @@ describe('extractDelegateResult', () => {
|
|
|
112
112
|
const messages: BaseMessage[] = [
|
|
113
113
|
new HumanMessage('task'),
|
|
114
114
|
];
|
|
115
|
-
const result = MultiAgentGraph.
|
|
115
|
+
const result = MultiAgentGraph.extractHandoffResult(messages, 'researcher');
|
|
116
116
|
expect(result).toBe('[Agent "researcher" completed but produced no text output]');
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
it('returns fallback for empty messages array', () => {
|
|
120
|
-
const result = MultiAgentGraph.
|
|
120
|
+
const result = MultiAgentGraph.extractHandoffResult([], 'agent');
|
|
121
121
|
expect(result).toBe('[Agent "agent" completed but produced no text output]');
|
|
122
122
|
});
|
|
123
123
|
|
|
@@ -125,30 +125,30 @@ describe('extractDelegateResult', () => {
|
|
|
125
125
|
const messages: BaseMessage[] = [
|
|
126
126
|
new AIMessage(' result with spaces \n\n'),
|
|
127
127
|
];
|
|
128
|
-
const result = MultiAgentGraph.
|
|
128
|
+
const result = MultiAgentGraph.extractHandoffResult(messages, 'agent');
|
|
129
129
|
expect(result).toBe('result with spaces');
|
|
130
130
|
});
|
|
131
131
|
});
|
|
132
132
|
|
|
133
133
|
// ---------------------------------------------------------------------------
|
|
134
|
-
//
|
|
134
|
+
// truncateHandoffResult
|
|
135
135
|
// ---------------------------------------------------------------------------
|
|
136
|
-
describe('
|
|
136
|
+
describe('truncateHandoffResult', () => {
|
|
137
137
|
it('returns result unchanged when within budget', () => {
|
|
138
138
|
const result = 'short result';
|
|
139
|
-
expect(MultiAgentGraph.
|
|
139
|
+
expect(MultiAgentGraph.truncateHandoffResult(result, 1000)).toBe(result);
|
|
140
140
|
});
|
|
141
141
|
|
|
142
142
|
it('returns empty string unchanged', () => {
|
|
143
|
-
expect(MultiAgentGraph.
|
|
143
|
+
expect(MultiAgentGraph.truncateHandoffResult('', 1000)).toBe('');
|
|
144
144
|
});
|
|
145
145
|
|
|
146
146
|
it('truncates with head/tail split when over budget', () => {
|
|
147
147
|
const longText = 'A'.repeat(1000);
|
|
148
|
-
const truncated = MultiAgentGraph.
|
|
148
|
+
const truncated = MultiAgentGraph.truncateHandoffResult(longText, 500);
|
|
149
149
|
|
|
150
150
|
expect(truncated.length).toBeLessThanOrEqual(500);
|
|
151
|
-
expect(truncated).toContain('
|
|
151
|
+
expect(truncated).toContain('handoff output truncated');
|
|
152
152
|
// Head should be present
|
|
153
153
|
expect(truncated.startsWith('AAAA')).toBe(true);
|
|
154
154
|
// Tail should be present
|
|
@@ -158,9 +158,9 @@ describe('truncateDelegateResult', () => {
|
|
|
158
158
|
it('preserves 60/40 head/tail ratio', () => {
|
|
159
159
|
const longText = 'H'.repeat(500) + 'T'.repeat(500);
|
|
160
160
|
const maxChars = 200;
|
|
161
|
-
const truncated = MultiAgentGraph.
|
|
161
|
+
const truncated = MultiAgentGraph.truncateHandoffResult(longText, maxChars);
|
|
162
162
|
|
|
163
|
-
const notice = '\n\n[...
|
|
163
|
+
const notice = '\n\n[... handoff output truncated — middle section omitted to fit parent context ...]\n\n';
|
|
164
164
|
const available = maxChars - notice.length;
|
|
165
165
|
const expectedHead = Math.floor(available * 0.6);
|
|
166
166
|
const expectedTail = available - expectedHead;
|
|
@@ -176,13 +176,13 @@ describe('truncateDelegateResult', () => {
|
|
|
176
176
|
|
|
177
177
|
it('handles result exactly at maxChars boundary', () => {
|
|
178
178
|
const result = 'X'.repeat(100);
|
|
179
|
-
expect(MultiAgentGraph.
|
|
179
|
+
expect(MultiAgentGraph.truncateHandoffResult(result, 100)).toBe(result);
|
|
180
180
|
});
|
|
181
181
|
|
|
182
182
|
it('handles very small maxChars gracefully', () => {
|
|
183
183
|
const result = 'A'.repeat(200);
|
|
184
184
|
// When maxChars is smaller than the truncation notice itself
|
|
185
|
-
const truncated = MultiAgentGraph.
|
|
185
|
+
const truncated = MultiAgentGraph.truncateHandoffResult(result, 10);
|
|
186
186
|
expect(truncated.length).toBeLessThanOrEqual(200);
|
|
187
187
|
});
|
|
188
188
|
});
|
|
@@ -190,19 +190,19 @@ describe('truncateDelegateResult', () => {
|
|
|
190
190
|
// ---------------------------------------------------------------------------
|
|
191
191
|
// Tool naming convention
|
|
192
192
|
// ---------------------------------------------------------------------------
|
|
193
|
-
describe('
|
|
194
|
-
it('
|
|
193
|
+
describe('handoff tool naming', () => {
|
|
194
|
+
it('handoff tool name follows lc_handoff_to_ pattern', () => {
|
|
195
195
|
const agentId = 'researcher_agent_123';
|
|
196
|
-
const expectedToolName = `${Constants.
|
|
197
|
-
expect(expectedToolName).toBe('
|
|
196
|
+
const expectedToolName = `${Constants.LC_HANDOFF_TO_}${agentId}`;
|
|
197
|
+
expect(expectedToolName).toBe('lc_handoff_to_researcher_agent_123');
|
|
198
198
|
});
|
|
199
199
|
|
|
200
|
-
it('
|
|
200
|
+
it('handoff tool prefix is distinct from transfer tool prefix', () => {
|
|
201
201
|
const agentId = 'test';
|
|
202
|
-
const
|
|
202
|
+
const handoffName = `${Constants.LC_HANDOFF_TO_}${agentId}`;
|
|
203
203
|
const transferName = `${Constants.LC_TRANSFER_TO_}${agentId}`;
|
|
204
|
-
expect(
|
|
205
|
-
expect(
|
|
204
|
+
expect(handoffName).not.toBe(transferName);
|
|
205
|
+
expect(handoffName).toBe('lc_handoff_to_test');
|
|
206
206
|
expect(transferName).toBe('lc_transfer_to_test');
|
|
207
207
|
});
|
|
208
208
|
});
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* and parallel group computation in MultiAgentGraph.
|
|
4
4
|
*
|
|
5
5
|
* These test the edge classification rules without instantiating the full graph:
|
|
6
|
-
* - Explicit EdgeType.
|
|
7
|
-
* - Explicit EdgeType.
|
|
8
|
-
* - Condition-based edges →
|
|
9
|
-
* - Default single→single →
|
|
10
|
-
* - Default single→many →
|
|
6
|
+
* - Explicit EdgeType.TRANSFER → transfer
|
|
7
|
+
* - Explicit EdgeType.SEQUENCE → sequence
|
|
8
|
+
* - Condition-based edges → transfer (always, regardless of edgeType)
|
|
9
|
+
* - Default single→single → transfer
|
|
10
|
+
* - Default single→many → sequence (fan-out pattern)
|
|
11
11
|
*/
|
|
12
12
|
import { EdgeType } from '@/common';
|
|
13
13
|
import type { GraphEdge, BaseGraphState } from '@/types';
|
|
@@ -17,34 +17,34 @@ import type { GraphEdge, BaseGraphState } from '@/types';
|
|
|
17
17
|
* Kept in sync with the private method for testability.
|
|
18
18
|
*/
|
|
19
19
|
function categorizeEdges(edges: GraphEdge[]): {
|
|
20
|
-
|
|
20
|
+
sequenceEdges: GraphEdge[];
|
|
21
|
+
transferEdges: GraphEdge[];
|
|
21
22
|
handoffEdges: GraphEdge[];
|
|
22
|
-
delegateEdges: GraphEdge[];
|
|
23
23
|
} {
|
|
24
|
-
const
|
|
24
|
+
const sequenceEdges: GraphEdge[] = [];
|
|
25
|
+
const transferEdges: GraphEdge[] = [];
|
|
25
26
|
const handoffEdges: GraphEdge[] = [];
|
|
26
|
-
const delegateEdges: GraphEdge[] = [];
|
|
27
27
|
|
|
28
28
|
for (const edge of edges) {
|
|
29
|
-
if (edge.edgeType === EdgeType.
|
|
30
|
-
delegateEdges.push(edge);
|
|
31
|
-
} else if (edge.edgeType === EdgeType.DIRECT) {
|
|
32
|
-
directEdges.push(edge);
|
|
33
|
-
} else if (edge.edgeType === EdgeType.HANDOFF || edge.condition != null) {
|
|
29
|
+
if (edge.edgeType === EdgeType.HANDOFF) {
|
|
34
30
|
handoffEdges.push(edge);
|
|
31
|
+
} else if (edge.edgeType === EdgeType.SEQUENCE) {
|
|
32
|
+
sequenceEdges.push(edge);
|
|
33
|
+
} else if (edge.edgeType === EdgeType.TRANSFER || edge.condition != null) {
|
|
34
|
+
transferEdges.push(edge);
|
|
35
35
|
} else {
|
|
36
36
|
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
37
37
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
38
38
|
|
|
39
39
|
if (sources.length === 1 && destinations.length > 1) {
|
|
40
|
-
|
|
40
|
+
sequenceEdges.push(edge);
|
|
41
41
|
} else {
|
|
42
|
-
|
|
42
|
+
transferEdges.push(edge);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
return {
|
|
47
|
+
return { sequenceEdges, transferEdges, handoffEdges };
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
@@ -79,114 +79,114 @@ function findStartingNodes(
|
|
|
79
79
|
// Edge categorization
|
|
80
80
|
// ---------------------------------------------------------------------------
|
|
81
81
|
describe('edge categorization', () => {
|
|
82
|
-
it('classifies explicit EdgeType.
|
|
82
|
+
it('classifies explicit EdgeType.TRANSFER as transfer', () => {
|
|
83
83
|
const edges: GraphEdge[] = [
|
|
84
|
-
{ from: 'a', to: 'b', edgeType: EdgeType.
|
|
84
|
+
{ from: 'a', to: 'b', edgeType: EdgeType.TRANSFER },
|
|
85
85
|
];
|
|
86
|
-
const {
|
|
87
|
-
expect(
|
|
88
|
-
expect(
|
|
86
|
+
const { sequenceEdges, transferEdges } = categorizeEdges(edges);
|
|
87
|
+
expect(transferEdges).toHaveLength(1);
|
|
88
|
+
expect(sequenceEdges).toHaveLength(0);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
it('classifies explicit EdgeType.
|
|
91
|
+
it('classifies explicit EdgeType.SEQUENCE as sequence', () => {
|
|
92
92
|
const edges: GraphEdge[] = [
|
|
93
|
-
{ from: 'a', to: 'b', edgeType: EdgeType.
|
|
93
|
+
{ from: 'a', to: 'b', edgeType: EdgeType.SEQUENCE },
|
|
94
94
|
];
|
|
95
|
-
const {
|
|
96
|
-
expect(
|
|
97
|
-
expect(
|
|
95
|
+
const { sequenceEdges, transferEdges } = categorizeEdges(edges);
|
|
96
|
+
expect(sequenceEdges).toHaveLength(1);
|
|
97
|
+
expect(transferEdges).toHaveLength(0);
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
it('classifies condition-based edges as
|
|
100
|
+
it('classifies condition-based edges as transfer regardless of edgeType', () => {
|
|
101
101
|
const condition = (_state: BaseGraphState) => true;
|
|
102
102
|
const edges: GraphEdge[] = [
|
|
103
103
|
{ from: 'a', to: 'b', condition },
|
|
104
|
-
{ from: 'c', to: 'd', condition, edgeType: EdgeType.
|
|
104
|
+
{ from: 'c', to: 'd', condition, edgeType: EdgeType.TRANSFER },
|
|
105
105
|
];
|
|
106
|
-
const {
|
|
107
|
-
expect(
|
|
108
|
-
expect(
|
|
106
|
+
const { sequenceEdges, transferEdges } = categorizeEdges(edges);
|
|
107
|
+
expect(transferEdges).toHaveLength(2);
|
|
108
|
+
expect(sequenceEdges).toHaveLength(0);
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
-
it('defaults single→single edges to
|
|
111
|
+
it('defaults single→single edges to transfer', () => {
|
|
112
112
|
const edges: GraphEdge[] = [{ from: 'supervisor', to: 'worker' }];
|
|
113
|
-
const {
|
|
114
|
-
expect(
|
|
115
|
-
expect(
|
|
113
|
+
const { sequenceEdges, transferEdges } = categorizeEdges(edges);
|
|
114
|
+
expect(transferEdges).toHaveLength(1);
|
|
115
|
+
expect(sequenceEdges).toHaveLength(0);
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
-
it('defaults single→many edges to
|
|
118
|
+
it('defaults single→many edges to sequence (fan-out pattern)', () => {
|
|
119
119
|
const edges: GraphEdge[] = [
|
|
120
120
|
{ from: 'coordinator', to: ['analyst1', 'analyst2', 'analyst3'] },
|
|
121
121
|
];
|
|
122
|
-
const {
|
|
123
|
-
expect(
|
|
124
|
-
expect(
|
|
122
|
+
const { sequenceEdges, transferEdges } = categorizeEdges(edges);
|
|
123
|
+
expect(sequenceEdges).toHaveLength(1);
|
|
124
|
+
expect(transferEdges).toHaveLength(0);
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
-
it('defaults many→single edges to
|
|
127
|
+
it('defaults many→single edges to transfer', () => {
|
|
128
128
|
const edges: GraphEdge[] = [
|
|
129
129
|
{ from: ['analyst1', 'analyst2'], to: 'summarizer' },
|
|
130
130
|
];
|
|
131
|
-
const {
|
|
132
|
-
expect(
|
|
133
|
-
expect(
|
|
131
|
+
const { sequenceEdges, transferEdges } = categorizeEdges(edges);
|
|
132
|
+
expect(transferEdges).toHaveLength(1);
|
|
133
|
+
expect(sequenceEdges).toHaveLength(0);
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
it('correctly categorizes a mixed set of edges', () => {
|
|
137
137
|
const edges: GraphEdge[] = [
|
|
138
|
-
// Explicit
|
|
139
|
-
{ from: 'researcher', to: 'analyst', edgeType: EdgeType.
|
|
140
|
-
// Fan-out: defaults to
|
|
138
|
+
// Explicit sequence: sequential chain
|
|
139
|
+
{ from: 'researcher', to: 'analyst', edgeType: EdgeType.SEQUENCE },
|
|
140
|
+
// Fan-out: defaults to sequence
|
|
141
141
|
{ from: 'analyst', to: ['reviewer1', 'reviewer2'] },
|
|
142
|
-
// Explicit
|
|
143
|
-
{ from: 'supervisor', to: 'specialist', edgeType: EdgeType.
|
|
144
|
-
// Default single→single:
|
|
142
|
+
// Explicit transfer: dynamic routing
|
|
143
|
+
{ from: 'supervisor', to: 'specialist', edgeType: EdgeType.TRANSFER },
|
|
144
|
+
// Default single→single: transfer
|
|
145
145
|
{ from: 'triage', to: 'handler' },
|
|
146
|
-
// Condition-based: always
|
|
146
|
+
// Condition-based: always transfer
|
|
147
147
|
{ from: 'router', to: 'a', condition: () => true },
|
|
148
148
|
];
|
|
149
149
|
|
|
150
|
-
const {
|
|
151
|
-
expect(
|
|
152
|
-
expect(
|
|
150
|
+
const { sequenceEdges, transferEdges } = categorizeEdges(edges);
|
|
151
|
+
expect(sequenceEdges).toHaveLength(2); // explicit sequence + fan-out
|
|
152
|
+
expect(transferEdges).toHaveLength(3); // explicit transfer + default single→single + condition
|
|
153
153
|
});
|
|
154
154
|
|
|
155
155
|
it('handles empty edges array', () => {
|
|
156
|
-
const {
|
|
157
|
-
expect(
|
|
156
|
+
const { sequenceEdges, transferEdges, handoffEdges } = categorizeEdges([]);
|
|
157
|
+
expect(sequenceEdges).toHaveLength(0);
|
|
158
|
+
expect(transferEdges).toHaveLength(0);
|
|
158
159
|
expect(handoffEdges).toHaveLength(0);
|
|
159
|
-
expect(delegateEdges).toHaveLength(0);
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
-
it('classifies explicit EdgeType.
|
|
162
|
+
it('classifies explicit EdgeType.HANDOFF as handoff', () => {
|
|
163
163
|
const edges: GraphEdge[] = [
|
|
164
|
-
{ from: 'supervisor', to: 'researcher', edgeType: EdgeType.
|
|
164
|
+
{ from: 'supervisor', to: 'researcher', edgeType: EdgeType.HANDOFF },
|
|
165
165
|
];
|
|
166
|
-
const {
|
|
167
|
-
expect(
|
|
168
|
-
expect(
|
|
169
|
-
expect(
|
|
166
|
+
const { sequenceEdges, transferEdges, handoffEdges } = categorizeEdges(edges);
|
|
167
|
+
expect(handoffEdges).toHaveLength(1);
|
|
168
|
+
expect(transferEdges).toHaveLength(0);
|
|
169
|
+
expect(sequenceEdges).toHaveLength(0);
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
-
it('supports mixed
|
|
172
|
+
it('supports mixed transfer, sequence, and handoff edges from same source', () => {
|
|
173
173
|
const edges: GraphEdge[] = [
|
|
174
|
-
{ from: 'supervisor', to: 'researcher', edgeType: EdgeType.
|
|
175
|
-
{ from: 'supervisor', to: 'writer', edgeType: EdgeType.
|
|
176
|
-
{ from: 'supervisor', to: 'formatter', edgeType: EdgeType.
|
|
174
|
+
{ from: 'supervisor', to: 'researcher', edgeType: EdgeType.HANDOFF },
|
|
175
|
+
{ from: 'supervisor', to: 'writer', edgeType: EdgeType.TRANSFER },
|
|
176
|
+
{ from: 'supervisor', to: 'formatter', edgeType: EdgeType.SEQUENCE },
|
|
177
177
|
];
|
|
178
|
-
const {
|
|
179
|
-
expect(delegateEdges).toHaveLength(1);
|
|
178
|
+
const { sequenceEdges, transferEdges, handoffEdges } = categorizeEdges(edges);
|
|
180
179
|
expect(handoffEdges).toHaveLength(1);
|
|
181
|
-
expect(
|
|
180
|
+
expect(transferEdges).toHaveLength(1);
|
|
181
|
+
expect(sequenceEdges).toHaveLength(1);
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
it('categorization works with
|
|
184
|
+
it('categorization works with handoff string value cast as EdgeType', () => {
|
|
185
185
|
const edges: GraphEdge[] = [
|
|
186
|
-
{ from: 'a', to: 'b', edgeType: '
|
|
186
|
+
{ from: 'a', to: 'b', edgeType: 'handoff' as EdgeType },
|
|
187
187
|
];
|
|
188
|
-
const {
|
|
189
|
-
expect(
|
|
188
|
+
const { handoffEdges } = categorizeEdges(edges);
|
|
189
|
+
expect(handoffEdges).toHaveLength(1);
|
|
190
190
|
});
|
|
191
191
|
});
|
|
192
192
|
|
|
@@ -250,27 +250,25 @@ describe('starting node identification', () => {
|
|
|
250
250
|
// ---------------------------------------------------------------------------
|
|
251
251
|
describe('EdgeType values match GraphEdge string literals', () => {
|
|
252
252
|
it('HANDOFF matches the string used in MongoDB documents', () => {
|
|
253
|
-
// ranger stores edges in MongoDB with string values
|
|
254
|
-
// This ensures the enum stays compatible
|
|
255
253
|
expect(EdgeType.HANDOFF).toBe('handoff');
|
|
256
254
|
});
|
|
257
255
|
|
|
258
|
-
it('
|
|
259
|
-
expect(EdgeType.
|
|
256
|
+
it('TRANSFER matches the string used in MongoDB documents', () => {
|
|
257
|
+
expect(EdgeType.TRANSFER).toBe('transfer');
|
|
260
258
|
});
|
|
261
259
|
|
|
262
|
-
it('
|
|
263
|
-
expect(EdgeType.
|
|
260
|
+
it('SEQUENCE matches the string used in MongoDB documents', () => {
|
|
261
|
+
expect(EdgeType.SEQUENCE).toBe('sequence');
|
|
264
262
|
});
|
|
265
263
|
|
|
266
264
|
it('categorization works with string values cast as EdgeType', () => {
|
|
267
265
|
// Simulates data coming from MongoDB where edgeType is stored as string
|
|
268
266
|
const edges: GraphEdge[] = [
|
|
269
|
-
{ from: 'a', to: 'b', edgeType: '
|
|
270
|
-
{ from: 'c', to: 'd', edgeType: '
|
|
267
|
+
{ from: 'a', to: 'b', edgeType: 'transfer' as EdgeType },
|
|
268
|
+
{ from: 'c', to: 'd', edgeType: 'sequence' as EdgeType },
|
|
271
269
|
];
|
|
272
|
-
const {
|
|
273
|
-
expect(
|
|
274
|
-
expect(
|
|
270
|
+
const { sequenceEdges, transferEdges } = categorizeEdges(edges);
|
|
271
|
+
expect(transferEdges).toHaveLength(1);
|
|
272
|
+
expect(sequenceEdges).toHaveLength(1);
|
|
275
273
|
});
|
|
276
274
|
});
|
|
@@ -19,7 +19,7 @@ function createSequentialChainEdges(agentIds) {
|
|
|
19
19
|
edges.push({
|
|
20
20
|
from: fromAgent,
|
|
21
21
|
to: toAgent,
|
|
22
|
-
edgeType: '
|
|
22
|
+
edgeType: 'sequence',
|
|
23
23
|
// Use a prompt function to create the buffer string from all previous results
|
|
24
24
|
prompt: (messages, startIndex) => {
|
|
25
25
|
// Get only the messages from this run (after startIndex)
|
|
@@ -28,7 +28,7 @@ function createSequentialChainEdges(agentIds: string[]): t.GraphEdge[] {
|
|
|
28
28
|
edges.push({
|
|
29
29
|
from: fromAgent,
|
|
30
30
|
to: toAgent,
|
|
31
|
-
edgeType: '
|
|
31
|
+
edgeType: 'sequence',
|
|
32
32
|
// Use a prompt function to create the buffer string from all previous results
|
|
33
33
|
prompt: (messages: BaseMessage[], startIndex: number) => {
|
|
34
34
|
// Get only the messages from this run (after startIndex)
|
|
@@ -13,7 +13,7 @@ function createDocumentReviewChain(agentIds) {
|
|
|
13
13
|
edges.push({
|
|
14
14
|
from: agentIds[i],
|
|
15
15
|
to: agentIds[i + 1],
|
|
16
|
-
edgeType: '
|
|
16
|
+
edgeType: 'sequence',
|
|
17
17
|
prompt: (messages, startIndex) => {
|
|
18
18
|
const runMessages = messages.slice(startIndex);
|
|
19
19
|
const bufferString = getBufferString(runMessages);
|
|
@@ -22,7 +22,7 @@ function createDocumentReviewChain(agentIds: string[]): t.GraphEdge[] {
|
|
|
22
22
|
edges.push({
|
|
23
23
|
from: agentIds[i],
|
|
24
24
|
to: agentIds[i + 1],
|
|
25
|
-
edgeType: '
|
|
25
|
+
edgeType: 'sequence',
|
|
26
26
|
prompt: (messages: BaseMessage[], startIndex: number) => {
|
|
27
27
|
const runMessages = messages.slice(startIndex);
|
|
28
28
|
const bufferString = getBufferString(runMessages);
|
|
@@ -102,7 +102,7 @@ async function testHybridMultiAgent() {
|
|
|
102
102
|
{
|
|
103
103
|
from: 'primary_agent',
|
|
104
104
|
to: 'standalone_agent',
|
|
105
|
-
edgeType: '
|
|
105
|
+
edgeType: 'transfer',
|
|
106
106
|
description: 'Transfer to standalone specialist for complex requests',
|
|
107
107
|
prompt: 'Specific instructions for the specialist',
|
|
108
108
|
},
|
|
@@ -110,14 +110,14 @@ async function testHybridMultiAgent() {
|
|
|
110
110
|
{
|
|
111
111
|
from: 'primary_agent',
|
|
112
112
|
to: 'agent_b',
|
|
113
|
-
edgeType: '
|
|
113
|
+
edgeType: 'sequence',
|
|
114
114
|
description: 'Continue to Agent B only if no handoff occurs',
|
|
115
115
|
},
|
|
116
116
|
// Direct edge: agent_b automatically continues to agent_c
|
|
117
117
|
{
|
|
118
118
|
from: 'agent_b',
|
|
119
119
|
to: 'agent_c',
|
|
120
|
-
edgeType: '
|
|
120
|
+
edgeType: 'sequence',
|
|
121
121
|
description: 'Automatic progression from B to C',
|
|
122
122
|
},
|
|
123
123
|
];
|
|
@@ -108,7 +108,7 @@ async function testHybridMultiAgent() {
|
|
|
108
108
|
{
|
|
109
109
|
from: 'primary_agent',
|
|
110
110
|
to: 'standalone_agent',
|
|
111
|
-
edgeType: '
|
|
111
|
+
edgeType: 'transfer',
|
|
112
112
|
description: 'Transfer to standalone specialist for complex requests',
|
|
113
113
|
prompt: 'Specific instructions for the specialist',
|
|
114
114
|
},
|
|
@@ -116,14 +116,14 @@ async function testHybridMultiAgent() {
|
|
|
116
116
|
{
|
|
117
117
|
from: 'primary_agent',
|
|
118
118
|
to: 'agent_b',
|
|
119
|
-
edgeType: '
|
|
119
|
+
edgeType: 'sequence',
|
|
120
120
|
description: 'Continue to Agent B only if no handoff occurs',
|
|
121
121
|
},
|
|
122
122
|
// Direct edge: agent_b automatically continues to agent_c
|
|
123
123
|
{
|
|
124
124
|
from: 'agent_b',
|
|
125
125
|
to: 'agent_c',
|
|
126
|
-
edgeType: '
|
|
126
|
+
edgeType: 'sequence',
|
|
127
127
|
description: 'Automatic progression from B to C',
|
|
128
128
|
},
|
|
129
129
|
];
|
|
@@ -152,13 +152,13 @@ async function testParallelMultiAgent() {
|
|
|
152
152
|
from: 'researcher',
|
|
153
153
|
to: ['analyst1', 'analyst2', 'analyst3'], // Fan-out to multiple analysts
|
|
154
154
|
description: 'Distribute research to specialist analysts',
|
|
155
|
-
edgeType: '
|
|
155
|
+
edgeType: 'sequence', // Explicitly set as direct for automatic transition (enables parallel execution)
|
|
156
156
|
},
|
|
157
157
|
{
|
|
158
158
|
from: ['analyst1', 'analyst2', 'analyst3'], // Fan-in from multiple sources
|
|
159
159
|
to: 'summarizer',
|
|
160
160
|
description: 'Aggregate analysis results',
|
|
161
|
-
edgeType: '
|
|
161
|
+
edgeType: 'sequence', // Fan-in is also direct
|
|
162
162
|
// Add prompt when all analysts have provided input
|
|
163
163
|
// prompt: (messages, runStartIndex) => {
|
|
164
164
|
// // Check if we have analysis content from all three analysts
|
|
@@ -160,13 +160,13 @@ async function testParallelMultiAgent() {
|
|
|
160
160
|
from: 'researcher',
|
|
161
161
|
to: ['analyst1', 'analyst2', 'analyst3'], // Fan-out to multiple analysts
|
|
162
162
|
description: 'Distribute research to specialist analysts',
|
|
163
|
-
edgeType: '
|
|
163
|
+
edgeType: 'sequence', // Explicitly set as direct for automatic transition (enables parallel execution)
|
|
164
164
|
},
|
|
165
165
|
{
|
|
166
166
|
from: ['analyst1', 'analyst2', 'analyst3'], // Fan-in from multiple sources
|
|
167
167
|
to: 'summarizer',
|
|
168
168
|
description: 'Aggregate analysis results',
|
|
169
|
-
edgeType: '
|
|
169
|
+
edgeType: 'sequence', // Fan-in is also direct
|
|
170
170
|
// Add prompt when all analysts have provided input
|
|
171
171
|
// prompt: (messages, runStartIndex) => {
|
|
172
172
|
// // Check if we have analysis content from all three analysts
|
|
@@ -75,13 +75,13 @@ async function testSequentialMultiAgent() {
|
|
|
75
75
|
{
|
|
76
76
|
from: 'agent_a',
|
|
77
77
|
to: 'agent_b',
|
|
78
|
-
edgeType: '
|
|
78
|
+
edgeType: 'sequence', // This creates direct edges without tools
|
|
79
79
|
description: 'Automatic transition from A to B',
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
from: 'agent_b',
|
|
83
83
|
to: 'agent_c',
|
|
84
|
-
edgeType: '
|
|
84
|
+
edgeType: 'sequence', // This creates direct edges without tools
|
|
85
85
|
description: 'Automatic transition from B to C',
|
|
86
86
|
},
|
|
87
87
|
];
|