@illuma-ai/agents 1.1.25 → 1.1.28
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/graphs/Graph.cjs +5 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/HandoffRegistry.cjs +104 -0
- package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -0
- package/dist/cjs/graphs/MultiAgentGraph.cjs +224 -47
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/stream.cjs +4 -4
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/types/graph.cjs.map +1 -1
- package/dist/cjs/utils/events.cjs +3 -0
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +20 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +5 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/HandoffRegistry.mjs +102 -0
- package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -0
- package/dist/esm/graphs/MultiAgentGraph.mjs +224 -47
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/stream.mjs +4 -4
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/types/graph.mjs.map +1 -1
- package/dist/esm/utils/events.mjs +3 -0
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/types/graphs/HandoffRegistry.d.ts +80 -0
- package/dist/types/graphs/MultiAgentGraph.d.ts +23 -3
- package/dist/types/graphs/index.d.ts +1 -0
- package/dist/types/types/graph.d.ts +6 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +20 -5
- package/src/graphs/Graph.ts +8 -0
- package/src/graphs/HandoffRegistry.ts +168 -0
- package/src/graphs/MultiAgentGraph.ts +274 -67
- package/src/graphs/__tests__/HandoffRegistry.test.ts +407 -0
- package/src/graphs/index.ts +1 -0
- package/src/stream.ts +4 -6
- package/src/tools/approval/__tests__/constants.test.ts +3 -3
- package/src/types/graph.ts +6 -0
- package/src/utils/events.ts +3 -0
|
@@ -0,0 +1,407 @@
|
|
|
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 (messages[i].getType() === 'ai' && typeof messages[i].content === 'string') {
|
|
46
|
+
return messages[i].content;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return '[no result]';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Stub truncateResult — pass through */
|
|
53
|
+
function truncateResult(text: string, _maxChars: number): string {
|
|
54
|
+
return text;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('HandoffRegistry', () => {
|
|
58
|
+
let registry: HandoffRegistry;
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
registry = new HandoffRegistry();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('spawn', () => {
|
|
65
|
+
it('should register a new handoff record', () => {
|
|
66
|
+
const promise = createChildPromise('test result');
|
|
67
|
+
|
|
68
|
+
registry.spawn({
|
|
69
|
+
id: 'agent-1',
|
|
70
|
+
name: 'Researcher',
|
|
71
|
+
task: 'Find information',
|
|
72
|
+
promise,
|
|
73
|
+
extractResult,
|
|
74
|
+
truncateResult,
|
|
75
|
+
maxResultChars: 5000,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(registry.size).toBe(1);
|
|
79
|
+
expect(registry.hasPending()).toBe(true);
|
|
80
|
+
|
|
81
|
+
const record = registry.get('agent-1');
|
|
82
|
+
expect(record).toBeDefined();
|
|
83
|
+
expect(record!.name).toBe('Researcher');
|
|
84
|
+
expect(record!.task).toBe('Find information');
|
|
85
|
+
expect(record!.status).toBe('running');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should update record on completion', async () => {
|
|
89
|
+
const promise = createChildPromise('research findings');
|
|
90
|
+
|
|
91
|
+
registry.spawn({
|
|
92
|
+
id: 'agent-1',
|
|
93
|
+
name: 'Researcher',
|
|
94
|
+
task: 'Find info',
|
|
95
|
+
promise,
|
|
96
|
+
extractResult,
|
|
97
|
+
truncateResult,
|
|
98
|
+
maxResultChars: 5000,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Wait for promise to resolve
|
|
102
|
+
await promise;
|
|
103
|
+
// Yield to let .then handler run
|
|
104
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
105
|
+
|
|
106
|
+
const record = registry.get('agent-1')!;
|
|
107
|
+
expect(record.status).toBe('completed');
|
|
108
|
+
expect(record.resultText).toBe('research findings');
|
|
109
|
+
expect(record.durationMs).toBeGreaterThanOrEqual(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should update record on failure', async () => {
|
|
113
|
+
const promise = createFailingPromise('timeout');
|
|
114
|
+
|
|
115
|
+
registry.spawn({
|
|
116
|
+
id: 'agent-1',
|
|
117
|
+
name: 'Researcher',
|
|
118
|
+
task: 'Find info',
|
|
119
|
+
promise,
|
|
120
|
+
extractResult,
|
|
121
|
+
truncateResult,
|
|
122
|
+
maxResultChars: 5000,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await promise;
|
|
127
|
+
} catch {
|
|
128
|
+
// expected
|
|
129
|
+
}
|
|
130
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
131
|
+
|
|
132
|
+
const record = registry.get('agent-1')!;
|
|
133
|
+
expect(record.status).toBe('failed');
|
|
134
|
+
expect(record.error).toBe('timeout');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should call onComplete callback on completion', async () => {
|
|
138
|
+
const onComplete = jest.fn();
|
|
139
|
+
const promise = createChildPromise('done');
|
|
140
|
+
|
|
141
|
+
registry.spawn({
|
|
142
|
+
id: 'agent-1',
|
|
143
|
+
name: 'Worker',
|
|
144
|
+
task: 'Do work',
|
|
145
|
+
promise,
|
|
146
|
+
extractResult,
|
|
147
|
+
truncateResult,
|
|
148
|
+
maxResultChars: 5000,
|
|
149
|
+
onComplete,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await promise;
|
|
153
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
154
|
+
|
|
155
|
+
expect(onComplete).toHaveBeenCalledTimes(1);
|
|
156
|
+
expect(onComplete).toHaveBeenCalledWith(
|
|
157
|
+
expect.objectContaining({
|
|
158
|
+
id: 'agent-1',
|
|
159
|
+
status: 'completed',
|
|
160
|
+
resultText: 'done',
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should call onComplete callback on failure', async () => {
|
|
166
|
+
const onComplete = jest.fn();
|
|
167
|
+
const promise = createFailingPromise('oops');
|
|
168
|
+
|
|
169
|
+
registry.spawn({
|
|
170
|
+
id: 'agent-1',
|
|
171
|
+
name: 'Worker',
|
|
172
|
+
task: 'Do work',
|
|
173
|
+
promise,
|
|
174
|
+
extractResult,
|
|
175
|
+
truncateResult,
|
|
176
|
+
maxResultChars: 5000,
|
|
177
|
+
onComplete,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
await promise;
|
|
182
|
+
} catch {
|
|
183
|
+
// expected
|
|
184
|
+
}
|
|
185
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
186
|
+
|
|
187
|
+
expect(onComplete).toHaveBeenCalledTimes(1);
|
|
188
|
+
expect(onComplete).toHaveBeenCalledWith(
|
|
189
|
+
expect.objectContaining({
|
|
190
|
+
id: 'agent-1',
|
|
191
|
+
status: 'failed',
|
|
192
|
+
error: 'oops',
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('listing methods', () => {
|
|
199
|
+
it('should list pending and completed separately', async () => {
|
|
200
|
+
const p1 = createChildPromise('result 1');
|
|
201
|
+
const p2 = createChildPromise('result 2', 100);
|
|
202
|
+
|
|
203
|
+
registry.spawn({
|
|
204
|
+
id: 'agent-1',
|
|
205
|
+
name: 'Agent 1',
|
|
206
|
+
task: 'task 1',
|
|
207
|
+
promise: p1,
|
|
208
|
+
extractResult,
|
|
209
|
+
truncateResult,
|
|
210
|
+
maxResultChars: 5000,
|
|
211
|
+
});
|
|
212
|
+
registry.spawn({
|
|
213
|
+
id: 'agent-2',
|
|
214
|
+
name: 'Agent 2',
|
|
215
|
+
task: 'task 2',
|
|
216
|
+
promise: p2,
|
|
217
|
+
extractResult,
|
|
218
|
+
truncateResult,
|
|
219
|
+
maxResultChars: 5000,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Wait for first to complete
|
|
223
|
+
await p1;
|
|
224
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
225
|
+
|
|
226
|
+
expect(registry.listCompleted()).toHaveLength(1);
|
|
227
|
+
expect(registry.listCompleted()[0].id).toBe('agent-1');
|
|
228
|
+
expect(registry.listPending()).toHaveLength(1);
|
|
229
|
+
expect(registry.listPending()[0].id).toBe('agent-2');
|
|
230
|
+
expect(registry.listAll()).toHaveLength(2);
|
|
231
|
+
|
|
232
|
+
// Wait for second
|
|
233
|
+
await p2;
|
|
234
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
235
|
+
|
|
236
|
+
expect(registry.listCompleted()).toHaveLength(2);
|
|
237
|
+
expect(registry.listPending()).toHaveLength(0);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('waitForAll', () => {
|
|
242
|
+
it('should wait for all pending handoffs', async () => {
|
|
243
|
+
const p1 = createChildPromise('result 1', 10);
|
|
244
|
+
const p2 = createChildPromise('result 2', 20);
|
|
245
|
+
|
|
246
|
+
registry.spawn({
|
|
247
|
+
id: 'agent-1',
|
|
248
|
+
name: 'Agent 1',
|
|
249
|
+
task: 'task 1',
|
|
250
|
+
promise: p1,
|
|
251
|
+
extractResult,
|
|
252
|
+
truncateResult,
|
|
253
|
+
maxResultChars: 5000,
|
|
254
|
+
});
|
|
255
|
+
registry.spawn({
|
|
256
|
+
id: 'agent-2',
|
|
257
|
+
name: 'Agent 2',
|
|
258
|
+
task: 'task 2',
|
|
259
|
+
promise: p2,
|
|
260
|
+
extractResult,
|
|
261
|
+
truncateResult,
|
|
262
|
+
maxResultChars: 5000,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const results = await registry.waitForAll();
|
|
266
|
+
|
|
267
|
+
expect(results).toHaveLength(2);
|
|
268
|
+
expect(results.every((r) => r.status === 'completed')).toBe(true);
|
|
269
|
+
expect(results[0].resultText).toBe('result 1');
|
|
270
|
+
expect(results[1].resultText).toBe('result 2');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should return immediately when no pending handoffs', async () => {
|
|
274
|
+
const results = await registry.waitForAll();
|
|
275
|
+
expect(results).toHaveLength(0);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('waitForAny', () => {
|
|
280
|
+
it('should return when any handoff completes', async () => {
|
|
281
|
+
const p1 = createChildPromise('fast result', 10);
|
|
282
|
+
const p2 = createChildPromise('slow result', 200);
|
|
283
|
+
|
|
284
|
+
registry.spawn({
|
|
285
|
+
id: 'agent-1',
|
|
286
|
+
name: 'Fast',
|
|
287
|
+
task: 'fast task',
|
|
288
|
+
promise: p1,
|
|
289
|
+
extractResult,
|
|
290
|
+
truncateResult,
|
|
291
|
+
maxResultChars: 5000,
|
|
292
|
+
});
|
|
293
|
+
registry.spawn({
|
|
294
|
+
id: 'agent-2',
|
|
295
|
+
name: 'Slow',
|
|
296
|
+
task: 'slow task',
|
|
297
|
+
promise: p2,
|
|
298
|
+
extractResult,
|
|
299
|
+
truncateResult,
|
|
300
|
+
maxResultChars: 5000,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const completed = await registry.waitForAny();
|
|
304
|
+
|
|
305
|
+
// At least the fast one should be completed
|
|
306
|
+
expect(completed.length).toBeGreaterThanOrEqual(1);
|
|
307
|
+
expect(completed.some((r) => r.id === 'agent-1')).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('clear', () => {
|
|
312
|
+
it('should clear all records', () => {
|
|
313
|
+
registry.spawn({
|
|
314
|
+
id: 'agent-1',
|
|
315
|
+
name: 'Agent',
|
|
316
|
+
task: 'task',
|
|
317
|
+
promise: createChildPromise('result'),
|
|
318
|
+
extractResult,
|
|
319
|
+
truncateResult,
|
|
320
|
+
maxResultChars: 5000,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
expect(registry.size).toBe(1);
|
|
324
|
+
registry.clear();
|
|
325
|
+
expect(registry.size).toBe(0);
|
|
326
|
+
expect(registry.hasPending()).toBe(false);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('parallel spawn workflow', () => {
|
|
331
|
+
it('should handle the full orchestrator workflow: spawn → collect → synthesize', async () => {
|
|
332
|
+
// Step 1: Orchestrator spawns two agents in parallel
|
|
333
|
+
const p1 = createChildPromise('Researcher found: X=42', 10);
|
|
334
|
+
const p2 = createChildPromise('Analyst found: Y=100', 20);
|
|
335
|
+
|
|
336
|
+
registry.spawn({
|
|
337
|
+
id: 'researcher',
|
|
338
|
+
name: 'Researcher',
|
|
339
|
+
task: 'Find X',
|
|
340
|
+
promise: p1,
|
|
341
|
+
extractResult,
|
|
342
|
+
truncateResult,
|
|
343
|
+
maxResultChars: 5000,
|
|
344
|
+
});
|
|
345
|
+
registry.spawn({
|
|
346
|
+
id: 'analyst',
|
|
347
|
+
name: 'Analyst',
|
|
348
|
+
task: 'Find Y',
|
|
349
|
+
promise: p2,
|
|
350
|
+
extractResult,
|
|
351
|
+
truncateResult,
|
|
352
|
+
maxResultChars: 5000,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Step 2: Orchestrator checks status (non-blocking)
|
|
356
|
+
expect(registry.hasPending()).toBe(true);
|
|
357
|
+
const status = registry.listAll();
|
|
358
|
+
expect(status).toHaveLength(2);
|
|
359
|
+
|
|
360
|
+
// Step 3: Orchestrator collects all results
|
|
361
|
+
const results = await registry.waitForAll();
|
|
362
|
+
expect(results).toHaveLength(2);
|
|
363
|
+
expect(results.every((r) => r.status === 'completed')).toBe(true);
|
|
364
|
+
|
|
365
|
+
// Step 4: Orchestrator uses results for next delegation
|
|
366
|
+
const researchResult = results.find((r) => r.id === 'researcher')!;
|
|
367
|
+
expect(researchResult.resultText).toBe('Researcher found: X=42');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should handle sequential spawn workflow: spawn → collect → spawn with data → collect', async () => {
|
|
371
|
+
// Round 1: Spawn researcher
|
|
372
|
+
const p1 = createChildPromise('Research: The answer is 42', 10);
|
|
373
|
+
registry.spawn({
|
|
374
|
+
id: 'researcher',
|
|
375
|
+
name: 'Researcher',
|
|
376
|
+
task: 'Find the answer',
|
|
377
|
+
promise: p1,
|
|
378
|
+
extractResult,
|
|
379
|
+
truncateResult,
|
|
380
|
+
maxResultChars: 5000,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Collect round 1 results
|
|
384
|
+
const round1 = await registry.waitForAll();
|
|
385
|
+
const researchData = round1[0].resultText!;
|
|
386
|
+
expect(researchData).toContain('42');
|
|
387
|
+
|
|
388
|
+
// Round 2: Spawn emailer with real data from round 1
|
|
389
|
+
const p2 = createChildPromise('Email sent with answer: 42', 10);
|
|
390
|
+
registry.spawn({
|
|
391
|
+
id: 'emailer',
|
|
392
|
+
name: 'Email Agent',
|
|
393
|
+
task: `Send email with: ${researchData}`,
|
|
394
|
+
promise: p2,
|
|
395
|
+
extractResult,
|
|
396
|
+
truncateResult,
|
|
397
|
+
maxResultChars: 5000,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Collect round 2 results
|
|
401
|
+
const round2 = await registry.waitForAll();
|
|
402
|
+
const emailResult = round2.find((r) => r.id === 'emailer')!;
|
|
403
|
+
expect(emailResult.status).toBe('completed');
|
|
404
|
+
expect(emailResult.resultText).toContain('Email sent');
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
});
|
package/src/graphs/index.ts
CHANGED
package/src/stream.ts
CHANGED
|
@@ -741,7 +741,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
|
|
|
741
741
|
const messageDelta = data as t.MessageDeltaEvent;
|
|
742
742
|
const runStep = stepMap.get(messageDelta.id);
|
|
743
743
|
if (!runStep) {
|
|
744
|
-
|
|
744
|
+
// Expected in handoff subgraphs — reasoning/message deltas can arrive before ON_RUN_STEP
|
|
745
745
|
return;
|
|
746
746
|
}
|
|
747
747
|
|
|
@@ -765,7 +765,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
|
|
|
765
765
|
const reasoningDelta = data as t.ReasoningDeltaEvent;
|
|
766
766
|
const runStep = stepMap.get(reasoningDelta.id);
|
|
767
767
|
if (!runStep) {
|
|
768
|
-
|
|
768
|
+
// Expected in handoff subgraphs — reasoning deltas arrive before ON_RUN_STEP
|
|
769
769
|
return;
|
|
770
770
|
}
|
|
771
771
|
|
|
@@ -786,7 +786,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
|
|
|
786
786
|
|
|
787
787
|
const runStep = stepMap.get(runStepDelta.id);
|
|
788
788
|
if (!runStep) {
|
|
789
|
-
|
|
789
|
+
// Expected in handoff subgraphs — step deltas can arrive before ON_RUN_STEP
|
|
790
790
|
return;
|
|
791
791
|
}
|
|
792
792
|
|
|
@@ -838,9 +838,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
|
|
|
838
838
|
|
|
839
839
|
const runStep = stepMap.get(stepId);
|
|
840
840
|
if (!runStep) {
|
|
841
|
-
|
|
842
|
-
'No run step or runId found for completed tool call event'
|
|
843
|
-
);
|
|
841
|
+
// Expected in handoff subgraphs — completion can arrive for untracked steps
|
|
844
842
|
return;
|
|
845
843
|
}
|
|
846
844
|
|
|
@@ -14,10 +14,10 @@ describe('HITL Approval Constants', () => {
|
|
|
14
14
|
expect(ExecutionContext.SCHEDULED).toBe('scheduled');
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
it('should only have
|
|
17
|
+
it('should only have 3 values (interactive, scheduled, handoff)', () => {
|
|
18
18
|
const values = Object.values(ExecutionContext);
|
|
19
|
-
expect(values).toHaveLength(
|
|
20
|
-
expect(values).toEqual(['interactive', 'scheduled']);
|
|
19
|
+
expect(values).toHaveLength(3);
|
|
20
|
+
expect(values).toEqual(['interactive', 'scheduled', 'handoff']);
|
|
21
21
|
});
|
|
22
22
|
});
|
|
23
23
|
|
package/src/types/graph.ts
CHANGED
|
@@ -56,6 +56,12 @@ export type AgentTransitionEvent = {
|
|
|
56
56
|
destinationAgentName: string;
|
|
57
57
|
edgeType: string; // 'handoff' | 'transfer' | 'sequence'
|
|
58
58
|
timestamp: number;
|
|
59
|
+
/** When true, this event signals handoff completion (child → parent return) */
|
|
60
|
+
isCompletion?: boolean;
|
|
61
|
+
/** Duration of child agent execution in milliseconds (only on completion events) */
|
|
62
|
+
durationMs?: number;
|
|
63
|
+
/** Length of child agent result text in characters (only on completion events) */
|
|
64
|
+
resultLength?: number;
|
|
59
65
|
};
|
|
60
66
|
|
|
61
67
|
export type GraphNode = GraphNodeKeys | typeof START;
|
package/src/utils/events.ts
CHANGED
|
@@ -13,6 +13,9 @@ export async function safeDispatchCustomEvent(
|
|
|
13
13
|
config?: RunnableConfig
|
|
14
14
|
): Promise<void> {
|
|
15
15
|
try {
|
|
16
|
+
if (event === 'on_agent_transition') {
|
|
17
|
+
console.log(`[safeDispatchCustomEvent] Dispatching: ${event}, payload=${JSON.stringify(payload)}`);
|
|
18
|
+
}
|
|
16
19
|
await dispatchCustomEvent(event, payload, config);
|
|
17
20
|
} catch (e) {
|
|
18
21
|
// Check if this is the known EventStreamCallbackHandler error
|