@illuma-ai/agents 1.1.14 → 1.1.16

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.
Files changed (66) hide show
  1. package/dist/cjs/common/enum.cjs +14 -3
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/graphs/MultiAgentGraph.cjs +304 -106
  4. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  5. package/dist/cjs/main.cjs +2 -0
  6. package/dist/cjs/main.cjs.map +1 -1
  7. package/dist/cjs/types/graph.cjs.map +1 -1
  8. package/dist/esm/common/enum.mjs +12 -4
  9. package/dist/esm/common/enum.mjs.map +1 -1
  10. package/dist/esm/graphs/MultiAgentGraph.mjs +306 -108
  11. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  12. package/dist/esm/main.mjs +1 -1
  13. package/dist/esm/types/graph.mjs.map +1 -1
  14. package/dist/types/common/enum.d.ts +11 -3
  15. package/dist/types/graphs/MultiAgentGraph.d.ts +72 -18
  16. package/dist/types/types/graph.d.ts +17 -5
  17. package/package.json +1 -1
  18. package/src/common/__tests__/enum.test.ts +15 -7
  19. package/src/common/enum.ts +13 -3
  20. package/src/graphs/MultiAgentGraph.ts +385 -107
  21. package/src/graphs/__tests__/multi-agent-delegate.test.ts +208 -0
  22. package/src/graphs/__tests__/multi-agent-edges.test.ts +98 -61
  23. package/src/scripts/multi-agent-chain.js +1 -1
  24. package/src/scripts/multi-agent-chain.ts +1 -1
  25. package/src/scripts/multi-agent-document-review-chain.js +1 -1
  26. package/src/scripts/multi-agent-document-review-chain.ts +1 -1
  27. package/src/scripts/multi-agent-hybrid-flow.js +3 -3
  28. package/src/scripts/multi-agent-hybrid-flow.ts +3 -3
  29. package/src/scripts/multi-agent-parallel.js +2 -2
  30. package/src/scripts/multi-agent-parallel.ts +2 -2
  31. package/src/scripts/multi-agent-sequence.js +2 -2
  32. package/src/scripts/multi-agent-sequence.ts +2 -2
  33. package/src/scripts/multi-agent-supervisor.js +5 -5
  34. package/src/scripts/multi-agent-supervisor.ts +5 -5
  35. package/src/scripts/poc-multi-agent-comprehensive.ts +7 -7
  36. package/src/scripts/sequential-full-metadata-test.js +1 -1
  37. package/src/scripts/sequential-full-metadata-test.ts +1 -1
  38. package/src/scripts/test-custom-prompt-key.js +3 -3
  39. package/src/scripts/test-custom-prompt-key.ts +3 -3
  40. package/src/scripts/test-handoff-input.js +1 -1
  41. package/src/scripts/test-handoff-input.ts +1 -1
  42. package/src/scripts/test-handoff-preamble.js +1 -1
  43. package/src/scripts/test-handoff-preamble.ts +1 -1
  44. package/src/scripts/test-handoff-steering.js +3 -3
  45. package/src/scripts/test-handoff-steering.ts +3 -3
  46. package/src/scripts/test-multi-agent-list-handoff.js +1 -1
  47. package/src/scripts/test-multi-agent-list-handoff.ts +1 -1
  48. package/src/scripts/test-parallel-agent-labeling.js +2 -2
  49. package/src/scripts/test-parallel-agent-labeling.ts +2 -2
  50. package/src/scripts/test-parallel-handoffs.js +2 -2
  51. package/src/scripts/test-parallel-handoffs.ts +2 -2
  52. package/src/scripts/test-thinking-handoff-bedrock.js +1 -1
  53. package/src/scripts/test-thinking-handoff-bedrock.ts +1 -1
  54. package/src/scripts/test-thinking-handoff.js +1 -1
  55. package/src/scripts/test-thinking-handoff.ts +1 -1
  56. package/src/scripts/test-thinking-to-thinking-handoff-bedrock.js +1 -1
  57. package/src/scripts/test-thinking-to-thinking-handoff-bedrock.ts +1 -1
  58. package/src/scripts/test-tool-before-handoff-role-order.js +1 -1
  59. package/src/scripts/test-tool-before-handoff-role-order.ts +1 -1
  60. package/src/scripts/test-tools-before-handoff.js +1 -1
  61. package/src/scripts/test-tools-before-handoff.ts +1 -1
  62. package/src/specs/agent-handoffs-bedrock.integration.test.ts +6 -6
  63. package/src/specs/agent-handoffs.test.ts +35 -35
  64. package/src/specs/thinking-handoff.test.ts +9 -9
  65. package/src/tools/search/search.test.ts +173 -0
  66. package/src/types/graph.ts +17 -5
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Unit tests for the handoff pattern (supervisor-delegate) in MultiAgentGraph.
3
+ *
4
+ * Tests cover:
5
+ * - Result extraction from child agent messages (extractHandoffResult)
6
+ * - Result truncation for parent context protection (truncateHandoffResult)
7
+ * - Constants and naming conventions
8
+ */
9
+ import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
10
+ import type { BaseMessage } from '@langchain/core/messages';
11
+ import {
12
+ Constants,
13
+ EdgeType,
14
+ DEFAULT_HANDOFF_MAX_RESULT_CHARS,
15
+ HANDOFF_TIMEOUT_MS,
16
+ } from '@/common';
17
+
18
+ /**
19
+ * Import the static helper methods from MultiAgentGraph.
20
+ * These are static so they can be tested without instantiating the full graph.
21
+ */
22
+ import { MultiAgentGraph } from '../MultiAgentGraph';
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Constants
26
+ // ---------------------------------------------------------------------------
27
+ describe('handoff constants', () => {
28
+ it('LC_HANDOFF_TO_ prefix matches expected pattern', () => {
29
+ expect(Constants.LC_HANDOFF_TO_).toBe('lc_handoff_to_');
30
+ });
31
+
32
+ it('LC_HANDOFF_TO_ is distinct from LC_TRANSFER_TO_', () => {
33
+ expect(Constants.LC_HANDOFF_TO_).not.toBe(Constants.LC_TRANSFER_TO_);
34
+ });
35
+
36
+ it('DEFAULT_HANDOFF_MAX_RESULT_CHARS is 32768', () => {
37
+ expect(DEFAULT_HANDOFF_MAX_RESULT_CHARS).toBe(32768);
38
+ });
39
+
40
+ it('HANDOFF_TIMEOUT_MS is 5 minutes', () => {
41
+ expect(HANDOFF_TIMEOUT_MS).toBe(300_000);
42
+ });
43
+
44
+ it('EdgeType.HANDOFF has correct value', () => {
45
+ expect(EdgeType.HANDOFF).toBe('handoff');
46
+ });
47
+ });
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // extractHandoffResult
51
+ // ---------------------------------------------------------------------------
52
+ describe('extractHandoffResult', () => {
53
+ it('extracts text from last AIMessage with string content', () => {
54
+ const messages: BaseMessage[] = [
55
+ new HumanMessage('find sales data'),
56
+ new AIMessage('Here are the sales figures for Q1...'),
57
+ ];
58
+ const result = MultiAgentGraph.extractHandoffResult(messages, 'researcher');
59
+ expect(result).toBe('Here are the sales figures for Q1...');
60
+ });
61
+
62
+ it('extracts text from last AIMessage with array content', () => {
63
+ const messages: BaseMessage[] = [
64
+ new HumanMessage('analyze this'),
65
+ new AIMessage({
66
+ content: [
67
+ { type: 'text', text: 'Analysis shows growth.' },
68
+ { type: 'text', text: 'Revenue up 15%.' },
69
+ ],
70
+ }),
71
+ ];
72
+ const result = MultiAgentGraph.extractHandoffResult(messages, 'analyst');
73
+ expect(result).toBe('Analysis shows growth.\nRevenue up 15%.');
74
+ });
75
+
76
+ it('skips non-AI messages and finds last AIMessage', () => {
77
+ const messages: BaseMessage[] = [
78
+ new HumanMessage('task'),
79
+ new AIMessage('intermediate result'),
80
+ new HumanMessage('continue'),
81
+ new AIMessage('final result here'),
82
+ ];
83
+ const result = MultiAgentGraph.extractHandoffResult(messages, 'agent');
84
+ expect(result).toBe('final result here');
85
+ });
86
+
87
+ it('skips AIMessages with empty content and finds previous', () => {
88
+ const messages: BaseMessage[] = [
89
+ new HumanMessage('task'),
90
+ new AIMessage('good result'),
91
+ new AIMessage(''),
92
+ ];
93
+ const result = MultiAgentGraph.extractHandoffResult(messages, 'agent');
94
+ expect(result).toBe('good result');
95
+ });
96
+
97
+ it('skips AIMessages with tool_calls only (no text)', () => {
98
+ const messages: BaseMessage[] = [
99
+ new HumanMessage('task'),
100
+ new AIMessage('found the data'),
101
+ new AIMessage({
102
+ content: '',
103
+ tool_calls: [{ name: 'search', args: {}, id: 'tc1', type: 'tool_call' }],
104
+ }),
105
+ new ToolMessage({ content: 'search result', tool_call_id: 'tc1' }),
106
+ ];
107
+ const result = MultiAgentGraph.extractHandoffResult(messages, 'agent');
108
+ expect(result).toBe('found the data');
109
+ });
110
+
111
+ it('returns fallback message when no AIMessage has text', () => {
112
+ const messages: BaseMessage[] = [
113
+ new HumanMessage('task'),
114
+ ];
115
+ const result = MultiAgentGraph.extractHandoffResult(messages, 'researcher');
116
+ expect(result).toBe('[Agent "researcher" completed but produced no text output]');
117
+ });
118
+
119
+ it('returns fallback for empty messages array', () => {
120
+ const result = MultiAgentGraph.extractHandoffResult([], 'agent');
121
+ expect(result).toBe('[Agent "agent" completed but produced no text output]');
122
+ });
123
+
124
+ it('trims whitespace from extracted text', () => {
125
+ const messages: BaseMessage[] = [
126
+ new AIMessage(' result with spaces \n\n'),
127
+ ];
128
+ const result = MultiAgentGraph.extractHandoffResult(messages, 'agent');
129
+ expect(result).toBe('result with spaces');
130
+ });
131
+ });
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // truncateHandoffResult
135
+ // ---------------------------------------------------------------------------
136
+ describe('truncateHandoffResult', () => {
137
+ it('returns result unchanged when within budget', () => {
138
+ const result = 'short result';
139
+ expect(MultiAgentGraph.truncateHandoffResult(result, 1000)).toBe(result);
140
+ });
141
+
142
+ it('returns empty string unchanged', () => {
143
+ expect(MultiAgentGraph.truncateHandoffResult('', 1000)).toBe('');
144
+ });
145
+
146
+ it('truncates with head/tail split when over budget', () => {
147
+ const longText = 'A'.repeat(1000);
148
+ const truncated = MultiAgentGraph.truncateHandoffResult(longText, 500);
149
+
150
+ expect(truncated.length).toBeLessThanOrEqual(500);
151
+ expect(truncated).toContain('handoff output truncated');
152
+ // Head should be present
153
+ expect(truncated.startsWith('AAAA')).toBe(true);
154
+ // Tail should be present
155
+ expect(truncated.endsWith('AAAA')).toBe(true);
156
+ });
157
+
158
+ it('preserves 60/40 head/tail ratio', () => {
159
+ const longText = 'H'.repeat(500) + 'T'.repeat(500);
160
+ const maxChars = 200;
161
+ const truncated = MultiAgentGraph.truncateHandoffResult(longText, maxChars);
162
+
163
+ const notice = '\n\n[... handoff output truncated — middle section omitted to fit parent context ...]\n\n';
164
+ const available = maxChars - notice.length;
165
+ const expectedHead = Math.floor(available * 0.6);
166
+ const expectedTail = available - expectedHead;
167
+
168
+ // Head portion should be all H's
169
+ const headPortion = truncated.substring(0, expectedHead);
170
+ expect(headPortion).toMatch(/^H+$/);
171
+
172
+ // Tail portion should be all T's
173
+ const tailPortion = truncated.substring(truncated.length - expectedTail);
174
+ expect(tailPortion).toMatch(/^T+$/);
175
+ });
176
+
177
+ it('handles result exactly at maxChars boundary', () => {
178
+ const result = 'X'.repeat(100);
179
+ expect(MultiAgentGraph.truncateHandoffResult(result, 100)).toBe(result);
180
+ });
181
+
182
+ it('handles very small maxChars gracefully', () => {
183
+ const result = 'A'.repeat(200);
184
+ // When maxChars is smaller than the truncation notice itself
185
+ const truncated = MultiAgentGraph.truncateHandoffResult(result, 10);
186
+ expect(truncated.length).toBeLessThanOrEqual(200);
187
+ });
188
+ });
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Tool naming convention
192
+ // ---------------------------------------------------------------------------
193
+ describe('handoff tool naming', () => {
194
+ it('handoff tool name follows lc_handoff_to_ pattern', () => {
195
+ const agentId = 'researcher_agent_123';
196
+ const expectedToolName = `${Constants.LC_HANDOFF_TO_}${agentId}`;
197
+ expect(expectedToolName).toBe('lc_handoff_to_researcher_agent_123');
198
+ });
199
+
200
+ it('handoff tool prefix is distinct from transfer tool prefix', () => {
201
+ const agentId = 'test';
202
+ const handoffName = `${Constants.LC_HANDOFF_TO_}${agentId}`;
203
+ const transferName = `${Constants.LC_TRANSFER_TO_}${agentId}`;
204
+ expect(handoffName).not.toBe(transferName);
205
+ expect(handoffName).toBe('lc_handoff_to_test');
206
+ expect(transferName).toBe('lc_transfer_to_test');
207
+ });
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.HANDOFFhandoff
7
- * - Explicit EdgeType.DIRECTdirect
8
- * - Condition-based edges → handoff (always, regardless of edgeType)
9
- * - Default single→single → handoff
10
- * - Default single→many → direct (fan-out pattern)
6
+ * - Explicit EdgeType.TRANSFERtransfer
7
+ * - Explicit EdgeType.SEQUENCEsequence
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,30 +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
- directEdges: GraphEdge[];
20
+ sequenceEdges: GraphEdge[];
21
+ transferEdges: GraphEdge[];
21
22
  handoffEdges: GraphEdge[];
22
23
  } {
23
- const directEdges: GraphEdge[] = [];
24
+ const sequenceEdges: GraphEdge[] = [];
25
+ const transferEdges: GraphEdge[] = [];
24
26
  const handoffEdges: GraphEdge[] = [];
25
27
 
26
28
  for (const edge of edges) {
27
- if (edge.edgeType === EdgeType.DIRECT) {
28
- directEdges.push(edge);
29
- } else if (edge.edgeType === EdgeType.HANDOFF || edge.condition != null) {
29
+ if (edge.edgeType === EdgeType.HANDOFF) {
30
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);
31
35
  } else {
32
36
  const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
33
37
  const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
34
38
 
35
39
  if (sources.length === 1 && destinations.length > 1) {
36
- directEdges.push(edge);
40
+ sequenceEdges.push(edge);
37
41
  } else {
38
- handoffEdges.push(edge);
42
+ transferEdges.push(edge);
39
43
  }
40
44
  }
41
45
  }
42
46
 
43
- return { directEdges, handoffEdges };
47
+ return { sequenceEdges, transferEdges, handoffEdges };
44
48
  }
45
49
 
46
50
  /**
@@ -75,84 +79,115 @@ function findStartingNodes(
75
79
  // Edge categorization
76
80
  // ---------------------------------------------------------------------------
77
81
  describe('edge categorization', () => {
78
- it('classifies explicit EdgeType.HANDOFF as handoff', () => {
82
+ it('classifies explicit EdgeType.TRANSFER as transfer', () => {
79
83
  const edges: GraphEdge[] = [
80
- { from: 'a', to: 'b', edgeType: EdgeType.HANDOFF },
84
+ { from: 'a', to: 'b', edgeType: EdgeType.TRANSFER },
81
85
  ];
82
- const { directEdges, handoffEdges } = categorizeEdges(edges);
83
- expect(handoffEdges).toHaveLength(1);
84
- expect(directEdges).toHaveLength(0);
86
+ const { sequenceEdges, transferEdges } = categorizeEdges(edges);
87
+ expect(transferEdges).toHaveLength(1);
88
+ expect(sequenceEdges).toHaveLength(0);
85
89
  });
86
90
 
87
- it('classifies explicit EdgeType.DIRECT as direct', () => {
91
+ it('classifies explicit EdgeType.SEQUENCE as sequence', () => {
88
92
  const edges: GraphEdge[] = [
89
- { from: 'a', to: 'b', edgeType: EdgeType.DIRECT },
93
+ { from: 'a', to: 'b', edgeType: EdgeType.SEQUENCE },
90
94
  ];
91
- const { directEdges, handoffEdges } = categorizeEdges(edges);
92
- expect(directEdges).toHaveLength(1);
93
- expect(handoffEdges).toHaveLength(0);
95
+ const { sequenceEdges, transferEdges } = categorizeEdges(edges);
96
+ expect(sequenceEdges).toHaveLength(1);
97
+ expect(transferEdges).toHaveLength(0);
94
98
  });
95
99
 
96
- it('classifies condition-based edges as handoff regardless of edgeType', () => {
100
+ it('classifies condition-based edges as transfer regardless of edgeType', () => {
97
101
  const condition = (_state: BaseGraphState) => true;
98
102
  const edges: GraphEdge[] = [
99
103
  { from: 'a', to: 'b', condition },
100
- { from: 'c', to: 'd', condition, edgeType: EdgeType.HANDOFF },
104
+ { from: 'c', to: 'd', condition, edgeType: EdgeType.TRANSFER },
101
105
  ];
102
- const { directEdges, handoffEdges } = categorizeEdges(edges);
103
- expect(handoffEdges).toHaveLength(2);
104
- expect(directEdges).toHaveLength(0);
106
+ const { sequenceEdges, transferEdges } = categorizeEdges(edges);
107
+ expect(transferEdges).toHaveLength(2);
108
+ expect(sequenceEdges).toHaveLength(0);
105
109
  });
106
110
 
107
- it('defaults single→single edges to handoff', () => {
111
+ it('defaults single→single edges to transfer', () => {
108
112
  const edges: GraphEdge[] = [{ from: 'supervisor', to: 'worker' }];
109
- const { directEdges, handoffEdges } = categorizeEdges(edges);
110
- expect(handoffEdges).toHaveLength(1);
111
- expect(directEdges).toHaveLength(0);
113
+ const { sequenceEdges, transferEdges } = categorizeEdges(edges);
114
+ expect(transferEdges).toHaveLength(1);
115
+ expect(sequenceEdges).toHaveLength(0);
112
116
  });
113
117
 
114
- it('defaults single→many edges to direct (fan-out pattern)', () => {
118
+ it('defaults single→many edges to sequence (fan-out pattern)', () => {
115
119
  const edges: GraphEdge[] = [
116
120
  { from: 'coordinator', to: ['analyst1', 'analyst2', 'analyst3'] },
117
121
  ];
118
- const { directEdges, handoffEdges } = categorizeEdges(edges);
119
- expect(directEdges).toHaveLength(1);
120
- expect(handoffEdges).toHaveLength(0);
122
+ const { sequenceEdges, transferEdges } = categorizeEdges(edges);
123
+ expect(sequenceEdges).toHaveLength(1);
124
+ expect(transferEdges).toHaveLength(0);
121
125
  });
122
126
 
123
- it('defaults many→single edges to handoff', () => {
127
+ it('defaults many→single edges to transfer', () => {
124
128
  const edges: GraphEdge[] = [
125
129
  { from: ['analyst1', 'analyst2'], to: 'summarizer' },
126
130
  ];
127
- const { directEdges, handoffEdges } = categorizeEdges(edges);
128
- expect(handoffEdges).toHaveLength(1);
129
- expect(directEdges).toHaveLength(0);
131
+ const { sequenceEdges, transferEdges } = categorizeEdges(edges);
132
+ expect(transferEdges).toHaveLength(1);
133
+ expect(sequenceEdges).toHaveLength(0);
130
134
  });
131
135
 
132
136
  it('correctly categorizes a mixed set of edges', () => {
133
137
  const edges: GraphEdge[] = [
134
- // Explicit direct: sequential chain
135
- { from: 'researcher', to: 'analyst', edgeType: EdgeType.DIRECT },
136
- // Fan-out: defaults to direct
138
+ // Explicit sequence: sequential chain
139
+ { from: 'researcher', to: 'analyst', edgeType: EdgeType.SEQUENCE },
140
+ // Fan-out: defaults to sequence
137
141
  { from: 'analyst', to: ['reviewer1', 'reviewer2'] },
138
- // Explicit handoff: dynamic routing
139
- { from: 'supervisor', to: 'specialist', edgeType: EdgeType.HANDOFF },
140
- // Default single→single: handoff
142
+ // Explicit transfer: dynamic routing
143
+ { from: 'supervisor', to: 'specialist', edgeType: EdgeType.TRANSFER },
144
+ // Default single→single: transfer
141
145
  { from: 'triage', to: 'handler' },
142
- // Condition-based: always handoff
146
+ // Condition-based: always transfer
143
147
  { from: 'router', to: 'a', condition: () => true },
144
148
  ];
145
149
 
146
- const { directEdges, handoffEdges } = categorizeEdges(edges);
147
- expect(directEdges).toHaveLength(2); // explicit direct + fan-out
148
- expect(handoffEdges).toHaveLength(3); // explicit handoff + default single→single + condition
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
149
153
  });
150
154
 
151
155
  it('handles empty edges array', () => {
152
- const { directEdges, handoffEdges } = categorizeEdges([]);
153
- expect(directEdges).toHaveLength(0);
156
+ const { sequenceEdges, transferEdges, handoffEdges } = categorizeEdges([]);
157
+ expect(sequenceEdges).toHaveLength(0);
158
+ expect(transferEdges).toHaveLength(0);
154
159
  expect(handoffEdges).toHaveLength(0);
155
160
  });
161
+
162
+ it('classifies explicit EdgeType.HANDOFF as handoff', () => {
163
+ const edges: GraphEdge[] = [
164
+ { from: 'supervisor', to: 'researcher', edgeType: EdgeType.HANDOFF },
165
+ ];
166
+ const { sequenceEdges, transferEdges, handoffEdges } = categorizeEdges(edges);
167
+ expect(handoffEdges).toHaveLength(1);
168
+ expect(transferEdges).toHaveLength(0);
169
+ expect(sequenceEdges).toHaveLength(0);
170
+ });
171
+
172
+ it('supports mixed transfer, sequence, and handoff edges from same source', () => {
173
+ const edges: GraphEdge[] = [
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
+ ];
178
+ const { sequenceEdges, transferEdges, handoffEdges } = categorizeEdges(edges);
179
+ expect(handoffEdges).toHaveLength(1);
180
+ expect(transferEdges).toHaveLength(1);
181
+ expect(sequenceEdges).toHaveLength(1);
182
+ });
183
+
184
+ it('categorization works with handoff string value cast as EdgeType', () => {
185
+ const edges: GraphEdge[] = [
186
+ { from: 'a', to: 'b', edgeType: 'handoff' as EdgeType },
187
+ ];
188
+ const { handoffEdges } = categorizeEdges(edges);
189
+ expect(handoffEdges).toHaveLength(1);
190
+ });
156
191
  });
157
192
 
158
193
  // ---------------------------------------------------------------------------
@@ -215,23 +250,25 @@ describe('starting node identification', () => {
215
250
  // ---------------------------------------------------------------------------
216
251
  describe('EdgeType values match GraphEdge string literals', () => {
217
252
  it('HANDOFF matches the string used in MongoDB documents', () => {
218
- // ranger stores edges in MongoDB with string values
219
- // This ensures the enum stays compatible
220
253
  expect(EdgeType.HANDOFF).toBe('handoff');
221
254
  });
222
255
 
223
- it('DIRECT matches the string used in MongoDB documents', () => {
224
- expect(EdgeType.DIRECT).toBe('direct');
256
+ it('TRANSFER matches the string used in MongoDB documents', () => {
257
+ expect(EdgeType.TRANSFER).toBe('transfer');
258
+ });
259
+
260
+ it('SEQUENCE matches the string used in MongoDB documents', () => {
261
+ expect(EdgeType.SEQUENCE).toBe('sequence');
225
262
  });
226
263
 
227
264
  it('categorization works with string values cast as EdgeType', () => {
228
265
  // Simulates data coming from MongoDB where edgeType is stored as string
229
266
  const edges: GraphEdge[] = [
230
- { from: 'a', to: 'b', edgeType: 'handoff' as EdgeType },
231
- { from: 'c', to: 'd', edgeType: 'direct' as EdgeType },
267
+ { from: 'a', to: 'b', edgeType: 'transfer' as EdgeType },
268
+ { from: 'c', to: 'd', edgeType: 'sequence' as EdgeType },
232
269
  ];
233
- const { directEdges, handoffEdges } = categorizeEdges(edges);
234
- expect(handoffEdges).toHaveLength(1);
235
- expect(directEdges).toHaveLength(1);
270
+ const { sequenceEdges, transferEdges } = categorizeEdges(edges);
271
+ expect(transferEdges).toHaveLength(1);
272
+ expect(sequenceEdges).toHaveLength(1);
236
273
  });
237
274
  });
@@ -19,7 +19,7 @@ function createSequentialChainEdges(agentIds) {
19
19
  edges.push({
20
20
  from: fromAgent,
21
21
  to: toAgent,
22
- edgeType: 'direct',
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: 'direct',
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: 'direct',
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: 'direct',
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: 'handoff',
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: 'direct',
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: 'direct',
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: 'handoff',
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: 'direct',
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: 'direct',
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: 'direct', // Explicitly set as direct for automatic transition (enables parallel execution)
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: 'direct', // Fan-in is also direct
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: 'direct', // Explicitly set as direct for automatic transition (enables parallel execution)
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: 'direct', // Fan-in is also direct
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: 'direct', // This creates direct edges without tools
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: 'direct', // This creates direct edges without tools
84
+ edgeType: 'sequence', // This creates direct edges without tools
85
85
  description: 'Automatic transition from B to C',
86
86
  },
87
87
  ];
@@ -82,13 +82,13 @@ async function testSequentialMultiAgent() {
82
82
  {
83
83
  from: 'agent_a',
84
84
  to: 'agent_b',
85
- edgeType: 'direct', // This creates direct edges without tools
85
+ edgeType: 'sequence', // This creates direct edges without tools
86
86
  description: 'Automatic transition from A to B',
87
87
  },
88
88
  {
89
89
  from: 'agent_b',
90
90
  to: 'agent_c',
91
- edgeType: 'direct', // This creates direct edges without tools
91
+ edgeType: 'sequence', // This creates direct edges without tools
92
92
  description: 'Automatic transition from B to C',
93
93
  },
94
94
  ];
@@ -228,31 +228,31 @@ async function testSupervisorMultiAgent() {
228
228
  from: 'supervisor',
229
229
  to: 'data_analyst',
230
230
  description: 'Transfer to data analyst for statistical analysis and metrics',
231
- edgeType: 'handoff',
231
+ edgeType: 'transfer',
232
232
  },
233
233
  {
234
234
  from: 'supervisor',
235
235
  to: 'security_expert',
236
236
  description: 'Transfer to security expert for cybersecurity assessment',
237
- edgeType: 'handoff',
237
+ edgeType: 'transfer',
238
238
  },
239
239
  {
240
240
  from: 'supervisor',
241
241
  to: 'product_designer',
242
242
  description: 'Transfer to product designer for UX/UI design',
243
- edgeType: 'handoff',
243
+ edgeType: 'transfer',
244
244
  },
245
245
  {
246
246
  from: 'supervisor',
247
247
  to: 'devops_engineer',
248
248
  description: 'Transfer to DevOps engineer for infrastructure and deployment',
249
- edgeType: 'handoff',
249
+ edgeType: 'transfer',
250
250
  },
251
251
  {
252
252
  from: 'supervisor',
253
253
  to: 'legal_advisor',
254
254
  description: 'Transfer to legal advisor for compliance and licensing',
255
- edgeType: 'handoff',
255
+ edgeType: 'transfer',
256
256
  },
257
257
  ];
258
258
  return {