@dxos/assistant 0.8.4-main.dedc0f3 → 0.8.4-main.ead640a

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 (89) hide show
  1. package/dist/lib/browser/{chunk-5GJYG2ML.mjs → chunk-CWGVG235.mjs} +65 -81
  2. package/dist/lib/browser/chunk-CWGVG235.mjs.map +7 -0
  3. package/dist/lib/browser/extraction/index.mjs +18 -14
  4. package/dist/lib/browser/extraction/index.mjs.map +4 -4
  5. package/dist/lib/browser/index.mjs +552 -88
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/node-esm/{chunk-FLFCEB4P.mjs → chunk-OV5PMQPB.mjs} +65 -81
  9. package/dist/lib/node-esm/chunk-OV5PMQPB.mjs.map +7 -0
  10. package/dist/lib/node-esm/extraction/index.mjs +18 -14
  11. package/dist/lib/node-esm/extraction/index.mjs.map +4 -4
  12. package/dist/lib/node-esm/index.mjs +552 -88
  13. package/dist/lib/node-esm/index.mjs.map +4 -4
  14. package/dist/lib/node-esm/meta.json +1 -1
  15. package/dist/types/src/conversation/context.d.ts +14 -7
  16. package/dist/types/src/conversation/context.d.ts.map +1 -1
  17. package/dist/types/src/conversation/conversation.d.ts +9 -10
  18. package/dist/types/src/conversation/conversation.d.ts.map +1 -1
  19. package/dist/types/src/errors.d.ts +38 -38
  20. package/dist/types/src/errors.d.ts.map +1 -1
  21. package/dist/types/src/execution-graph.d.ts +82 -0
  22. package/dist/types/src/execution-graph.d.ts.map +1 -0
  23. package/dist/types/src/execution-graph.test.d.ts +2 -0
  24. package/dist/types/src/execution-graph.test.d.ts.map +1 -0
  25. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts.map +1 -1
  26. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts +61 -57
  27. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts.map +1 -1
  28. package/dist/types/src/extraction/extraction.d.ts +10 -6
  29. package/dist/types/src/extraction/extraction.d.ts.map +1 -1
  30. package/dist/types/src/extraction/quotes.d.ts +1 -1
  31. package/dist/types/src/extraction/quotes.d.ts.map +1 -1
  32. package/dist/types/src/functions/services.d.ts +17 -5
  33. package/dist/types/src/functions/services.d.ts.map +1 -1
  34. package/dist/types/src/index.d.ts +2 -1
  35. package/dist/types/src/index.d.ts.map +1 -1
  36. package/dist/types/src/session/artifact-diff.d.ts +2 -2
  37. package/dist/types/src/session/artifact-diff.d.ts.map +1 -1
  38. package/dist/types/src/session/error-handling.d.ts +2 -2
  39. package/dist/types/src/session/error-handling.d.ts.map +1 -1
  40. package/dist/types/src/session/format.d.ts +34 -32
  41. package/dist/types/src/session/format.d.ts.map +1 -1
  42. package/dist/types/src/session/observer.d.ts +1 -1
  43. package/dist/types/src/session/observer.d.ts.map +1 -1
  44. package/dist/types/src/session/session.d.ts +13 -9
  45. package/dist/types/src/session/session.d.ts.map +1 -1
  46. package/dist/types/src/session/toolkit.d.ts +7 -6
  47. package/dist/types/src/session/toolkit.d.ts.map +1 -1
  48. package/dist/types/src/session/version-pin.d.ts +2 -2
  49. package/dist/types/src/session/version-pin.d.ts.map +1 -1
  50. package/dist/types/src/{types.d.ts → types/artifact.d.ts} +3 -3
  51. package/dist/types/src/types/artifact.d.ts.map +1 -0
  52. package/dist/types/src/types/index.d.ts +2 -0
  53. package/dist/types/src/types/index.d.ts.map +1 -0
  54. package/dist/types/tsconfig.tsbuildinfo +1 -1
  55. package/package.json +26 -31
  56. package/src/conversation/context.ts +49 -17
  57. package/src/conversation/conversation.ts +23 -18
  58. package/src/errors.ts +4 -4
  59. package/src/execution-graph.test.ts +738 -0
  60. package/src/execution-graph.ts +500 -0
  61. package/src/extraction/extraction-llm-function/extraction-llm-function.ts +3 -1
  62. package/src/extraction/extraction-ner-function/extraction-ner-function.ts +3 -1
  63. package/src/extraction/extraction-ner-function/named-entity-recognition.test.ts +2 -1
  64. package/src/extraction/extraction.ts +2 -2
  65. package/src/extraction/quotes.test.ts +2 -2
  66. package/src/extraction/quotes.ts +3 -3
  67. package/src/functions/services.ts +85 -46
  68. package/src/index.ts +2 -1
  69. package/src/session/artifact-diff.ts +2 -2
  70. package/src/session/error-handling.ts +11 -6
  71. package/src/session/format.test.ts +1 -1
  72. package/src/session/format.ts +9 -6
  73. package/src/session/observer.ts +2 -1
  74. package/src/session/session.conversations.json +1 -0
  75. package/src/session/session.test.ts +50 -209
  76. package/src/session/session.ts +31 -20
  77. package/src/session/toolkit.ts +19 -13
  78. package/src/session/version-pin.ts +2 -2
  79. package/src/summarization/summarization.test.ts +8 -3
  80. package/src/templates/system.tpl +10 -12
  81. package/src/{types.ts → types/artifact.ts} +2 -2
  82. package/src/types/index.ts +5 -0
  83. package/dist/lib/browser/chunk-5GJYG2ML.mjs.map +0 -7
  84. package/dist/lib/node-esm/chunk-FLFCEB4P.mjs.map +0 -7
  85. package/dist/types/src/session/version-pin.test.d.ts +0 -2
  86. package/dist/types/src/session/version-pin.test.d.ts.map +0 -1
  87. package/dist/types/src/types.d.ts.map +0 -1
  88. package/src/session/__snapshots__/session.test.ts.snap +0 -32
  89. package/src/session/version-pin.test.ts +0 -7
