@fluxpointstudios/orynq-sdk-process-trace 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/dist/index.cjs +574 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +573 -9
- package/dist/index.d.ts +573 -9
- package/dist/index.js +562 -9
- package/dist/index.js.map +1 -1
- package/package.json +17 -3
- package/src/__tests__/bundle.test.ts +942 -860
- package/src/__tests__/governance-round4.test.ts +149 -0
- package/src/__tests__/governance.test.ts +382 -0
- package/src/__tests__/hardening-round2.test.ts +173 -0
- package/src/__tests__/hardening-round3.test.ts +208 -0
- package/src/__tests__/integration.test.ts +611 -611
- package/src/__tests__/merkle.test.ts +756 -756
- package/src/__tests__/model-manifest.test.ts +136 -0
- package/src/__tests__/rolling-hash.test.ts +622 -622
- package/src/__tests__/trace-builder.test.ts +1012 -1012
- package/src/__tests__/types.test.ts +420 -414
- package/src/bundle.ts +1004 -810
- package/src/disclosure.ts +527 -527
- package/src/governance.ts +711 -0
- package/src/index.ts +334 -265
- package/src/manifest.ts +687 -687
- package/src/merkle.ts +428 -428
- package/src/model-manifest.ts +0 -0
- package/src/rolling-hash.ts +375 -366
- package/src/trace-builder.ts +791 -725
- package/src/types.ts +713 -522
package/src/trace-builder.ts
CHANGED
|
@@ -1,725 +1,791 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Main API for building traces - creating runs, adding spans, events, and finalizing.
|
|
3
|
-
*
|
|
4
|
-
* Location: packages/process-trace/src/trace-builder.ts
|
|
5
|
-
*
|
|
6
|
-
* This module provides the primary entry points for constructing trace runs. It handles:
|
|
7
|
-
* - Creating new trace runs with proper initialization
|
|
8
|
-
* - Adding spans (logical groupings of related events)
|
|
9
|
-
* - Adding events with automatic sequencing, timestamping, and hashing
|
|
10
|
-
* - Closing spans and computing span hashes
|
|
11
|
-
* - Finalizing traces with Merkle tree construction and root hash computation
|
|
12
|
-
* - Generating public views for external sharing
|
|
13
|
-
*
|
|
14
|
-
* The trace builder maintains internal state (rolling hash, sequence counters) and
|
|
15
|
-
* ensures cryptographic integrity at each step. Events are ordered by monotonic
|
|
16
|
-
* sequence numbers (seq), not timestamps, to guarantee deterministic ordering.
|
|
17
|
-
*
|
|
18
|
-
* Used by:
|
|
19
|
-
* - Agent implementations to record execution traces
|
|
20
|
-
* - Integration tests for trace verification
|
|
21
|
-
* - Audit workflows for compliance reporting
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```typescript
|
|
25
|
-
* // Create a new trace
|
|
26
|
-
* const run = await createTrace({ agentId: "agent-1" });
|
|
27
|
-
*
|
|
28
|
-
* // Add a span for a logical unit of work
|
|
29
|
-
* const span = addSpan(run, { name: "build-project" });
|
|
30
|
-
*
|
|
31
|
-
* // Add events to the span
|
|
32
|
-
* await addEvent(run, span.id, { kind: "command", command: "npm install" });
|
|
33
|
-
* await addEvent(run, span.id, { kind: "output", stream: "stdout", content: "done" });
|
|
34
|
-
*
|
|
35
|
-
* // Close the span and finalize
|
|
36
|
-
* await closeSpan(run, span.id);
|
|
37
|
-
* const bundle = await finalizeTrace(run);
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
import type {
|
|
42
|
-
TraceRun,
|
|
43
|
-
TraceSpan,
|
|
44
|
-
TraceEvent,
|
|
45
|
-
TraceBundle,
|
|
46
|
-
TraceBundlePublicView,
|
|
47
|
-
CreateTraceOptions,
|
|
48
|
-
CreateSpanOptions,
|
|
49
|
-
Visibility,
|
|
50
|
-
TraceStatus,
|
|
51
|
-
TraceEventKind,
|
|
52
|
-
AnnotatedSpan,
|
|
53
|
-
} from "./types.js";
|
|
54
|
-
import { DEFAULT_EVENT_VISIBILITY } from "./types.js";
|
|
55
|
-
import {
|
|
56
|
-
computeEventHash,
|
|
57
|
-
initRollingHash,
|
|
58
|
-
updateRollingHash,
|
|
59
|
-
computeRootHash,
|
|
60
|
-
} from "./rolling-hash.js";
|
|
61
|
-
import { buildSpanMerkleTree, computeSpanHash } from "./merkle.js";
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* -
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* @
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Add
|
|
125
|
-
if (opts.
|
|
126
|
-
run.metadata = {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
name
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
//
|
|
225
|
-
run.
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
//
|
|
470
|
-
span
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
* @
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
run
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
run
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
//
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
//
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main API for building traces - creating runs, adding spans, events, and finalizing.
|
|
3
|
+
*
|
|
4
|
+
* Location: packages/process-trace/src/trace-builder.ts
|
|
5
|
+
*
|
|
6
|
+
* This module provides the primary entry points for constructing trace runs. It handles:
|
|
7
|
+
* - Creating new trace runs with proper initialization
|
|
8
|
+
* - Adding spans (logical groupings of related events)
|
|
9
|
+
* - Adding events with automatic sequencing, timestamping, and hashing
|
|
10
|
+
* - Closing spans and computing span hashes
|
|
11
|
+
* - Finalizing traces with Merkle tree construction and root hash computation
|
|
12
|
+
* - Generating public views for external sharing
|
|
13
|
+
*
|
|
14
|
+
* The trace builder maintains internal state (rolling hash, sequence counters) and
|
|
15
|
+
* ensures cryptographic integrity at each step. Events are ordered by monotonic
|
|
16
|
+
* sequence numbers (seq), not timestamps, to guarantee deterministic ordering.
|
|
17
|
+
*
|
|
18
|
+
* Used by:
|
|
19
|
+
* - Agent implementations to record execution traces
|
|
20
|
+
* - Integration tests for trace verification
|
|
21
|
+
* - Audit workflows for compliance reporting
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* // Create a new trace
|
|
26
|
+
* const run = await createTrace({ agentId: "agent-1" });
|
|
27
|
+
*
|
|
28
|
+
* // Add a span for a logical unit of work
|
|
29
|
+
* const span = addSpan(run, { name: "build-project" });
|
|
30
|
+
*
|
|
31
|
+
* // Add events to the span
|
|
32
|
+
* await addEvent(run, span.id, { kind: "command", command: "npm install" });
|
|
33
|
+
* await addEvent(run, span.id, { kind: "output", stream: "stdout", content: "done" });
|
|
34
|
+
*
|
|
35
|
+
* // Close the span and finalize
|
|
36
|
+
* await closeSpan(run, span.id);
|
|
37
|
+
* const bundle = await finalizeTrace(run);
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import type {
|
|
42
|
+
TraceRun,
|
|
43
|
+
TraceSpan,
|
|
44
|
+
TraceEvent,
|
|
45
|
+
TraceBundle,
|
|
46
|
+
TraceBundlePublicView,
|
|
47
|
+
CreateTraceOptions,
|
|
48
|
+
CreateSpanOptions,
|
|
49
|
+
Visibility,
|
|
50
|
+
TraceStatus,
|
|
51
|
+
TraceEventKind,
|
|
52
|
+
AnnotatedSpan,
|
|
53
|
+
} from "./types.js";
|
|
54
|
+
import { DEFAULT_EVENT_VISIBILITY } from "./types.js";
|
|
55
|
+
import {
|
|
56
|
+
computeEventHash,
|
|
57
|
+
initRollingHash,
|
|
58
|
+
updateRollingHash,
|
|
59
|
+
computeRootHash,
|
|
60
|
+
} from "./rolling-hash.js";
|
|
61
|
+
import { buildSpanMerkleTree, computeSpanHash } from "./merkle.js";
|
|
62
|
+
import {
|
|
63
|
+
computeModelManifestHash,
|
|
64
|
+
validateModelManifest,
|
|
65
|
+
freezeModelManifest,
|
|
66
|
+
} from "./model-manifest.js";
|
|
67
|
+
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// TRACE CREATION
|
|
70
|
+
// =============================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a new trace run.
|
|
74
|
+
*
|
|
75
|
+
* Initializes a fresh trace with:
|
|
76
|
+
* - Unique UUID for the run ID
|
|
77
|
+
* - Schema version "1.0"
|
|
78
|
+
* - Status "running"
|
|
79
|
+
* - Genesis rolling hash state
|
|
80
|
+
* - Empty events and spans arrays
|
|
81
|
+
* - Sequence counters at 0
|
|
82
|
+
*
|
|
83
|
+
* @param opts - Options for creating the trace
|
|
84
|
+
* @param opts.agentId - Identifier of the agent producing this trace
|
|
85
|
+
* @param opts.description - Optional human-readable description
|
|
86
|
+
* @param opts.metadata - Optional key-value metadata
|
|
87
|
+
* @returns Promise resolving to the initialized TraceRun
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* const run = await createTrace({
|
|
92
|
+
* agentId: "claude-agent-v1",
|
|
93
|
+
* description: "Build and test the project",
|
|
94
|
+
* metadata: { environment: "production" },
|
|
95
|
+
* });
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export async function createTrace(opts: CreateTraceOptions): Promise<TraceRun> {
|
|
99
|
+
// Validate required fields
|
|
100
|
+
if (!opts.agentId || typeof opts.agentId !== "string") {
|
|
101
|
+
throw new Error("agentId is required and must be a non-empty string");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Generate unique run ID using crypto.randomUUID (Node 18+)
|
|
105
|
+
const runId = crypto.randomUUID();
|
|
106
|
+
|
|
107
|
+
// Initialize rolling hash state
|
|
108
|
+
const hashState = await initRollingHash();
|
|
109
|
+
|
|
110
|
+
// Build the trace run object
|
|
111
|
+
const run: TraceRun = {
|
|
112
|
+
id: runId,
|
|
113
|
+
schemaVersion: "1.0",
|
|
114
|
+
agentId: opts.agentId,
|
|
115
|
+
status: "running",
|
|
116
|
+
startedAt: new Date().toISOString(),
|
|
117
|
+
events: [],
|
|
118
|
+
spans: [],
|
|
119
|
+
rollingHash: hashState.currentHash,
|
|
120
|
+
nextSeq: 0,
|
|
121
|
+
nextSpanSeq: 0,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Add optional metadata
|
|
125
|
+
if (opts.metadata !== undefined) {
|
|
126
|
+
run.metadata = { ...opts.metadata };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Add description to metadata if provided
|
|
130
|
+
if (opts.description !== undefined) {
|
|
131
|
+
run.metadata = {
|
|
132
|
+
...run.metadata,
|
|
133
|
+
description: opts.description,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// -------------------------------------------------------------------------
|
|
138
|
+
// Pre-execution model-manifest pinning (issue #59)
|
|
139
|
+
// -------------------------------------------------------------------------
|
|
140
|
+
const strict = opts.strict ?? false;
|
|
141
|
+
if (strict) {
|
|
142
|
+
run.strict = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (opts.manifest !== undefined) {
|
|
146
|
+
const manifest = validateModelManifest(opts.manifest);
|
|
147
|
+
// Pin the hash now, BEFORE any event is recorded — this is the enforcement
|
|
148
|
+
// point for "the model was not altered over the run".
|
|
149
|
+
run.modelManifestHash = await computeModelManifestHash(manifest);
|
|
150
|
+
// Freeze the object so any later mutation throws (ESM strict mode). The pin
|
|
151
|
+
// is the hash computed above, not the live object.
|
|
152
|
+
run.modelManifest = freezeModelManifest(manifest);
|
|
153
|
+
} else if (strict) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
"createTrace: strict mode requires a `manifest` to be pinned before execution"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return run;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// =============================================================================
|
|
163
|
+
// SPAN MANAGEMENT
|
|
164
|
+
// =============================================================================
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Add a new span to a trace run.
|
|
168
|
+
*
|
|
169
|
+
* Creates a span with:
|
|
170
|
+
* - Unique UUID for span ID
|
|
171
|
+
* - Assigned spanSeq from run.nextSpanSeq
|
|
172
|
+
* - Status "running"
|
|
173
|
+
* - Empty eventIds and childSpanIds arrays
|
|
174
|
+
*
|
|
175
|
+
* If a parentSpanId is provided, the span is added to the parent's childSpanIds.
|
|
176
|
+
*
|
|
177
|
+
* @param run - The trace run to add the span to (mutated in place)
|
|
178
|
+
* @param opts - Options for creating the span
|
|
179
|
+
* @param opts.name - Human-readable name for the span
|
|
180
|
+
* @param opts.parentSpanId - Optional parent span ID for nesting
|
|
181
|
+
* @param opts.visibility - Span visibility level (defaults to "private")
|
|
182
|
+
* @param opts.metadata - Optional key-value metadata
|
|
183
|
+
* @returns The created TraceSpan
|
|
184
|
+
* @throws Error if the run is finalized or parent span is not found
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```typescript
|
|
188
|
+
* // Create a top-level span
|
|
189
|
+
* const buildSpan = addSpan(run, { name: "build" });
|
|
190
|
+
*
|
|
191
|
+
* // Create a nested span
|
|
192
|
+
* const installSpan = addSpan(run, {
|
|
193
|
+
* name: "npm-install",
|
|
194
|
+
* parentSpanId: buildSpan.id,
|
|
195
|
+
* visibility: "public",
|
|
196
|
+
* });
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
export function addSpan(run: TraceRun, opts: CreateSpanOptions): TraceSpan {
|
|
200
|
+
// Validate run is not finalized
|
|
201
|
+
if (isFinalized(run)) {
|
|
202
|
+
throw new Error("Cannot add span to a finalized trace run");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Validate required fields
|
|
206
|
+
if (!opts.name || typeof opts.name !== "string") {
|
|
207
|
+
throw new Error("name is required and must be a non-empty string");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Validate parent span exists if specified
|
|
211
|
+
if (opts.parentSpanId !== undefined) {
|
|
212
|
+
const parentSpan = getSpan(run, opts.parentSpanId);
|
|
213
|
+
if (!parentSpan) {
|
|
214
|
+
throw new Error(`Parent span not found: ${opts.parentSpanId}`);
|
|
215
|
+
}
|
|
216
|
+
if (parentSpan.status !== "running") {
|
|
217
|
+
throw new Error(`Parent span is not running: ${opts.parentSpanId}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Generate unique span ID
|
|
222
|
+
const spanId = crypto.randomUUID();
|
|
223
|
+
|
|
224
|
+
// Assign spanSeq and increment counter
|
|
225
|
+
const spanSeq = run.nextSpanSeq++;
|
|
226
|
+
|
|
227
|
+
// Determine visibility (default to "private" if not specified)
|
|
228
|
+
const visibility: Visibility = opts.visibility ?? "private";
|
|
229
|
+
|
|
230
|
+
// Create the span
|
|
231
|
+
const span: TraceSpan = {
|
|
232
|
+
id: spanId,
|
|
233
|
+
spanSeq,
|
|
234
|
+
name: opts.name,
|
|
235
|
+
status: "running",
|
|
236
|
+
visibility,
|
|
237
|
+
startedAt: new Date().toISOString(),
|
|
238
|
+
eventIds: [],
|
|
239
|
+
childSpanIds: [],
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Add optional fields
|
|
243
|
+
if (opts.parentSpanId !== undefined) {
|
|
244
|
+
span.parentSpanId = opts.parentSpanId;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (opts.metadata !== undefined) {
|
|
248
|
+
span.metadata = { ...opts.metadata };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Add to run's spans array
|
|
252
|
+
run.spans.push(span);
|
|
253
|
+
|
|
254
|
+
// If there's a parent span, add this span to its childSpanIds
|
|
255
|
+
if (opts.parentSpanId !== undefined) {
|
|
256
|
+
const parentSpan = getSpan(run, opts.parentSpanId);
|
|
257
|
+
if (parentSpan) {
|
|
258
|
+
parentSpan.childSpanIds.push(spanId);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return span;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get a span by ID from a run.
|
|
267
|
+
*
|
|
268
|
+
* @param run - The trace run to search
|
|
269
|
+
* @param spanId - The span ID to find
|
|
270
|
+
* @returns The span if found, undefined otherwise
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* ```typescript
|
|
274
|
+
* const span = getSpan(run, "some-span-id");
|
|
275
|
+
* if (span) {
|
|
276
|
+
* console.log(`Found span: ${span.name}`);
|
|
277
|
+
* }
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
export function getSpan(run: TraceRun, spanId: string): TraceSpan | undefined {
|
|
281
|
+
return run.spans.find((s) => s.id === spanId);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get events for a span.
|
|
286
|
+
*
|
|
287
|
+
* Returns all events belonging to the specified span, sorted by sequence number.
|
|
288
|
+
*
|
|
289
|
+
* @param run - The trace run containing the events
|
|
290
|
+
* @param spanId - The span ID to get events for
|
|
291
|
+
* @returns Array of TraceEvents for the span, sorted by seq
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```typescript
|
|
295
|
+
* const events = getSpanEvents(run, span.id);
|
|
296
|
+
* for (const event of events) {
|
|
297
|
+
* console.log(`Event ${event.seq}: ${event.kind}`);
|
|
298
|
+
* }
|
|
299
|
+
* ```
|
|
300
|
+
*/
|
|
301
|
+
export function getSpanEvents(run: TraceRun, spanId: string): TraceEvent[] {
|
|
302
|
+
const span = getSpan(run, spanId);
|
|
303
|
+
if (!span) {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Get events by their IDs and sort by seq
|
|
308
|
+
const eventMap = new Map(run.events.map((e) => [e.id, e]));
|
|
309
|
+
const spanEvents = span.eventIds
|
|
310
|
+
.map((id) => eventMap.get(id))
|
|
311
|
+
.filter((e): e is TraceEvent => e !== undefined);
|
|
312
|
+
|
|
313
|
+
return spanEvents.sort((a, b) => a.seq - b.seq);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// =============================================================================
|
|
317
|
+
// EVENT MANAGEMENT
|
|
318
|
+
// =============================================================================
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Type helper to extract the event type by kind.
|
|
322
|
+
* Used for type-safe event creation without runtime fields.
|
|
323
|
+
*/
|
|
324
|
+
type EventWithoutRuntimeFields<K extends TraceEventKind> = Omit<
|
|
325
|
+
Extract<TraceEvent, { kind: K }>,
|
|
326
|
+
"id" | "seq" | "timestamp" | "hash"
|
|
327
|
+
>;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Add an event to a span within a trace run.
|
|
331
|
+
*
|
|
332
|
+
* Automatically assigns:
|
|
333
|
+
* - Unique UUID for event ID
|
|
334
|
+
* - Monotonic sequence number from run.nextSeq
|
|
335
|
+
* - ISO 8601 timestamp
|
|
336
|
+
* - Default visibility based on event kind (if not specified)
|
|
337
|
+
* - Computed event hash
|
|
338
|
+
*
|
|
339
|
+
* Also updates the run's rolling hash to maintain cryptographic chain.
|
|
340
|
+
*
|
|
341
|
+
* @param run - The trace run (mutated in place)
|
|
342
|
+
* @param spanId - ID of the span to add event to
|
|
343
|
+
* @param event - Event data without runtime fields (id, seq, timestamp, hash)
|
|
344
|
+
* @returns Promise resolving to the complete TraceEvent
|
|
345
|
+
* @throws Error if run is finalized, span not found, or span is closed
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```typescript
|
|
349
|
+
* // Add a command event
|
|
350
|
+
* const cmdEvent = await addEvent(run, span.id, {
|
|
351
|
+
* kind: "command",
|
|
352
|
+
* command: "npm install",
|
|
353
|
+
* args: ["--save-dev", "typescript"],
|
|
354
|
+
* visibility: "public",
|
|
355
|
+
* });
|
|
356
|
+
*
|
|
357
|
+
* // Add an output event (will use default "private" visibility)
|
|
358
|
+
* const outEvent = await addEvent(run, span.id, {
|
|
359
|
+
* kind: "output",
|
|
360
|
+
* stream: "stdout",
|
|
361
|
+
* content: "added 120 packages",
|
|
362
|
+
* });
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
365
|
+
export async function addEvent<K extends TraceEventKind>(
|
|
366
|
+
run: TraceRun,
|
|
367
|
+
spanId: string,
|
|
368
|
+
event: EventWithoutRuntimeFields<K>
|
|
369
|
+
): Promise<TraceEvent> {
|
|
370
|
+
// Validate run is not finalized
|
|
371
|
+
if (isFinalized(run)) {
|
|
372
|
+
throw new Error("Cannot add event to a finalized trace run");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Find the span
|
|
376
|
+
const span = getSpan(run, spanId);
|
|
377
|
+
if (!span) {
|
|
378
|
+
throw new Error(`Span not found: ${spanId}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Validate span is still running
|
|
382
|
+
if (span.status !== "running") {
|
|
383
|
+
throw new Error(`Cannot add event to closed span: ${spanId} (status: ${span.status})`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Validate event has a kind
|
|
387
|
+
if (!event.kind || typeof event.kind !== "string") {
|
|
388
|
+
throw new Error("Event kind is required and must be a non-empty string");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Generate event ID
|
|
392
|
+
const eventId = crypto.randomUUID();
|
|
393
|
+
|
|
394
|
+
// Assign sequence number and increment counter
|
|
395
|
+
const seq = run.nextSeq++;
|
|
396
|
+
|
|
397
|
+
// Get current timestamp
|
|
398
|
+
const timestamp = new Date().toISOString();
|
|
399
|
+
|
|
400
|
+
// Determine visibility: use provided value or default for the event kind
|
|
401
|
+
const visibility: Visibility =
|
|
402
|
+
event.visibility ?? DEFAULT_EVENT_VISIBILITY[event.kind as TraceEventKind] ?? "private";
|
|
403
|
+
|
|
404
|
+
// Build the complete event (without hash initially)
|
|
405
|
+
// We use 'as unknown as TraceEvent' because TypeScript cannot infer
|
|
406
|
+
// that adding runtime fields to EventWithoutRuntimeFields<K> produces a valid TraceEvent.
|
|
407
|
+
// The caller ensures the correct event shape via the generic constraint.
|
|
408
|
+
const completeEvent = {
|
|
409
|
+
...event,
|
|
410
|
+
id: eventId,
|
|
411
|
+
seq,
|
|
412
|
+
timestamp,
|
|
413
|
+
visibility,
|
|
414
|
+
} as unknown as TraceEvent;
|
|
415
|
+
|
|
416
|
+
// Compute event hash
|
|
417
|
+
const eventHash = await computeEventHash(completeEvent);
|
|
418
|
+
completeEvent.hash = eventHash;
|
|
419
|
+
|
|
420
|
+
// Update rolling hash
|
|
421
|
+
const currentState = {
|
|
422
|
+
currentHash: run.rollingHash,
|
|
423
|
+
itemCount: run.events.length,
|
|
424
|
+
};
|
|
425
|
+
const newState = await updateRollingHash(currentState, eventHash);
|
|
426
|
+
run.rollingHash = newState.currentHash;
|
|
427
|
+
|
|
428
|
+
// Add event ID to span's eventIds
|
|
429
|
+
span.eventIds.push(eventId);
|
|
430
|
+
|
|
431
|
+
// Add event to run's events array
|
|
432
|
+
run.events.push(completeEvent);
|
|
433
|
+
|
|
434
|
+
return completeEvent;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// =============================================================================
|
|
438
|
+
// SPAN CLOSING
|
|
439
|
+
// =============================================================================
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Close a span, marking it as completed/failed/cancelled.
|
|
443
|
+
*
|
|
444
|
+
* Sets the span's:
|
|
445
|
+
* - status (default "completed")
|
|
446
|
+
* - endedAt timestamp
|
|
447
|
+
* - durationMs (calculated from startedAt to endedAt)
|
|
448
|
+
* - hash (computed from span header + event hashes)
|
|
449
|
+
*
|
|
450
|
+
* @param run - The trace run containing the span (mutated in place)
|
|
451
|
+
* @param spanId - ID of the span to close
|
|
452
|
+
* @param status - Final status (default "completed")
|
|
453
|
+
* @throws Error if span not found or already closed
|
|
454
|
+
*
|
|
455
|
+
* @example
|
|
456
|
+
* ```typescript
|
|
457
|
+
* // Close with default "completed" status
|
|
458
|
+
* await closeSpan(run, span.id);
|
|
459
|
+
*
|
|
460
|
+
* // Close with explicit status
|
|
461
|
+
* await closeSpan(run, span.id, "failed");
|
|
462
|
+
* ```
|
|
463
|
+
*/
|
|
464
|
+
export async function closeSpan(
|
|
465
|
+
run: TraceRun,
|
|
466
|
+
spanId: string,
|
|
467
|
+
status: TraceStatus = "completed"
|
|
468
|
+
): Promise<void> {
|
|
469
|
+
// Find the span
|
|
470
|
+
const span = getSpan(run, spanId);
|
|
471
|
+
if (!span) {
|
|
472
|
+
throw new Error(`Span not found: ${spanId}`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Validate span is still running
|
|
476
|
+
if (span.status !== "running") {
|
|
477
|
+
throw new Error(`Span already closed: ${spanId} (status: ${span.status})`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Set final status
|
|
481
|
+
span.status = status;
|
|
482
|
+
|
|
483
|
+
// Set end timestamp
|
|
484
|
+
const endedAt = new Date().toISOString();
|
|
485
|
+
span.endedAt = endedAt;
|
|
486
|
+
|
|
487
|
+
// Calculate duration
|
|
488
|
+
const startTime = new Date(span.startedAt).getTime();
|
|
489
|
+
const endTime = new Date(endedAt).getTime();
|
|
490
|
+
span.durationMs = endTime - startTime;
|
|
491
|
+
|
|
492
|
+
// Get event hashes for this span in seq order
|
|
493
|
+
const spanEvents = getSpanEvents(run, spanId);
|
|
494
|
+
const eventHashes = spanEvents.map((e) => e.hash ?? "");
|
|
495
|
+
|
|
496
|
+
// Compute span hash
|
|
497
|
+
span.hash = await computeSpanHash(span, eventHashes);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// =============================================================================
|
|
501
|
+
// TRACE FINALIZATION
|
|
502
|
+
// =============================================================================
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Check if a run is finalized.
|
|
506
|
+
*
|
|
507
|
+
* A run is considered finalized when it has a rootHash set.
|
|
508
|
+
*
|
|
509
|
+
* @param run - The trace run to check
|
|
510
|
+
* @returns true if the run is finalized
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* ```typescript
|
|
514
|
+
* if (!isFinalized(run)) {
|
|
515
|
+
* // Can still add spans and events
|
|
516
|
+
* await addEvent(run, span.id, { kind: "command", command: "ls" });
|
|
517
|
+
* }
|
|
518
|
+
* ```
|
|
519
|
+
*/
|
|
520
|
+
export function isFinalized(run: TraceRun): boolean {
|
|
521
|
+
return run.rootHash !== undefined;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Finalize a trace run, computing all final hashes and creating a bundle.
|
|
526
|
+
*
|
|
527
|
+
* Finalization performs:
|
|
528
|
+
* 1. Closes any open spans (with status "completed")
|
|
529
|
+
* 2. Sets run status to "completed"
|
|
530
|
+
* 3. Sets run endedAt and durationMs
|
|
531
|
+
* 4. Builds Merkle tree from spans
|
|
532
|
+
* 5. Computes root hash from rolling hash + span hashes
|
|
533
|
+
* 6. Creates public view (only public spans with their events)
|
|
534
|
+
* 7. Returns complete TraceBundle
|
|
535
|
+
*
|
|
536
|
+
* After finalization, no more spans or events can be added.
|
|
537
|
+
*
|
|
538
|
+
* @param run - The trace run to finalize (mutated in place)
|
|
539
|
+
* @returns Promise resolving to the complete TraceBundle
|
|
540
|
+
* @throws Error if the run is already finalized
|
|
541
|
+
*
|
|
542
|
+
* @example
|
|
543
|
+
* ```typescript
|
|
544
|
+
* // Finalize and get the bundle
|
|
545
|
+
* const bundle = await finalizeTrace(run);
|
|
546
|
+
*
|
|
547
|
+
* // Access the cryptographic commitments
|
|
548
|
+
* console.log(`Root hash: ${bundle.rootHash}`);
|
|
549
|
+
* console.log(`Merkle root: ${bundle.merkleRoot}`);
|
|
550
|
+
*
|
|
551
|
+
* // Access the public view for sharing
|
|
552
|
+
* console.log(`Public spans: ${bundle.publicView.publicSpans.length}`);
|
|
553
|
+
* ```
|
|
554
|
+
*/
|
|
555
|
+
export async function finalizeTrace(run: TraceRun): Promise<TraceBundle> {
|
|
556
|
+
// Validate run is not already finalized
|
|
557
|
+
if (isFinalized(run)) {
|
|
558
|
+
throw new Error("Trace run is already finalized");
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// -------------------------------------------------------------------------
|
|
562
|
+
// Pre-execution manifest enforcement (issue #59)
|
|
563
|
+
// -------------------------------------------------------------------------
|
|
564
|
+
if (run.modelManifest === undefined) {
|
|
565
|
+
if (run.strict) {
|
|
566
|
+
throw new Error(
|
|
567
|
+
"finalizeTrace: strict mode requires a model manifest pinned at createTrace() time"
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
// Warn-only path for v0.x — surfaces the missing model-immutability guarantee.
|
|
571
|
+
// Becomes a hard error under strict-by-default in v1.0.
|
|
572
|
+
console.warn(
|
|
573
|
+
"[orynq] finalizeTrace: no model manifest was pinned — model/data immutability " +
|
|
574
|
+
"is NOT proven for this trace. Pass `manifest` to createTrace() (and " +
|
|
575
|
+
"`strict: true` to enforce). This will become an error in v1.0."
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Close any open spans
|
|
580
|
+
for (const span of run.spans) {
|
|
581
|
+
if (span.status === "running") {
|
|
582
|
+
await closeSpan(run, span.id, "completed");
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Set run status to completed
|
|
587
|
+
run.status = "completed";
|
|
588
|
+
|
|
589
|
+
// Set end timestamp and duration
|
|
590
|
+
const endedAt = new Date().toISOString();
|
|
591
|
+
run.endedAt = endedAt;
|
|
592
|
+
const startTime = new Date(run.startedAt).getTime();
|
|
593
|
+
const endTime = new Date(endedAt).getTime();
|
|
594
|
+
run.durationMs = endTime - startTime;
|
|
595
|
+
|
|
596
|
+
// Build Merkle tree from spans
|
|
597
|
+
const merkleTree = await buildSpanMerkleTree(run.spans, run.events);
|
|
598
|
+
|
|
599
|
+
// Compute root hash from rolling hash + span hashes, binding the pinned
|
|
600
|
+
// model-manifest commitment into the committed root (#59).
|
|
601
|
+
const rootHash = await computeRootHash(
|
|
602
|
+
run.rollingHash,
|
|
603
|
+
run.spans,
|
|
604
|
+
run.modelManifestHash
|
|
605
|
+
);
|
|
606
|
+
run.rootHash = rootHash;
|
|
607
|
+
|
|
608
|
+
// Create public view
|
|
609
|
+
const publicView = createPublicView(run, merkleTree.rootHash);
|
|
610
|
+
|
|
611
|
+
// Build and return the complete bundle
|
|
612
|
+
const bundle: TraceBundle = {
|
|
613
|
+
formatVersion: "1.0",
|
|
614
|
+
publicView,
|
|
615
|
+
privateRun: run,
|
|
616
|
+
merkleRoot: merkleTree.rootHash,
|
|
617
|
+
rootHash,
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
// Surface the pinned model manifest on the bundle (public-safe: hashes only).
|
|
621
|
+
if (run.modelManifestHash !== undefined) {
|
|
622
|
+
bundle.modelManifestHash = run.modelManifestHash;
|
|
623
|
+
}
|
|
624
|
+
if (run.modelManifest !== undefined) {
|
|
625
|
+
bundle.modelManifest = run.modelManifest;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return bundle;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// =============================================================================
|
|
632
|
+
// PUBLIC VIEW GENERATION
|
|
633
|
+
// =============================================================================
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Create a public view of the trace suitable for external sharing.
|
|
637
|
+
*
|
|
638
|
+
* The public view includes:
|
|
639
|
+
* - Run metadata (id, agentId, timestamps, etc.)
|
|
640
|
+
* - Cryptographic commitments (rootHash, merkleRoot)
|
|
641
|
+
* - Public spans with their events
|
|
642
|
+
* - Hashes of redacted (non-public) spans
|
|
643
|
+
*
|
|
644
|
+
* Private and secret data is excluded, but their hashes are included
|
|
645
|
+
* for verification purposes.
|
|
646
|
+
*
|
|
647
|
+
* @param run - The finalized trace run
|
|
648
|
+
* @param merkleRoot - The Merkle root from the span tree
|
|
649
|
+
* @returns The public view of the trace bundle
|
|
650
|
+
*/
|
|
651
|
+
function createPublicView(
|
|
652
|
+
run: TraceRun,
|
|
653
|
+
merkleRoot: string
|
|
654
|
+
): TraceBundlePublicView {
|
|
655
|
+
// Build event lookup map
|
|
656
|
+
const eventMap = new Map(run.events.map((e) => [e.id, e]));
|
|
657
|
+
|
|
658
|
+
// Separate public spans from non-public
|
|
659
|
+
const publicSpans: AnnotatedSpan[] = [];
|
|
660
|
+
const redactedSpanHashes: Array<{ spanId: string; hash: string }> = [];
|
|
661
|
+
|
|
662
|
+
for (const span of run.spans) {
|
|
663
|
+
if (span.visibility === "public") {
|
|
664
|
+
// Include public spans with their events
|
|
665
|
+
const spanEvents = span.eventIds
|
|
666
|
+
.map((id) => eventMap.get(id))
|
|
667
|
+
.filter((e): e is TraceEvent => e !== undefined)
|
|
668
|
+
// Only include public events within public spans
|
|
669
|
+
.filter((e) => e.visibility === "public")
|
|
670
|
+
.sort((a, b) => a.seq - b.seq);
|
|
671
|
+
|
|
672
|
+
const annotatedSpan: AnnotatedSpan = {
|
|
673
|
+
...span,
|
|
674
|
+
events: spanEvents,
|
|
675
|
+
};
|
|
676
|
+
publicSpans.push(annotatedSpan);
|
|
677
|
+
} else {
|
|
678
|
+
// Include only the hash for non-public spans
|
|
679
|
+
if (span.hash) {
|
|
680
|
+
redactedSpanHashes.push({
|
|
681
|
+
spanId: span.id,
|
|
682
|
+
hash: span.hash,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Sort public spans by spanSeq
|
|
689
|
+
publicSpans.sort((a, b) => a.spanSeq - b.spanSeq);
|
|
690
|
+
|
|
691
|
+
// Sort redacted span hashes by spanId for consistency
|
|
692
|
+
redactedSpanHashes.sort((a, b) => a.spanId.localeCompare(b.spanId));
|
|
693
|
+
|
|
694
|
+
const publicView: TraceBundlePublicView = {
|
|
695
|
+
runId: run.id,
|
|
696
|
+
agentId: run.agentId,
|
|
697
|
+
schemaVersion: run.schemaVersion,
|
|
698
|
+
startedAt: run.startedAt,
|
|
699
|
+
endedAt: run.endedAt ?? run.startedAt, // Fallback for safety
|
|
700
|
+
durationMs: run.durationMs ?? 0,
|
|
701
|
+
status: run.status,
|
|
702
|
+
totalEvents: run.events.length,
|
|
703
|
+
totalSpans: run.spans.length,
|
|
704
|
+
rootHash: run.rootHash ?? "",
|
|
705
|
+
merkleRoot,
|
|
706
|
+
publicSpans,
|
|
707
|
+
redactedSpanHashes,
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
// Model-state commitment is public-safe (it is only a hash).
|
|
711
|
+
if (run.modelManifestHash !== undefined) {
|
|
712
|
+
publicView.modelManifestHash = run.modelManifestHash;
|
|
713
|
+
}
|
|
714
|
+
if (run.modelManifest !== undefined) {
|
|
715
|
+
publicView.modelManifest = run.modelManifest;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return publicView;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// =============================================================================
|
|
722
|
+
// UTILITY FUNCTIONS
|
|
723
|
+
// =============================================================================
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Get total event count for a trace run.
|
|
727
|
+
*
|
|
728
|
+
* @param run - The trace run
|
|
729
|
+
* @returns Number of events in the run
|
|
730
|
+
*/
|
|
731
|
+
export function getEventCount(run: TraceRun): number {
|
|
732
|
+
return run.events.length;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Get total span count for a trace run.
|
|
737
|
+
*
|
|
738
|
+
* @param run - The trace run
|
|
739
|
+
* @returns Number of spans in the run
|
|
740
|
+
*/
|
|
741
|
+
export function getSpanCount(run: TraceRun): number {
|
|
742
|
+
return run.spans.length;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Get all root spans (spans without a parent).
|
|
747
|
+
*
|
|
748
|
+
* @param run - The trace run
|
|
749
|
+
* @returns Array of root-level spans
|
|
750
|
+
*/
|
|
751
|
+
export function getRootSpans(run: TraceRun): TraceSpan[] {
|
|
752
|
+
return run.spans.filter((s) => s.parentSpanId === undefined);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Get child spans for a given parent span.
|
|
757
|
+
*
|
|
758
|
+
* @param run - The trace run
|
|
759
|
+
* @param parentSpanId - The parent span ID
|
|
760
|
+
* @returns Array of child spans
|
|
761
|
+
*/
|
|
762
|
+
export function getChildSpans(run: TraceRun, parentSpanId: string): TraceSpan[] {
|
|
763
|
+
return run.spans.filter((s) => s.parentSpanId === parentSpanId);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Get an event by ID from a run.
|
|
768
|
+
*
|
|
769
|
+
* @param run - The trace run
|
|
770
|
+
* @param eventId - The event ID to find
|
|
771
|
+
* @returns The event if found, undefined otherwise
|
|
772
|
+
*/
|
|
773
|
+
export function getEvent(run: TraceRun, eventId: string): TraceEvent | undefined {
|
|
774
|
+
return run.events.find((e) => e.id === eventId);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Get all events of a specific kind from a run.
|
|
779
|
+
*
|
|
780
|
+
* @param run - The trace run
|
|
781
|
+
* @param kind - The event kind to filter by
|
|
782
|
+
* @returns Array of events matching the kind
|
|
783
|
+
*/
|
|
784
|
+
export function getEventsByKind<K extends TraceEventKind>(
|
|
785
|
+
run: TraceRun,
|
|
786
|
+
kind: K
|
|
787
|
+
): Extract<TraceEvent, { kind: K }>[] {
|
|
788
|
+
return run.events.filter(
|
|
789
|
+
(e): e is Extract<TraceEvent, { kind: K }> => e.kind === kind
|
|
790
|
+
);
|
|
791
|
+
}
|