@tangle-network/traces 0.19.1 → 0.21.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/dist/index.js CHANGED
@@ -18,14 +18,15 @@ import {
18
18
  DEFAULT_ASK_CONCURRENCY,
19
19
  DEFAULT_HARNESS,
20
20
  DEFAULT_SANDBOX_BASE_URL,
21
+ DEFAULT_SWEEP_THRESHOLD,
21
22
  EMPTY_SPAN_RUN_SNAPSHOT,
22
23
  EXPORT_RULE,
23
24
  EmptySessionError,
24
25
  FACT_LIST_CAP,
25
26
  FACT_TEXT_CAP,
26
- FactoryAdapter,
27
27
  ForgeAdapter,
28
28
  GeminiAdapter,
29
+ HarnessSessionsAdapter,
29
30
  INGEST_SOURCE_CLI,
30
31
  INHERITED_SOURCE_ATTR,
31
32
  INHERITED_SPANS_OMITTED_ATTR,
@@ -39,7 +40,6 @@ import {
39
40
  POLICY_EVIDENCE_NOTE,
40
41
  PREPARED_CONTEXT_BYTE_CEILING,
41
42
  PartialUploadError,
42
- PiAdapter,
43
43
  QwenAdapter,
44
44
  REACTIONS,
45
45
  RESULT_FILE,
@@ -54,6 +54,7 @@ import {
54
54
  SYNTHESIZED_SOURCE_ATTR,
55
55
  SYNTHESIZED_SPAN_ATTR,
56
56
  SessionWorkflowError,
57
+ SkillUsageStore,
57
58
  TRACE_QUESTION_PREVIEW_HEAD_CHARS,
58
59
  ZERO_ROLL,
59
60
  addSpend,
@@ -75,6 +76,7 @@ import {
75
76
  buildSessionFactsReport,
76
77
  buildSessionIndex,
77
78
  buildSessionIndexFromRows,
79
+ buildSkillUsageReport,
78
80
  buildSpanRunTree,
79
81
  buildTraceFindingPacket,
80
82
  capText,
@@ -101,6 +103,9 @@ import {
101
103
  createFindingRejectionTally,
102
104
  cwdMatchesSelection,
103
105
  cwdMatchesSelectionResolved,
106
+ defaultSkillCatalogRoots,
107
+ defaultSkillUsageSources,
108
+ defaultSkillUsageStorePath,
104
109
  defaultTraceLiveAnalysts,
105
110
  describeSessionRelationship,
106
111
  diffRuns,
@@ -139,6 +144,8 @@ import {
139
144
  isChatTrajectoryMessage,
140
145
  isFileRunContextDir,
141
146
  isInheritedSpan,
147
+ isMissingJsonSource,
148
+ isMissingPathError,
142
149
  isSynthesizedSpan,
143
150
  iterSerializedSpans,
144
151
  knownHarnesses,
@@ -167,6 +174,7 @@ import {
167
174
  projectSessionBundle,
168
175
  readCoordinationLog,
169
176
  readFileRunContext,
177
+ readInstalledSkills,
170
178
  readJsonFile,
171
179
  readJsonl,
172
180
  readOtlpInput,
@@ -179,6 +187,7 @@ import {
179
187
  readSessionIndexFile,
180
188
  readSpanRunSnapshot,
181
189
  recordSessionCorruption,
190
+ recordToolOutput,
182
191
  redactSpans,
183
192
  renderConformance,
184
193
  renderInspectionReport,
@@ -187,6 +196,7 @@ import {
187
196
  renderRunWatchSnapshot,
188
197
  renderSessionFacts,
189
198
  renderSessionFactsContext,
199
+ renderSkillUsage,
190
200
  renderSpanRunSnapshot,
191
201
  renderTraceQuestionInstructions,
192
202
  renderTraceQuestionPrompt,
@@ -220,9 +230,11 @@ import {
220
230
  sessionHash,
221
231
  sessionIdFromAttributes,
222
232
  sessionIntegrityAttributes,
233
+ sessionJsonlOptions,
223
234
  sessionTextSignatures,
224
235
  shellCommands,
225
236
  skippedAnalyses,
237
+ sourceOf,
226
238
  span,
227
239
  spanRunTotals,
228
240
  stampEnvironmentAttrs,
@@ -236,10 +248,12 @@ import {
236
248
  summarizeSpanStructure,
237
249
  takeJsonl,
238
250
  textIsSynthetic,
251
+ textSources,
239
252
  toOpenInferenceSpan,
240
253
  toRuntimeStore,
241
254
  toTraceSpanEvents,
242
255
  tokens,
256
+ toolIoAttributes,
243
257
  totalAgenticFailureMessage,
244
258
  totalFindingRejections,
245
259
  traceAgenticKinds,
@@ -270,7 +284,517 @@ import {
270
284
  writeTraceQuestionsArtifacts,
271
285
  zaiChatCaller,
272
286
  zeroPricedCost
273
- } from "./chunk-YVCHWHCA.js";
287
+ } from "./chunk-KEB25UHZ.js";
288
+
289
+ // src/adapters/factory.ts
290
+ import { readdir, stat } from "fs/promises";
291
+ import { homedir } from "os";
292
+ import { basename, join } from "path";
293
+ var SERVICE = "factory";
294
+ function textOf(content) {
295
+ return capText(
296
+ (content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("")
297
+ );
298
+ }
299
+ var FactoryAdapter = class {
300
+ harness = "factory";
301
+ aliases = ["factory-droids", "droid"];
302
+ root() {
303
+ return join(homedir(), ".factory", "sessions");
304
+ }
305
+ async locate(opts = {}) {
306
+ const root = this.root();
307
+ let dirs;
308
+ try {
309
+ dirs = await readdir(root);
310
+ } catch (error) {
311
+ if (isMissingPathError(error)) return [];
312
+ throw error;
313
+ }
314
+ const refs = [];
315
+ for (const dir of dirs) {
316
+ const dp = join(root, dir);
317
+ let files;
318
+ try {
319
+ files = await readdir(dp);
320
+ } catch (error) {
321
+ if (isMissingPathError(error)) continue;
322
+ throw error;
323
+ }
324
+ const cwd = dir.replace(/-/g, "/");
325
+ if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
326
+ for (const f of files) {
327
+ if (!f.endsWith(".jsonl")) continue;
328
+ const path = join(dp, f);
329
+ let st;
330
+ try {
331
+ st = await stat(path);
332
+ } catch (error) {
333
+ if (isMissingPathError(error)) continue;
334
+ throw error;
335
+ }
336
+ if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
337
+ refs.push({ harness: this.harness, sessionId: basename(f, ".jsonl"), path, cwd, mtimeMs: st.mtimeMs });
338
+ }
339
+ }
340
+ return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
341
+ }
342
+ async sourcePaths(ref) {
343
+ const sidecar = ref.path.replace(/\.jsonl$/, ".settings.json");
344
+ try {
345
+ await stat(sidecar);
346
+ return [ref.path, sidecar];
347
+ } catch (error) {
348
+ if (isMissingPathError(error)) return [ref.path];
349
+ throw error;
350
+ }
351
+ }
352
+ async parse(ref, options = {}) {
353
+ let settings = {};
354
+ try {
355
+ settings = await readJsonFile(ref.path.replace(/\.jsonl$/, ".settings.json"));
356
+ } catch (error) {
357
+ if (!isMissingJsonSource(error)) throw error;
358
+ }
359
+ const sourceTraceId = ref.sessionId;
360
+ const sourceRootId = `root:${sourceTraceId}`;
361
+ const spans = [];
362
+ const toolByUseId = /* @__PURE__ */ new Map();
363
+ let firstTimestamp;
364
+ let lastTimestamp;
365
+ let sessionLine;
366
+ let sawLine = false;
367
+ let step = 0;
368
+ let lastLlm = sourceRootId;
369
+ for await (const l of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
370
+ if (!sawLine) {
371
+ firstTimestamp = l.timestamp;
372
+ sawLine = true;
373
+ }
374
+ lastTimestamp = l.timestamp;
375
+ if (!sessionLine && l.type === "session_start") {
376
+ sessionLine = { id: l.id, timestamp: l.timestamp };
377
+ }
378
+ if (l.type !== "message" || !l.message) continue;
379
+ const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
380
+ const role = l.message.role;
381
+ const text = textOf(l.message.content);
382
+ if (role === "user") {
383
+ if (text) {
384
+ spans.push(
385
+ userPromptSpan({
386
+ traceId: sourceTraceId,
387
+ spanId: `user:${l.id ?? step}`,
388
+ parentSpanId: sourceRootId,
389
+ startTime: ts,
390
+ content: text,
391
+ contentSource: textSources(l.message, "content"),
392
+ service: SERVICE,
393
+ agent: SERVICE,
394
+ step
395
+ })
396
+ );
397
+ step += 1;
398
+ }
399
+ } else {
400
+ const llmId = `llm:${l.id ?? step}`;
401
+ spans.push(
402
+ span({
403
+ traceId: sourceTraceId,
404
+ spanId: llmId,
405
+ parentSpanId: sourceRootId,
406
+ name: `message.${role ?? "unknown"}`,
407
+ kind: "LLM",
408
+ startTime: ts,
409
+ service: SERVICE,
410
+ agent: SERVICE,
411
+ model: settings.model ?? null,
412
+ step,
413
+ content: text || null,
414
+ contentSource: textSources(l.message, "content")
415
+ })
416
+ );
417
+ lastLlm = llmId;
418
+ step += 1;
419
+ }
420
+ for (const b of l.message.content ?? []) {
421
+ if (b.type === "tool_use" && b.name) {
422
+ const t = span({
423
+ traceId: sourceTraceId,
424
+ spanId: `tool:${b.id ?? `${l.id}:${step}`}`,
425
+ parentSpanId: lastLlm,
426
+ name: `tool.${b.name}`,
427
+ kind: "TOOL",
428
+ startTime: ts,
429
+ service: SERVICE,
430
+ agent: SERVICE,
431
+ tool: b.name,
432
+ step,
433
+ extra: toolIoAttributes({ input: b.input, inputSource: sourceOf(b, "input") })
434
+ });
435
+ spans.push(t);
436
+ if (b.id) toolByUseId.set(b.id, t);
437
+ step += 1;
438
+ } else if (b.type === "tool_result" && b.tool_use_id) {
439
+ const t = toolByUseId.get(b.tool_use_id);
440
+ if (t) {
441
+ t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
442
+ recordToolOutput(t, b.content, sourceOf(b, "content"));
443
+ }
444
+ }
445
+ }
446
+ }
447
+ const traceId = sessionLine?.id ?? sourceTraceId;
448
+ const rootId = `root:${traceId}`;
449
+ for (const item of spans) {
450
+ item.trace_id = traceId;
451
+ if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
452
+ }
453
+ const root = span({
454
+ traceId,
455
+ spanId: rootId,
456
+ parentSpanId: null,
457
+ name: "session",
458
+ kind: "AGENT",
459
+ startTime: sessionLine?.timestamp ?? firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
460
+ endTime: lastTimestamp,
461
+ service: SERVICE,
462
+ agent: SERVICE,
463
+ model: settings.model ?? null,
464
+ extra: settings.tokenUsage ? {
465
+ "session.input_tokens": settings.tokenUsage.inputTokens ?? 0,
466
+ "session.output_tokens": settings.tokenUsage.outputTokens ?? 0
467
+ } : void 0
468
+ });
469
+ if (sessionLine?.id) root.attributes["tangle.sessionId"] = sessionLine.id;
470
+ return [root, ...spans];
471
+ }
472
+ };
473
+
474
+ // src/adapters/pi.ts
475
+ import { readdir as readdir2, stat as stat2 } from "fs/promises";
476
+ import { homedir as homedir2 } from "os";
477
+ import { basename as basename2, join as join2, resolve } from "path";
478
+ var SERVICE2 = "pi";
479
+ function terminalStatus(message) {
480
+ const stopReason = message?.stopReason ?? "missing";
481
+ if (message?.role === "assistant" && stopReason === "stop") {
482
+ return { status: "OK", stopReason };
483
+ }
484
+ if (message?.role === "assistant" && (stopReason === "error" || message.errorMessage)) {
485
+ return {
486
+ status: "ERROR",
487
+ statusMessage: message.errorMessage?.slice(0, 500) ?? "Pi assistant stopped with an error",
488
+ stopReason
489
+ };
490
+ }
491
+ return {
492
+ status: "UNSET",
493
+ statusMessage: message === void 0 ? "Pi session contained no messages" : `Pi session ended after ${message.role} with stopReason "${stopReason}"`,
494
+ stopReason
495
+ };
496
+ }
497
+ function isToolBlock(b) {
498
+ return typeof b.type === "string" && /tool[_-]?call|tool[_-]?use|^tool$/i.test(b.type);
499
+ }
500
+ function isToolResultBlock(b) {
501
+ return typeof b.type === "string" && /tool[_-]?result|tool[_-]?output/i.test(b.type);
502
+ }
503
+ function textOf2(content) {
504
+ if (!content) return "";
505
+ return capText(
506
+ content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("")
507
+ );
508
+ }
509
+ function toolResultOutput(content) {
510
+ if (content?.length === 1 && content[0]?.type === "text" && typeof content[0].text === "string") {
511
+ return content[0].text;
512
+ }
513
+ return content;
514
+ }
515
+ function completeToolSpan(toolSpan, result) {
516
+ toolSpan.end_time = result.timestamp;
517
+ toolSpan.status = result.isError ? { code: "ERROR", message: result.errorMessage } : { code: "OK" };
518
+ recordToolOutput(toolSpan, result.output, result.source);
519
+ }
520
+ var PiAdapter = class {
521
+ harness = "pi";
522
+ sessionsRoot;
523
+ constructor(options = {}) {
524
+ this.sessionsRoot = resolve(options.sessionsRoot ?? join2(homedir2(), ".pi", "agent", "sessions"));
525
+ }
526
+ root() {
527
+ return this.sessionsRoot;
528
+ }
529
+ async locate(opts = {}) {
530
+ const root = this.root();
531
+ let entries;
532
+ try {
533
+ entries = await readdir2(root, { withFileTypes: true });
534
+ } catch (error) {
535
+ if (isMissingPathError(error)) return [];
536
+ throw error;
537
+ }
538
+ const paths = [];
539
+ for (const entry of entries) {
540
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) {
541
+ paths.push(join2(root, entry.name));
542
+ continue;
543
+ }
544
+ if (!entry.isDirectory()) continue;
545
+ const dir = join2(root, entry.name);
546
+ let files;
547
+ try {
548
+ files = await readdir2(dir);
549
+ } catch (error) {
550
+ if (isMissingPathError(error)) continue;
551
+ throw error;
552
+ }
553
+ for (const file of files) {
554
+ if (file.endsWith(".jsonl")) paths.push(join2(dir, file));
555
+ }
556
+ }
557
+ const refs = [];
558
+ for (const path of paths) {
559
+ const f = basename2(path);
560
+ let st;
561
+ try {
562
+ st = await stat2(path);
563
+ } catch (error) {
564
+ if (isMissingPathError(error)) continue;
565
+ throw error;
566
+ }
567
+ if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
568
+ let id = basename2(f, ".jsonl").replace(/^[\dTZ.-]+_/, "");
569
+ let cwd = null;
570
+ const ref = {
571
+ harness: this.harness,
572
+ sessionId: id,
573
+ path,
574
+ cwd,
575
+ mtimeMs: st.mtimeMs
576
+ };
577
+ const [session] = await takeJsonl(path, 1, sessionJsonlOptions(ref));
578
+ if (session?.type === "session") {
579
+ if (session.id) id = session.id;
580
+ if (session.cwd) cwd = session.cwd;
581
+ }
582
+ ref.sessionId = id;
583
+ ref.cwd = cwd;
584
+ if (ref.integrity) {
585
+ ref.integrity.corruptions = ref.integrity.corruptions.map((receipt) => ({
586
+ ...receipt,
587
+ sessionId: id
588
+ }));
589
+ }
590
+ if (opts.cwd && (!cwd || !cwd.startsWith(opts.cwd))) continue;
591
+ refs.push(ref);
592
+ }
593
+ return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
594
+ }
595
+ async parse(ref, options = {}) {
596
+ const sourceTraceId = ref.sessionId;
597
+ const sourceRootId = `root:${sourceTraceId}`;
598
+ const spans = [];
599
+ const toolByCallId = /* @__PURE__ */ new Map();
600
+ const pendingResults = /* @__PURE__ */ new Map();
601
+ let firstTimestamp;
602
+ let lastTimestamp;
603
+ let sessionLine;
604
+ let terminalMessage;
605
+ let sawLine = false;
606
+ let step = 0;
607
+ for await (const l of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
608
+ if (!sawLine) {
609
+ firstTimestamp = l.timestamp;
610
+ sawLine = true;
611
+ }
612
+ lastTimestamp = l.timestamp;
613
+ if (!sessionLine && l.type === "session") {
614
+ sessionLine = { id: l.id, timestamp: l.timestamp };
615
+ }
616
+ if (l.type === "compaction") {
617
+ spans.push(span({
618
+ traceId: sourceTraceId,
619
+ spanId: "compacted:" + (l.id ?? step),
620
+ parentSpanId: sourceRootId,
621
+ name: "session.compacted",
622
+ kind: "CHAIN",
623
+ service: SERVICE2,
624
+ agent: SERVICE2,
625
+ startTime: l.timestamp ?? "",
626
+ step: step++,
627
+ content: typeof l.summary === "string" ? capText(l.summary) : void 0,
628
+ contentSource: sourceOf(l, "summary"),
629
+ extra: {
630
+ ...typeof l.tokensBefore === "number" && Number.isFinite(l.tokensBefore) && l.tokensBefore >= 0 ? { "traces.compaction.tokens_before": l.tokensBefore } : {},
631
+ ...typeof l.firstKeptEntryId === "string" ? { "traces.pi.first_kept_entry_id": l.firstKeptEntryId } : {},
632
+ ...l.systemMessage !== void 0 ? { "gen_ai.system_instructions": JSON.stringify(l.systemMessage) } : {}
633
+ }
634
+ }));
635
+ continue;
636
+ }
637
+ if (l.type !== "message" || !l.message) continue;
638
+ const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
639
+ const mid = l.id ?? `m${step}`;
640
+ const msg = l.message;
641
+ terminalMessage = {
642
+ role: msg.role ?? "unknown",
643
+ stopReason: msg.stopReason,
644
+ errorMessage: msg.errorMessage
645
+ };
646
+ const llmId = `llm:${mid}`;
647
+ if (msg.role === "toolResult") {
648
+ const callId = msg.toolCallId ?? `${mid}:result`;
649
+ const errorMessage = textOf2(msg.content).slice(0, 500) || "tool result reported error";
650
+ const result = {
651
+ callId,
652
+ toolName: msg.toolName ?? "tool",
653
+ timestamp: ts,
654
+ isError: msg.isError === true,
655
+ output: toolResultOutput(msg.content),
656
+ source: msg.content?.length === 1 && msg.content[0]?.type === "text" && typeof msg.content[0].text === "string" ? sourceOf(msg.content[0], "text") : sourceOf(msg, "content"),
657
+ errorMessage,
658
+ step
659
+ };
660
+ const toolSpan = toolByCallId.get(callId);
661
+ if (toolSpan) completeToolSpan(toolSpan, result);
662
+ else {
663
+ pendingResults.set(callId, result);
664
+ step += 1;
665
+ }
666
+ continue;
667
+ } else if (msg.role === "user") {
668
+ const prompt = textOf2(msg.content);
669
+ if (prompt) {
670
+ spans.push(
671
+ userPromptSpan({
672
+ traceId: sourceTraceId,
673
+ spanId: `${mid}:user`,
674
+ parentSpanId: sourceRootId,
675
+ startTime: ts,
676
+ service: SERVICE2,
677
+ agent: SERVICE2,
678
+ step,
679
+ content: prompt,
680
+ contentSource: textSources(msg, "content")
681
+ })
682
+ );
683
+ step += 1;
684
+ }
685
+ } else {
686
+ const errored = !!msg.errorMessage;
687
+ spans.push(
688
+ span({
689
+ traceId: sourceTraceId,
690
+ spanId: llmId,
691
+ parentSpanId: sourceRootId,
692
+ name: `message.${msg.role ?? "unknown"}`,
693
+ kind: "LLM",
694
+ startTime: ts,
695
+ status: errored ? "ERROR" : "OK",
696
+ statusMessage: errored ? msg.errorMessage.slice(0, 500) : void 0,
697
+ service: SERVICE2,
698
+ agent: SERVICE2,
699
+ model: msg.model ?? null,
700
+ inputTokens: msg.usage?.input ?? null,
701
+ outputTokens: msg.usage?.output ?? null,
702
+ reasoningTokens: msg.usage?.reasoning ?? null,
703
+ cachedInputTokens: msg.usage?.cacheRead ?? null,
704
+ cacheWriteInputTokens: msg.usage?.cacheWrite ?? null,
705
+ step,
706
+ content: textOf2(msg.content) || null,
707
+ contentSource: textSources(msg, "content")
708
+ })
709
+ );
710
+ step += 1;
711
+ }
712
+ for (const b of msg.content ?? []) {
713
+ if (isToolBlock(b)) {
714
+ const name = b.toolName ?? b.name ?? "tool";
715
+ const callId = b.id ?? b.callId ?? b.toolCallId ?? `${mid}:${step}`;
716
+ const toolSpan = span({
717
+ traceId: sourceTraceId,
718
+ spanId: `tool:${callId}`,
719
+ parentSpanId: llmId,
720
+ name: `tool.${name}`,
721
+ kind: "TOOL",
722
+ startTime: ts,
723
+ service: SERVICE2,
724
+ agent: SERVICE2,
725
+ tool: name,
726
+ status: "UNSET",
727
+ step,
728
+ extra: toolIoAttributes({ input: b.input ?? b.args ?? b.arguments, inputSource: sourceOf(b, b.input != null ? "input" : b.args != null ? "args" : "arguments") })
729
+ });
730
+ spans.push(toolSpan);
731
+ toolByCallId.set(callId, toolSpan);
732
+ const pending = pendingResults.get(callId);
733
+ if (pending) {
734
+ completeToolSpan(toolSpan, pending);
735
+ pendingResults.delete(callId);
736
+ }
737
+ step += 1;
738
+ } else if (isToolResultBlock(b)) {
739
+ const callId = b.toolCallId ?? b.callId ?? b.id ?? "";
740
+ const t = toolByCallId.get(callId);
741
+ if (t) {
742
+ const err = b.isError === true || b.is_error === true;
743
+ t.end_time = ts;
744
+ t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
745
+ recordToolOutput(t, b.output ?? b.result ?? b.content ?? b.text, sourceOf(b, b.output != null ? "output" : b.result != null ? "result" : b.content != null ? "content" : "text"));
746
+ }
747
+ }
748
+ }
749
+ }
750
+ for (const result of pendingResults.values()) {
751
+ spans.push(
752
+ span({
753
+ traceId: sourceTraceId,
754
+ spanId: `tool:${result.callId}`,
755
+ parentSpanId: sourceRootId,
756
+ name: `tool.${result.toolName}`,
757
+ kind: "TOOL",
758
+ startTime: result.timestamp,
759
+ endTime: result.timestamp,
760
+ status: result.isError ? "ERROR" : "OK",
761
+ statusMessage: result.isError ? result.errorMessage : void 0,
762
+ service: SERVICE2,
763
+ agent: SERVICE2,
764
+ tool: result.toolName,
765
+ step: result.step,
766
+ extra: {
767
+ ...toolIoAttributes({ output: result.output, outputSource: result.source }),
768
+ "traces.pi.tool_result_without_call": true
769
+ }
770
+ })
771
+ );
772
+ }
773
+ const traceId = sessionLine?.id ?? sourceTraceId;
774
+ const rootId = `root:${traceId}`;
775
+ const terminal = terminalStatus(terminalMessage);
776
+ for (const item of spans) {
777
+ item.trace_id = traceId;
778
+ if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
779
+ }
780
+ const root = span({
781
+ traceId,
782
+ spanId: rootId,
783
+ parentSpanId: null,
784
+ name: "session",
785
+ kind: "AGENT",
786
+ startTime: sessionLine?.timestamp ?? firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
787
+ endTime: lastTimestamp,
788
+ status: terminal.status,
789
+ statusMessage: terminal.statusMessage,
790
+ service: SERVICE2,
791
+ agent: SERVICE2,
792
+ extra: { "traces.pi.stop_reason": terminal.stopReason }
793
+ });
794
+ if (sessionLine?.id) root.attributes["tangle.sessionId"] = sessionLine.id;
795
+ return [root, ...spans];
796
+ }
797
+ };
274
798
 
275
799
  // src/collect.ts
276
800
  async function collectSessions(opts = {}) {
@@ -306,7 +830,7 @@ import {
306
830
  import { createHash } from "crypto";
307
831
  import { mkdir, mkdtemp, rm, writeFile } from "fs/promises";
308
832
  import { tmpdir } from "os";
309
- import { dirname, isAbsolute, join, relative, resolve } from "path";
833
+ import { dirname, isAbsolute, join as join3, relative, resolve as resolve2 } from "path";
310
834
  var RETAINED_SESSION_READER_VERSION = "2";
311
835
  async function parseRetainedSession(input) {
312
836
  input.signal?.throwIfAborted();
@@ -332,12 +856,12 @@ async function parseRetainedSession(input) {
332
856
  return unavailable("Retained capture exceeds the 16 MiB per-file or 64 MiB total text limit.");
333
857
  }
334
858
  }
335
- const root = await mkdtemp(join(tmpdir(), "traces-retained-"));
859
+ const root = await mkdtemp(join3(tmpdir(), "traces-retained-"));
336
860
  try {
337
861
  const files = /* @__PURE__ */ new Map();
338
862
  for (const file of input.files) {
339
863
  input.signal?.throwIfAborted();
340
- const path = resolve(root, file.path);
864
+ const path = resolve2(root, file.path);
341
865
  const rel = relative(root, path);
342
866
  if (!file.path || isAbsolute(file.path) || file.path.includes("\\") || file.path.includes("\0") || rel === "" || rel === ".." || rel.startsWith("../") || files.has(path)) {
343
867
  throw new Error("Retained source paths must be unique relative files inside the capture.");
@@ -355,7 +879,7 @@ async function parseRetainedSession(input) {
355
879
  const ref = { harness: adapter.harness, sessionId: fallback, path, cwd: null, mtimeMs: 0 };
356
880
  try {
357
881
  const sources = adapter.sourcePaths ? await adapter.sourcePaths(ref, { signal: input.signal }) : [path];
358
- if (sources.some((source) => !files.has(resolve(source)))) {
882
+ if (sources.some((source) => !files.has(resolve2(source)))) {
359
883
  throw new Error("Parser requires files outside the supplied retained capture.");
360
884
  }
361
885
  const spans = validateOtlpSpans(await adapter.parse(ref, {
@@ -400,7 +924,7 @@ async function parseRetainedSession(input) {
400
924
  reason: null,
401
925
  sessionId: input.nativeSessionId,
402
926
  sourceFiles: chosen.paths.map((path) => {
403
- const file = files.get(resolve(path));
927
+ const file = files.get(resolve2(path));
404
928
  return { path: file.path, sha256: `sha256:${createHash("sha256").update(file.content).digest("hex")}` };
405
929
  }).sort((a, b) => a.path.localeCompare(b.path)),
406
930
  duplicateFiles: selected.slice(1).map((candidate) => candidate.file.path),
@@ -432,6 +956,7 @@ export {
432
956
  DEFAULT_ASK_CONCURRENCY,
433
957
  DEFAULT_HARNESS,
434
958
  DEFAULT_SANDBOX_BASE_URL,
959
+ DEFAULT_SWEEP_THRESHOLD,
435
960
  EMPTY_SPAN_RUN_SNAPSHOT,
436
961
  EXPORT_RULE,
437
962
  EmptySessionError,
@@ -440,6 +965,7 @@ export {
440
965
  FactoryAdapter,
441
966
  ForgeAdapter,
442
967
  GeminiAdapter,
968
+ HarnessSessionsAdapter,
443
969
  INGEST_SOURCE_CLI,
444
970
  INHERITED_SOURCE_ATTR,
445
971
  INHERITED_SPANS_OMITTED_ATTR,
@@ -469,6 +995,7 @@ export {
469
995
  SYNTHESIZED_SOURCE_ATTR,
470
996
  SYNTHESIZED_SPAN_ATTR,
471
997
  SessionWorkflowError,
998
+ SkillUsageStore,
472
999
  TRACE_QUESTION_PREVIEW_HEAD_CHARS,
473
1000
  ZERO_ROLL,
474
1001
  addSpend,
@@ -492,6 +1019,7 @@ export {
492
1019
  buildSessionFactsReport,
493
1020
  buildSessionIndex,
494
1021
  buildSessionIndexFromRows,
1022
+ buildSkillUsageReport,
495
1023
  buildSpanRunTree,
496
1024
  buildTraceFindingPacket,
497
1025
  capText,
@@ -520,6 +1048,9 @@ export {
520
1048
  createHostedClient,
521
1049
  cwdMatchesSelection,
522
1050
  cwdMatchesSelectionResolved,
1051
+ defaultSkillCatalogRoots,
1052
+ defaultSkillUsageSources,
1053
+ defaultSkillUsageStorePath,
523
1054
  defaultTraceLiveAnalysts,
524
1055
  describeSessionRelationship,
525
1056
  diffRuns,
@@ -589,6 +1120,7 @@ export {
589
1120
  projectSessionBundle,
590
1121
  readCoordinationLog,
591
1122
  readFileRunContext,
1123
+ readInstalledSkills,
592
1124
  readJsonFile,
593
1125
  readJsonl,
594
1126
  readOtlpInput,
@@ -612,6 +1144,7 @@ export {
612
1144
  renderRunWatchSnapshot,
613
1145
  renderSessionFacts,
614
1146
  renderSessionFactsContext,
1147
+ renderSkillUsage,
615
1148
  renderSpanRunSnapshot,
616
1149
  renderTraceQuestionInstructions,
617
1150
  renderTraceQuestionPrompt,