@@ -0,0 +1,738 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { describe, expect, it } from '@effect/vitest';
6
+
7
+ import { AgentStatus } from '@dxos/ai';
8
+ import { Obj } from '@dxos/echo';
9
+ import { ObjectId } from '@dxos/echo/internal';
10
+ import { log } from '@dxos/log';
11
+ import { DataType } from '@dxos/schema';
12
+
13
+ import { ExecutionGraph } from './execution-graph';
14
+
15
+ // Helper function to create valid object IDs for testing
16
+ const createTestId = () => ObjectId.random();
17
+
18
+ describe('ExecutionGraph', () => {
19
+ describe('basic functionality', () => {
20
+ it('should create an empty graph initially', () => {
21
+ const graph = new ExecutionGraph();
22
+ const result = graph.getGraph();
23
+
24
+ expect(result.branches).toEqual([]);
25
+ expect(result.commits).toEqual([]);
26
+ });
27
+
28
+ it('should add a simple user message', () => {
29
+ const graph = new ExecutionGraph();
30
+ const messageId = createTestId();
31
+ const message = Obj.make(DataType.Message, {
32
+ id: messageId,
33
+ created: '2025-01-01T00:00:00Z',
34
+ sender: { role: 'user' },
35
+ blocks: [
36
+ {
37
+ _tag: 'text',
38
+ text: 'Hello, world!',
39
+ },
40
+ ],
41
+ properties: {},
42
+ });
43
+
44
+ graph.addEvents([message]);
45
+ const result = graph.getGraph();
46
+
47
+ expect(result.branches).toEqual(['main']);
48
+ expect(result.commits).toHaveLength(1);
49
+ expect(result.commits[0]).toMatchObject({
50
+ id: `${messageId}_block_0`,
51
+ branch: 'main',
52
+ message: 'Processing request...',
53
+ parents: [],
54
+ tags: ['user'],
55
+ });
56
+ });
57
+
58
+ it('should handle basic status & message processing', () => {
59
+ const graph = new ExecutionGraph();
60
+ const status = Obj.make(AgentStatus, {
61
+ created: '2025-01-01T00:00:00Z',
62
+ message: 'Running Research',
63
+ });
64
+ const message = Obj.make(DataType.Message, {
65
+ created: '2025-09-29T14:29:37.860Z',
66
+ sender: {
67
+ role: 'user',
68
+ },
69
+ blocks: [
70
+ {
71
+ _tag: 'text',
72
+ text: '{"id":"01K6AXZKK79H4E54RKP1YZDARK","@type":"dxn:type:dxos.org/type/Organization:0.1.0","@meta":{"keys":[]},"name":"Blue Yard","website":"https://blueyard.com"}',
73
+ },
74
+ ],
75
+ properties: {},
76
+ });
77
+
78
+ graph.addEvents([status, message]);
79
+ const result = graph.getGraph();
80
+ log.info('result', result);
81
+
82
+ expect(result.branches).toEqual(['main']);
83
+ expect(result.commits).toHaveLength(2);
84
+ expect(result.commits[0].branch).toEqual('main');
85
+ expect(result.commits[0].message).toEqual('Running Research');
86
+ expect(result.commits[0].parents).toEqual([]);
87
+ expect(result.commits[1].branch).toEqual('main');
88
+ expect(result.commits[1].message).toEqual('Processing request...');
89
+ expect(result.commits[1].parents).toEqual([status.id]);
90
+ });
91
+
92
+ it('should add an assistant message with text', () => {
93
+ const graph = new ExecutionGraph();
94
+ const messageId = createTestId();
95
+ const message = Obj.make(DataType.Message, {
96
+ id: messageId,
97
+ created: '2025-01-01T00:00:00Z',
98
+ sender: { role: 'assistant' },
99
+ blocks: [
100
+ {
101
+ _tag: 'text',
102
+ text: 'Hello! How can I help you today?',
103
+ },
104
+ ],
105
+ properties: {},
106
+ });
107
+
108
+ graph.addEvents([message]);
109
+ const result = graph.getGraph();
110
+
111
+ expect(result.commits).toHaveLength(1);
112
+ expect(result.commits[0]).toMatchObject({
113
+ id: `${messageId}_block_0`,
114
+ branch: 'main',
115
+ message: 'Response (7 words)',
116
+ parents: [],
117
+ });
118
+ });
119
+ });
120
+
121
+ describe('sequential chaining within messages', () => {
122
+ it('should chain blocks within a message', () => {
123
+ const graph = new ExecutionGraph();
124
+ const messageId = createTestId();
125
+ const message = Obj.make(DataType.Message, {
126
+ id: messageId,
127
+ created: '2025-01-01T00:00:00Z',
128
+ sender: { role: 'assistant' },
129
+ blocks: [
130
+ {
131
+ _tag: 'text',
132
+ text: 'First block',
133
+ },
134
+ {
135
+ _tag: 'text',
136
+ text: 'Second block',
137
+ },
138
+ {
139
+ _tag: 'text',
140
+ text: 'Third block',
141
+ },
142
+ ],
143
+ properties: {},
144
+ });
145
+
146
+ graph.addEvents([message]);
147
+ const result = graph.getGraph();
148
+
149
+ expect(result.commits).toHaveLength(3);
150
+
151
+ // First block has no parents
152
+ expect(result.commits[0]).toMatchObject({
153
+ id: `${messageId}_block_0`,
154
+ parents: [],
155
+ });
156
+
157
+ // Second block has first block as parent
158
+ expect(result.commits[1]).toMatchObject({
159
+ id: `${messageId}_block_1`,
160
+ parents: [`${messageId}_block_0`],
161
+ });
162
+
163
+ // Third block has second block as parent
164
+ expect(result.commits[2]).toMatchObject({
165
+ id: `${messageId}_block_2`,
166
+ parents: [`${messageId}_block_1`],
167
+ });
168
+ });
169
+
170
+ it('should chain across multiple messages', () => {
171
+ const graph = new ExecutionGraph();
172
+
173
+ // First message
174
+ const message1Id = createTestId();
175
+ const message1 = Obj.make(DataType.Message, {
176
+ id: message1Id,
177
+ created: '2025-01-01T00:00:00Z',
178
+ sender: { role: 'user' },
179
+ blocks: [{ _tag: 'text', text: 'Hello' }],
180
+ properties: {},
181
+ });
182
+
183
+ // Second message
184
+ const message2Id = createTestId();
185
+ const message2 = Obj.make(DataType.Message, {
186
+ id: message2Id,
187
+ created: '2025-01-01T00:01:00Z',
188
+ sender: { role: 'assistant' },
189
+ blocks: [{ _tag: 'text', text: 'Hi there!' }],
190
+ properties: {},
191
+ });
192
+
193
+ graph.addEvents([message1, message2]);
194
+ const result = graph.getGraph();
195
+
196
+ expect(result.commits).toHaveLength(2);
197
+
198
+ // First message block has no parents
199
+ expect(result.commits[0]).toMatchObject({
200
+ id: `${message1Id}_block_0`,
201
+ parents: [],
202
+ });
203
+
204
+ // Second message block has first message block as parent
205
+ expect(result.commits[1]).toMatchObject({
206
+ id: `${message2Id}_block_0`,
207
+ parents: [`${message1Id}_block_0`],
208
+ });
209
+ });
210
+ });
211
+
212
+ describe('tool calls and results', () => {
213
+ it('should handle tool calls and results with proper parent relationships', () => {
214
+ const graph = new ExecutionGraph();
215
+
216
+ const message1Id = createTestId();
217
+ const message2Id = createTestId();
218
+ const statusId = createTestId();
219
+ const toolCallId = 'tool1';
220
+
221
+ // Assistant message with tool call
222
+ const toolCallMessage = Obj.make(DataType.Message, {
223
+ id: message1Id,
224
+ created: '2025-01-01T00:00:00Z',
225
+ sender: { role: 'assistant' },
226
+ blocks: [
227
+ {
228
+ _tag: 'text',
229
+ text: 'I will help you with that.',
230
+ },
231
+ {
232
+ _tag: 'toolCall',
233
+ toolCallId,
234
+ name: 'test_tool',
235
+ input: '{"param": "value"}',
236
+ providerExecuted: false,
237
+ },
238
+ ],
239
+ properties: {},
240
+ });
241
+
242
+ // User message with tool result
243
+ const toolResultMessage = Obj.make(DataType.Message, {
244
+ id: message2Id,
245
+ created: '2025-01-01T00:01:00Z',
246
+ sender: { role: 'user' },
247
+ blocks: [
248
+ {
249
+ _tag: 'toolResult',
250
+ toolCallId,
251
+ name: 'test_tool',
252
+ result: '{"result": "success"}',
253
+ providerExecuted: false,
254
+ },
255
+ ],
256
+ properties: {},
257
+ });
258
+
259
+ // AgentStatus event
260
+ const agentStatus = Obj.make(AgentStatus, {
261
+ id: statusId,
262
+ created: '2025-01-01T00:02:00Z',
263
+ parentMessage: message1Id,
264
+ toolCallId,
265
+ message: 'Processing tool call...',
266
+ });
267
+
268
+ graph.addEvents([toolCallMessage, toolResultMessage, agentStatus]);
269
+ const result = graph.getGraph();
270
+
271
+ expect(result.commits).toHaveLength(4);
272
+
273
+ // Text block
274
+ expect(result.commits[0]).toMatchObject({
275
+ id: `${message1Id}_block_0`,
276
+ parents: [],
277
+ });
278
+
279
+ // Tool call block
280
+ expect(result.commits[1]).toMatchObject({
281
+ id: `${message1Id}_toolCall_${toolCallId}`,
282
+ parents: [`${message1Id}_block_0`],
283
+ });
284
+
285
+ // Tool result should have two parents: AgentStatus and tool call
286
+ expect(result.commits[2]).toMatchObject({
287
+ id: `${message2Id}_toolResult_${toolCallId}`,
288
+ parents: [statusId, `${message1Id}_toolCall_${toolCallId}`],
289
+ });
290
+
291
+ // AgentStatus
292
+ expect(result.commits[3]).toMatchObject({
293
+ id: statusId,
294
+ parents: [`${message1Id}_toolCall_${toolCallId}`],
295
+ });
296
+ });
297
+
298
+ it('should handle tool errors with proper parent relationships', () => {
299
+ const graph = new ExecutionGraph();
300
+
301
+ const message1Id = createTestId();
302
+ const message2Id = createTestId();
303
+ const statusId = createTestId();
304
+ const toolCallId = 'tool1';
305
+
306
+ // Assistant message with tool call
307
+ const toolCallMessage = Obj.make(DataType.Message, {
308
+ id: message1Id,
309
+ created: '2025-01-01T00:00:00Z',
310
+ sender: { role: 'assistant' },
311
+ blocks: [
312
+ {
313
+ _tag: 'toolCall',
314
+ toolCallId,
315
+ name: 'test_tool',
316
+ input: '{"param": "value"}',
317
+ providerExecuted: false,
318
+ },
319
+ ],
320
+ properties: {},
321
+ });
322
+
323
+ // User message with tool error
324
+ const toolErrorMessage = Obj.make(DataType.Message, {
325
+ id: message2Id,
326
+ created: '2025-01-01T00:01:00Z',
327
+ sender: { role: 'user' },
328
+ blocks: [
329
+ {
330
+ _tag: 'toolResult',
331
+ toolCallId,
332
+ name: 'test_tool',
333
+ error: 'Tool execution failed',
334
+ providerExecuted: false,
335
+ },
336
+ ],
337
+ properties: {},
338
+ });
339
+
340
+ // AgentStatus event
341
+ const agentStatus = Obj.make(AgentStatus, {
342
+ id: statusId,
343
+ created: '2025-01-01T00:02:00Z',
344
+ parentMessage: message1Id,
345
+ toolCallId,
346
+ message: 'Tool execution failed',
347
+ });
348
+
349
+ graph.addEvents([toolCallMessage, toolErrorMessage, agentStatus]);
350
+ const result = graph.getGraph();
351
+
352
+ expect(result.commits).toHaveLength(3);
353
+
354
+ // Tool call block
355
+ expect(result.commits[0]).toMatchObject({
356
+ id: `${message1Id}_toolCall_${toolCallId}`,
357
+ parents: [],
358
+ });
359
+
360
+ // Tool error should have two parents: AgentStatus and tool call
361
+ expect(result.commits[1]).toMatchObject({
362
+ id: `${message2Id}_toolResult_${toolCallId}`,
363
+ parents: [statusId, `${message1Id}_toolCall_${toolCallId}`],
364
+ });
365
+
366
+ // AgentStatus
367
+ expect(result.commits[2]).toMatchObject({
368
+ id: statusId,
369
+ parents: [`${message1Id}_toolCall_${toolCallId}`],
370
+ });
371
+ });
372
+ });
373
+
374
+ describe('different processing orders', () => {
375
+ it('should handle AgentStatus processed after tool result', () => {
376
+ const graph = new ExecutionGraph();
377
+
378
+ const message1Id = createTestId();
379
+ const message2Id = createTestId();
380
+ const statusId = createTestId();
381
+ const toolCallId = 'tool1';
382
+
383
+ // Tool call message
384
+ const toolCallMessage = Obj.make(DataType.Message, {
385
+ id: message1Id,
386
+ created: '2025-01-01T00:00:00Z',
387
+ sender: { role: 'assistant' },
388
+ blocks: [
389
+ {
390
+ _tag: 'toolCall',
391
+ toolCallId,
392
+ name: 'test_tool',
393
+ input: '{}',
394
+ providerExecuted: false,
395
+ },
396
+ ],
397
+ properties: {},
398
+ });
399
+
400
+ // Tool result message
401
+ const toolResultMessage = Obj.make(DataType.Message, {
402
+ id: message2Id,
403
+ created: '2025-01-01T00:01:00Z',
404
+ sender: { role: 'user' },
405
+ blocks: [
406
+ {
407
+ _tag: 'toolResult',
408
+ toolCallId,
409
+ name: 'test_tool',
410
+ result: '{"result": "success"}',
411
+ providerExecuted: false,
412
+ },
413
+ ],
414
+ properties: {},
415
+ });
416
+
417
+ // AgentStatus event (processed after tool result)
418
+ const agentStatus = Obj.make(AgentStatus, {
419
+ id: statusId,
420
+ created: '2025-01-01T00:00:00Z',
421
+ parentMessage: message1Id,
422
+ toolCallId,
423
+ message: 'Processing...',
424
+ });
425
+
426
+ graph.addEvents([toolCallMessage, toolResultMessage, agentStatus]);
427
+ const result = graph.getGraph();
428
+
429
+ // Tool result should be updated to point to AgentStatus
430
+ const toolResult = result.commits.find((c) => c.id === `${message2Id}_toolResult_${toolCallId}`);
431
+ expect(toolResult?.parents).toContain(statusId);
432
+ });
433
+
434
+ it('should handle AgentStatus processed before tool result', () => {
435
+ const graph = new ExecutionGraph();
436
+
437
+ const message1Id = createTestId();
438
+ const message2Id = createTestId();
439
+ const statusId = createTestId();
440
+ const toolCallId = 'tool1';
441
+
442
+ // Tool call message
443
+ const toolCallMessage = Obj.make(DataType.Message, {
444
+ id: message1Id,
445
+ created: '2025-01-01T00:00:00Z',
446
+ sender: { role: 'assistant' },
447
+ blocks: [
448
+ {
449
+ _tag: 'toolCall',
450
+ toolCallId,
451
+ name: 'test_tool',
452
+ input: '{}',
453
+ providerExecuted: false,
454
+ },
455
+ ],
456
+ properties: {},
457
+ });
458
+
459
+ // AgentStatus event (processed before tool result)
460
+ const agentStatus = Obj.make(AgentStatus, {
461
+ id: statusId,
462
+ created: '2025-01-01T00:00:00Z',
463
+ parentMessage: message1Id,
464
+ toolCallId,
465
+ message: 'Processing...',
466
+ });
467
+
468
+ // Tool result message
469
+ const toolResultMessage = Obj.make(DataType.Message, {
470
+ id: message2Id,
471
+ created: '2025-01-01T00:01:00Z',
472
+ sender: { role: 'user' },
473
+ blocks: [
474
+ {
475
+ _tag: 'toolResult',
476
+ toolCallId,
477
+ name: 'test_tool',
478
+ result: '{"result": "success"}',
479
+ providerExecuted: false,
480
+ },
481
+ ],
482
+ properties: {},
483
+ });
484
+
485
+ graph.addEvents([toolCallMessage, agentStatus, toolResultMessage]);
486
+ const result = graph.getGraph();
487
+
488
+ // Tool result should point to AgentStatus
489
+ const toolResult = result.commits.find((c) => c.id === `${message2Id}_toolResult_${toolCallId}`);
490
+ expect(toolResult?.parents).toContain(statusId);
491
+ });
492
+ });
493
+
494
+ describe('complex scenarios', () => {
495
+ it('should handle multiple tool calls in sequence', () => {
496
+ const graph = new ExecutionGraph();
497
+
498
+ const msg1Id = createTestId();
499
+ const msg2Id = createTestId();
500
+ const msg3Id = createTestId();
501
+ const msg4Id = createTestId();
502
+ const msg5Id = createTestId();
503
+ const status1Id = createTestId();
504
+ const status2Id = createTestId();
505
+
506
+ const events = [
507
+ // User message
508
+ Obj.make(DataType.Message, {
509
+ id: msg1Id,
510
+ created: '2025-01-01T00:00:00Z',
511
+ sender: { role: 'user' },
512
+ blocks: [{ _tag: 'text', text: 'Help me with two tasks' }],
513
+ properties: {},
514
+ }),
515
+
516
+ // Assistant with first tool call
517
+ Obj.make(DataType.Message, {
518
+ id: msg2Id,
519
+ created: '2025-01-01T00:01:00Z',
520
+ sender: { role: 'assistant' },
521
+ blocks: [
522
+ { _tag: 'text', text: 'I will help with both tasks' },
523
+ { _tag: 'toolCall', toolCallId: 'tool1', name: 'task1', input: '{}', providerExecuted: false },
524
+ ],
525
+ properties: {},
526
+ }),
527
+
528
+ // First tool result
529
+ Obj.make(DataType.Message, {
530
+ id: msg3Id,
531
+ created: '2025-01-01T00:02:00Z',
532
+ sender: { role: 'user' },
533
+ blocks: [
534
+ {
535
+ _tag: 'toolResult',
536
+ toolCallId: 'tool1',
537
+ name: 'task1',
538
+ result: '{"done": true}',
539
+ providerExecuted: false,
540
+ },
541
+ ],
542
+ properties: {},
543
+ }),
544
+
545
+ // Assistant with second tool call
546
+ Obj.make(DataType.Message, {
547
+ id: msg4Id,
548
+ created: '2025-01-01T00:03:00Z',
549
+ sender: { role: 'assistant' },
550
+ blocks: [
551
+ { _tag: 'text', text: 'Now for the second task' },
552
+ { _tag: 'toolCall', toolCallId: 'tool2', name: 'task2', input: '{}', providerExecuted: false },
553
+ ],
554
+ properties: {},
555
+ }),
556
+
557
+ // Second tool result
558
+ Obj.make(DataType.Message, {
559
+ id: msg5Id,
560
+ created: '2025-01-01T00:04:00Z',
561
+ sender: { role: 'user' },
562
+ blocks: [
563
+ {
564
+ _tag: 'toolResult',
565
+ toolCallId: 'tool2',
566
+ name: 'task2',
567
+ result: '{"done": true}',
568
+ providerExecuted: false,
569
+ },
570
+ ],
571
+ properties: {},
572
+ }),
573
+
574
+ // AgentStatus events
575
+ Obj.make(AgentStatus, {
576
+ id: status1Id,
577
+ created: '2025-01-01T00:05:00Z',
578
+ parentMessage: msg2Id,
579
+ toolCallId: 'tool1',
580
+ message: 'Processing task 1...',
581
+ }),
582
+
583
+ Obj.make(AgentStatus, {
584
+ id: status2Id,
585
+ created: '2025-01-01T00:06:00Z',
586
+ parentMessage: msg4Id,
587
+ toolCallId: 'tool2',
588
+ message: 'Processing task 2...',
589
+ }),
590
+ ];
591
+
592
+ graph.addEvents(events);
593
+ const result = graph.getGraph();
594
+
595
+ expect(result.commits).toHaveLength(9);
596
+
597
+ // Check sequential chaining
598
+ expect(result.commits[0].parents).toEqual([]); // msg1_block_0
599
+ expect(result.commits[1].parents).toEqual([`${msg1Id}_block_0`]); // msg2_block_0
600
+ expect(result.commits[2].parents).toEqual([`${msg2Id}_block_0`]); // msg2_toolCall_tool1
601
+ expect(result.commits[3].parents).toEqual([status1Id, `${msg2Id}_toolCall_tool1`]); // msg3_toolResult_tool1
602
+ expect(result.commits[4].parents).toEqual([`${msg3Id}_toolResult_tool1`]); // msg4_block_0
603
+ expect(result.commits[5].parents).toEqual([`${msg4Id}_block_0`]); // msg4_toolCall_tool2
604
+ expect(result.commits[6].parents).toEqual([status2Id, `${msg4Id}_toolCall_tool2`]); // msg5_toolResult_tool2
605
+ });
606
+
607
+ it('should handle mixed block types in a message', () => {
608
+ const graph = new ExecutionGraph();
609
+
610
+ const messageId = createTestId();
611
+ const message = Obj.make(DataType.Message, {
612
+ id: messageId,
613
+ created: '2025-01-01T00:00:00Z',
614
+ sender: { role: 'assistant' },
615
+ blocks: [
616
+ { _tag: 'text', text: 'Let me help you' },
617
+ { _tag: 'reasoning', reasoningText: 'I need to think about this' },
618
+ { _tag: 'toolCall', toolCallId: 'tool1', name: 'analyze', input: '{}', providerExecuted: false },
619
+ {
620
+ _tag: 'summary',
621
+ message: 'OK',
622
+ duration: 1000,
623
+ toolCalls: 1,
624
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
625
+ },
626
+ ],
627
+ properties: {},
628
+ });
629
+
630
+ graph.addEvents([message]);
631
+ const result = graph.getGraph();
632
+
633
+ expect(result.commits).toHaveLength(4);
634
+
635
+ // All blocks should be chained sequentially
636
+ expect(result.commits[0].parents).toEqual([]); // text
637
+ expect(result.commits[1].parents).toEqual([`${messageId}_block_0`]); // reasoning
638
+ expect(result.commits[2].parents).toEqual([`${messageId}_block_1`]); // toolCall
639
+ expect(result.commits[3].parents).toEqual([`${messageId}_toolCall_tool1`]); // summary
640
+ });
641
+ });
642
+
643
+ describe('edge cases', () => {
644
+ it('should handle empty text blocks', () => {
645
+ const graph = new ExecutionGraph();
646
+
647
+ const messageId = createTestId();
648
+ const message = Obj.make(DataType.Message, {
649
+ id: messageId,
650
+ created: '2025-01-01T00:00:00Z',
651
+ sender: { role: 'assistant' },
652
+ blocks: [
653
+ { _tag: 'text', text: 'Valid text' },
654
+ { _tag: 'text', text: '' }, // Empty text should be filtered out
655
+ { _tag: 'text', text: 'More valid text' },
656
+ ],
657
+ properties: {},
658
+ });
659
+
660
+ graph.addEvents([message]);
661
+ const result = graph.getGraph();
662
+
663
+ expect(result.commits).toHaveLength(2); // Empty block should be filtered out
664
+ expect(result.commits[0].parents).toEqual([]); // First text block
665
+ expect(result.commits[1].parents).toEqual([`${messageId}_block_0`]); // Third text block
666
+ });
667
+
668
+ it('should handle messages with no blocks', () => {
669
+ const graph = new ExecutionGraph();
670
+
671
+ const messageId = createTestId();
672
+ const message = Obj.make(DataType.Message, {
673
+ id: messageId,
674
+ created: '2025-01-01T00:00:00Z',
675
+ sender: { role: 'assistant' },
676
+ blocks: [],
677
+ properties: {},
678
+ });
679
+
680
+ graph.addEvents([message]);
681
+ const result = graph.getGraph();
682
+
683
+ expect(result.commits).toHaveLength(0);
684
+ });
685
+
686
+ it('should handle getGraph with lastRequest filter', () => {
687
+ const graph = new ExecutionGraph();
688
+
689
+ const msg1Id = createTestId();
690
+ const msg2Id = createTestId();
691
+ const msg3Id = createTestId();
692
+ const msg4Id = createTestId();
693
+
694
+ const events = [
695
+ Obj.make(DataType.Message, {
696
+ id: msg1Id,
697
+ created: '2025-01-01T00:00:00Z',
698
+ sender: { role: 'user' },
699
+ blocks: [{ _tag: 'text', text: 'First request' }],
700
+ properties: {},
701
+ }),
702
+ Obj.make(DataType.Message, {
703
+ id: msg2Id,
704
+ created: '2025-01-01T00:01:00Z',
705
+ sender: { role: 'assistant' },
706
+ blocks: [{ _tag: 'text', text: 'Response 1' }],
707
+ properties: {},
708
+ }),
709
+ Obj.make(DataType.Message, {
710
+ id: msg3Id,
711
+ created: '2025-01-01T00:02:00Z',
712
+ sender: { role: 'user' },
713
+ blocks: [{ _tag: 'text', text: 'Second request' }],
714
+ properties: {},
715
+ }),
716
+ Obj.make(DataType.Message, {
717
+ id: msg4Id,
718
+ created: '2025-01-01T00:03:00Z',
719
+ sender: { role: 'assistant' },
720
+ blocks: [{ _tag: 'text', text: 'Response 2' }],
721
+ properties: {},
722
+ }),
723
+ ];
724
+
725
+ graph.addEvents(events);
726
+
727
+ // Get all commits
728
+ const allCommits = graph.getGraph();
729
+ expect(allCommits.commits).toHaveLength(4);
730
+
731
+ // Get commits from last user request
732
+ const lastRequestCommits = graph.getGraph(true);
733
+ expect(lastRequestCommits.commits).toHaveLength(2); // Only msg3 and msg4
734
+ expect(lastRequestCommits.commits[0].id).toBe(`${msg3Id}_block_0`);
735
+ expect(lastRequestCommits.commits[1].id).toBe(`${msg4Id}_block_0`);
736
+ });
737
+ });
738
+ });