@tangle-network/traces 0.2.2 → 0.4.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.
@@ -70,6 +70,25 @@ async function analyzeSpans(spans, opts = {}) {
70
70
  return { otlpPath, result };
71
71
  }
72
72
 
73
+ // src/time.ts
74
+ function parseIsoToEpochMs(ts) {
75
+ if (!ts) return 0;
76
+ if (/^\d+$/.test(ts)) return Number(ts);
77
+ const n = Date.parse(ts);
78
+ return Number.isNaN(n) ? 0 : n;
79
+ }
80
+ function parseSince(value) {
81
+ const m = value.match(/^(\d+)\s*([mhd])$/i);
82
+ if (m) {
83
+ const unit = m[2].toLowerCase();
84
+ const ms = unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
85
+ return Date.now() - Number(m[1]) * ms;
86
+ }
87
+ const t = Date.parse(value);
88
+ if (Number.isNaN(t)) throw new Error(`--since: expected 30m / 2h / 7d or an ISO date, got "${value}"`);
89
+ return t;
90
+ }
91
+
73
92
  // src/runtime-store.ts
74
93
  import { InMemoryTraceStore } from "@tangle-network/agent-eval";
75
94
  var KIND = {
@@ -79,10 +98,6 @@ var KIND = {
79
98
  CHAIN: "custom",
80
99
  SPAN: "custom"
81
100
  };
82
- function ms(iso2) {
83
- const n = Date.parse(iso2);
84
- return Number.isNaN(n) ? 0 : n;
85
- }
86
101
  async function toRuntimeStore(spans) {
87
102
  const store = new InMemoryTraceStore();
88
103
  const runIds = /* @__PURE__ */ new Set();
@@ -97,8 +112,8 @@ async function toRuntimeStore(spans) {
97
112
  parentSpanId: s.parent_span_id ?? void 0,
98
113
  runId: s.trace_id,
99
114
  name: s.name,
100
- startedAt: ms(s.start_time),
101
- endedAt: ms(s.end_time),
115
+ startedAt: parseIsoToEpochMs(s.start_time),
116
+ endedAt: parseIsoToEpochMs(s.end_time),
102
117
  status: s.status.code === "ERROR" ? "error" : "ok",
103
118
  error: s.status.message,
104
119
  attributes: s.attributes
@@ -129,6 +144,11 @@ import { readdir, readFile, stat } from "fs/promises";
129
144
  import { homedir } from "os";
130
145
  import { basename, join } from "path";
131
146
  var SERVICE = "amp";
147
+ var CONTENT_CAP = 8e3;
148
+ function textOf(content) {
149
+ if (typeof content === "string") return content.slice(0, CONTENT_CAP);
150
+ return (Array.isArray(content) ? content : []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n").trim().slice(0, CONTENT_CAP);
151
+ }
132
152
  var AmpAdapter = class {
133
153
  harness = "amp";
134
154
  threadsRoot() {
@@ -177,25 +197,47 @@ var AmpAdapter = class {
177
197
  for (const m of messages) {
178
198
  const mid = m.messageId ?? step;
179
199
  const llmId = `llm:${mid}`;
180
- const u = m.usage;
181
- const inputTokens2 = u ? (u.inputTokens ?? 0) + (u.cacheReadInputTokens ?? 0) + (u.cacheCreationInputTokens ?? 0) || null : null;
182
- spans.push(
183
- span({
184
- traceId,
185
- spanId: llmId,
186
- parentSpanId: rootId,
187
- name: `message.${m.role ?? "unknown"}`,
188
- kind: "LLM",
189
- startTime: start,
190
- service: SERVICE,
191
- agent: SERVICE,
192
- model: u?.model ?? null,
193
- inputTokens: inputTokens2,
194
- outputTokens: u?.outputTokens ?? null,
195
- step
196
- })
197
- );
198
- step += 1;
200
+ if (m.role === "user") {
201
+ const prompt = textOf(m.content);
202
+ if (prompt) {
203
+ spans.push(
204
+ span({
205
+ traceId,
206
+ spanId: `${llmId}:user`,
207
+ parentSpanId: rootId,
208
+ name: "user.prompt",
209
+ kind: "CHAIN",
210
+ startTime: start,
211
+ service: SERVICE,
212
+ agent: SERVICE,
213
+ step,
214
+ content: prompt
215
+ })
216
+ );
217
+ step += 1;
218
+ }
219
+ } else {
220
+ const u = m.usage;
221
+ const inputTokens2 = u ? (u.inputTokens ?? 0) + (u.cacheReadInputTokens ?? 0) + (u.cacheCreationInputTokens ?? 0) || null : null;
222
+ spans.push(
223
+ span({
224
+ traceId,
225
+ spanId: llmId,
226
+ parentSpanId: rootId,
227
+ name: `message.${m.role ?? "unknown"}`,
228
+ kind: "LLM",
229
+ startTime: start,
230
+ service: SERVICE,
231
+ agent: SERVICE,
232
+ model: u?.model ?? null,
233
+ inputTokens: inputTokens2,
234
+ outputTokens: u?.outputTokens ?? null,
235
+ step,
236
+ content: textOf(m.content) || null
237
+ })
238
+ );
239
+ step += 1;
240
+ }
199
241
  for (const b of m.content ?? []) {
200
242
  if (b.type === "tool_use" && b.name) {
201
243
  const t = span({
@@ -248,6 +290,11 @@ function inputTokens(u) {
248
290
  function asBlocks(content) {
249
291
  return Array.isArray(content) ? content : [];
250
292
  }
293
+ var CONTENT_CAP2 = 8e3;
294
+ function textOf2(content) {
295
+ if (typeof content === "string") return content.slice(0, CONTENT_CAP2);
296
+ return asBlocks(content).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n").trim().slice(0, CONTENT_CAP2);
297
+ }
251
298
  function stringifyToolResult(content) {
252
299
  if (typeof content === "string") return content;
253
300
  if (Array.isArray(content)) {
@@ -277,7 +324,8 @@ function parseClaudeStream(events, ctx) {
277
324
  model: ev.message.model ?? null,
278
325
  inputTokens: inputTokens(ev.message.usage),
279
326
  outputTokens: ev.message.usage?.output_tokens ?? null,
280
- step
327
+ step,
328
+ content: textOf2(ev.message.content) || null
281
329
  })
282
330
  );
283
331
  step += 1;
@@ -303,6 +351,24 @@ function parseClaudeStream(events, ctx) {
303
351
  step += 1;
304
352
  }
305
353
  } else if (ev.type === "user" && ev.message) {
354
+ const prompt = textOf2(ev.message.content);
355
+ if (prompt) {
356
+ spans.push(
357
+ span({
358
+ traceId: ctx.traceId,
359
+ spanId: `${ctx.idPrefix}${uid}:user`,
360
+ parentSpanId: ctx.rootParent,
361
+ name: "user.prompt",
362
+ kind: "CHAIN",
363
+ startTime: ts,
364
+ service: SERVICE2,
365
+ agent: ctx.agent,
366
+ step,
367
+ content: prompt
368
+ })
369
+ );
370
+ step += 1;
371
+ }
306
372
  for (const block of asBlocks(ev.message.content)) {
307
373
  if (block.type !== "tool_result" || !block.tool_use_id) continue;
308
374
  backfillResult(toolSpanByUseId.get(block.tool_use_id), ts, block.is_error === true, stringifyToolResult(block.content));
@@ -452,6 +518,10 @@ function contentToString(content) {
452
518
  }
453
519
  return "";
454
520
  }
521
+ var CONTENT_CAP3 = 8e3;
522
+ function textOf3(content) {
523
+ return contentToString(content).trim().slice(0, CONTENT_CAP3);
524
+ }
455
525
  function outputIsError(output) {
456
526
  const s = typeof output === "string" ? output : JSON.stringify(output ?? "");
457
527
  const error = /"success"\s*:\s*false|exit(?:_| )code["\s:]+[1-9]|\bcommand failed\b|\bENOENT\b|\berror:/i.test(s);
@@ -599,8 +669,27 @@ var CodexAdapter = class {
599
669
  t.end_time = ts;
600
670
  t.status = error ? { code: "ERROR", message } : { code: "OK" };
601
671
  }
672
+ } else if (l.type === "response_item" && l.payload?.type === "message" && l.payload.role === "user") {
673
+ const prompt = textOf3(l.payload.content);
674
+ if (prompt) {
675
+ spans.push(
676
+ span({
677
+ traceId,
678
+ spanId: `msg:${step}:user`,
679
+ parentSpanId: rootId,
680
+ name: "user.prompt",
681
+ kind: "CHAIN",
682
+ startTime: ts,
683
+ service: SERVICE3,
684
+ agent: SERVICE3,
685
+ step,
686
+ content: prompt
687
+ })
688
+ );
689
+ step += 1;
690
+ }
602
691
  } else if (l.type === "response_item" && l.payload?.type === "message") {
603
- const text = contentToString(l.payload.content);
692
+ const text = textOf3(l.payload.content);
604
693
  if (text) {
605
694
  spans.push(
606
695
  span({
@@ -613,7 +702,7 @@ var CodexAdapter = class {
613
702
  service: SERVICE3,
614
703
  agent: SERVICE3,
615
704
  step,
616
- content: text.slice(0, 8e3)
705
+ content: text
617
706
  })
618
707
  );
619
708
  step += 1;
@@ -755,6 +844,10 @@ import { readdir as readdir5, readFile as readFile5, stat as stat5 } from "fs/pr
755
844
  import { homedir as homedir5 } from "os";
756
845
  import { basename as basename4, join as join5 } from "path";
757
846
  var SERVICE5 = "factory";
847
+ var CONTENT_CAP4 = 8e3;
848
+ function textOf4(content) {
849
+ return (content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("").trim().slice(0, CONTENT_CAP4);
850
+ }
758
851
  function parseLines4(raw) {
759
852
  const out = [];
760
853
  for (const line of raw.split("\n")) {
@@ -841,25 +934,45 @@ var FactoryAdapter = class {
841
934
  if (l.type !== "message" || !l.message) continue;
842
935
  const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
843
936
  const role = l.message.role;
844
- const llmId = `llm:${l.id ?? step}`;
845
- const text = (l.message.content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
846
- spans.push(
847
- span({
848
- traceId,
849
- spanId: llmId,
850
- parentSpanId: rootId,
851
- name: `message.${role ?? "unknown"}`,
852
- kind: "LLM",
853
- startTime: ts,
854
- service: SERVICE5,
855
- agent: SERVICE5,
856
- model: settings.model ?? null,
857
- step,
858
- content: text ? text.slice(0, 8e3) : null
859
- })
860
- );
861
- lastLlm = llmId;
862
- step += 1;
937
+ const text = textOf4(l.message.content);
938
+ if (role === "user") {
939
+ if (text) {
940
+ spans.push(
941
+ span({
942
+ traceId,
943
+ spanId: `user:${l.id ?? step}`,
944
+ parentSpanId: rootId,
945
+ name: "user.prompt",
946
+ kind: "CHAIN",
947
+ startTime: ts,
948
+ service: SERVICE5,
949
+ agent: SERVICE5,
950
+ step,
951
+ content: text
952
+ })
953
+ );
954
+ step += 1;
955
+ }
956
+ } else {
957
+ const llmId = `llm:${l.id ?? step}`;
958
+ spans.push(
959
+ span({
960
+ traceId,
961
+ spanId: llmId,
962
+ parentSpanId: rootId,
963
+ name: `message.${role ?? "unknown"}`,
964
+ kind: "LLM",
965
+ startTime: ts,
966
+ service: SERVICE5,
967
+ agent: SERVICE5,
968
+ model: settings.model ?? null,
969
+ step,
970
+ content: text || null
971
+ })
972
+ );
973
+ lastLlm = llmId;
974
+ step += 1;
975
+ }
863
976
  for (const b of l.message.content ?? []) {
864
977
  if (b.type === "tool_use" && b.name) {
865
978
  const t = span({
@@ -897,6 +1010,10 @@ function tokens(t) {
897
1010
  if (!t) return null;
898
1011
  return t.actual ?? t.approx ?? null;
899
1012
  }
1013
+ var CONTENT_CAP5 = 8e3;
1014
+ function textOf5(content) {
1015
+ return (content ?? "").trim().slice(0, CONTENT_CAP5);
1016
+ }
900
1017
  var ForgeAdapter = class {
901
1018
  harness = "forge";
902
1019
  aliases = ["forgecode"];
@@ -949,7 +1066,26 @@ var ForgeAdapter = class {
949
1066
  for (const raw of messages) {
950
1067
  const entry = raw.message ?? raw;
951
1068
  const ts = (/* @__PURE__ */ new Date(0)).toISOString();
952
- if (entry.text) {
1069
+ if (entry.text && entry.text.role === "user") {
1070
+ const prompt = textOf5(entry.text.content);
1071
+ if (prompt) {
1072
+ spans.push(
1073
+ span({
1074
+ traceId,
1075
+ spanId: `user:${step}`,
1076
+ parentSpanId: rootId,
1077
+ name: "user.prompt",
1078
+ kind: "CHAIN",
1079
+ startTime: ts,
1080
+ service: SERVICE6,
1081
+ agent: SERVICE6,
1082
+ step,
1083
+ content: prompt
1084
+ })
1085
+ );
1086
+ step += 1;
1087
+ }
1088
+ } else if (entry.text) {
953
1089
  const llmId = `llm:${step}`;
954
1090
  spans.push(
955
1091
  span({
@@ -965,7 +1101,7 @@ var ForgeAdapter = class {
965
1101
  inputTokens: tokens(raw.usage?.prompt_tokens),
966
1102
  outputTokens: tokens(raw.usage?.completion_tokens),
967
1103
  step,
968
- content: entry.text.content ? entry.text.content.slice(0, 8e3) : null
1104
+ content: textOf5(entry.text.content) || null
969
1105
  })
970
1106
  );
971
1107
  lastLlm = llmId;
@@ -1010,6 +1146,11 @@ function iso(ts, fallback) {
1010
1146
  function toolStatusError(status) {
1011
1147
  return status != null && /error|fail|cancel/i.test(status);
1012
1148
  }
1149
+ var CONTENT_CAP6 = 8e3;
1150
+ function textOf6(content) {
1151
+ const s = typeof content === "string" ? content : JSON.stringify(content ?? "");
1152
+ return s.trim().slice(0, CONTENT_CAP6);
1153
+ }
1013
1154
  var GeminiFamilyAdapter = class {
1014
1155
  harness;
1015
1156
  aliases;
@@ -1086,7 +1227,26 @@ var GeminiFamilyAdapter = class {
1086
1227
  const mid = m.id ?? `m${step}`;
1087
1228
  const ts = iso(m.timestamp, start);
1088
1229
  const llmId = `llm:${mid}`;
1089
- const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? "");
1230
+ if (m.type === "user") {
1231
+ const prompt = textOf6(m.content);
1232
+ if (prompt) {
1233
+ spans.push(
1234
+ span({
1235
+ traceId,
1236
+ spanId: `${llmId}:user`,
1237
+ parentSpanId: rootId,
1238
+ name: "user.prompt",
1239
+ kind: "CHAIN",
1240
+ startTime: ts,
1241
+ service: this.service,
1242
+ agent: this.service,
1243
+ step,
1244
+ content: prompt
1245
+ })
1246
+ );
1247
+ step += 1;
1248
+ }
1249
+ }
1090
1250
  spans.push(
1091
1251
  span({
1092
1252
  traceId,
@@ -1101,7 +1261,7 @@ var GeminiFamilyAdapter = class {
1101
1261
  inputTokens: m.tokens?.input ?? null,
1102
1262
  outputTokens: m.tokens?.output ?? null,
1103
1263
  step,
1104
- content: content ? content.slice(0, 8e3) : null
1264
+ content: textOf6(m.content) || null
1105
1265
  })
1106
1266
  );
1107
1267
  step += 1;
@@ -1141,8 +1301,12 @@ import { readdir as readdir8, readFile as readFile8, stat as stat8 } from "fs/pr
1141
1301
  import { homedir as homedir8 } from "os";
1142
1302
  import { join as join8 } from "path";
1143
1303
  var SERVICE7 = "opencode";
1144
- function isoOf(ms2) {
1145
- return new Date(ms2 ?? 0).toISOString();
1304
+ function isoOf(ms) {
1305
+ return new Date(ms ?? 0).toISOString();
1306
+ }
1307
+ var CONTENT_CAP7 = 8e3;
1308
+ function textOf7(parts) {
1309
+ return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("\n").trim().slice(0, CONTENT_CAP7);
1146
1310
  }
1147
1311
  var OpencodeAdapter = class {
1148
1312
  harness = "opencode";
@@ -1209,6 +1373,19 @@ var OpencodeAdapter = class {
1209
1373
  for (const { msg } of messages) {
1210
1374
  const mid = msg.id ?? `m${step}`;
1211
1375
  const llmId = `llm:${mid}`;
1376
+ const pdir = join8(partRoot, mid);
1377
+ const parts = [];
1378
+ try {
1379
+ const partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
1380
+ for (const pf of partFiles) {
1381
+ try {
1382
+ parts.push(JSON.parse(await readFile8(join8(pdir, pf), "utf8")));
1383
+ } catch {
1384
+ }
1385
+ }
1386
+ } catch {
1387
+ }
1388
+ const turnText = textOf7(parts);
1212
1389
  spans.push(
1213
1390
  span({
1214
1391
  traceId,
@@ -1223,24 +1400,29 @@ var OpencodeAdapter = class {
1223
1400
  model: msg.modelID ?? null,
1224
1401
  inputTokens: msg.tokens?.input ?? null,
1225
1402
  outputTokens: msg.tokens?.output ?? null,
1226
- step
1403
+ step,
1404
+ content: turnText || null
1227
1405
  })
1228
1406
  );
1229
1407
  step += 1;
1230
- const pdir = join8(partRoot, mid);
1231
- let partFiles;
1232
- try {
1233
- partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
1234
- } catch {
1235
- continue;
1408
+ if (msg.role === "user" && turnText) {
1409
+ spans.push(
1410
+ span({
1411
+ traceId,
1412
+ spanId: `${llmId}:user`,
1413
+ parentSpanId: rootId,
1414
+ name: "user.prompt",
1415
+ kind: "CHAIN",
1416
+ startTime: isoOf(msg.time?.created),
1417
+ service: SERVICE7,
1418
+ agent: SERVICE7,
1419
+ step,
1420
+ content: turnText
1421
+ })
1422
+ );
1423
+ step += 1;
1236
1424
  }
1237
- for (const pf of partFiles) {
1238
- let part;
1239
- try {
1240
- part = JSON.parse(await readFile8(join8(pdir, pf), "utf8"));
1241
- } catch {
1242
- continue;
1243
- }
1425
+ for (const part of parts) {
1244
1426
  if (part.type !== "tool" || !part.tool) continue;
1245
1427
  const err = part.state?.status === "error";
1246
1428
  spans.push(
@@ -1290,9 +1472,10 @@ function isToolBlock(b) {
1290
1472
  function isToolResultBlock(b) {
1291
1473
  return typeof b.type === "string" && /tool[_-]?result|tool[_-]?output/i.test(b.type);
1292
1474
  }
1293
- function textOf(content) {
1475
+ var CONTENT_CAP8 = 8e3;
1476
+ function textOf8(content) {
1294
1477
  if (!content) return "";
1295
- return content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
1478
+ return content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("").trim().slice(0, CONTENT_CAP8);
1296
1479
  }
1297
1480
  var PiAdapter = class {
1298
1481
  harness = "pi";
@@ -1360,27 +1543,48 @@ var PiAdapter = class {
1360
1543
  const mid = l.id ?? `m${step}`;
1361
1544
  const msg = l.message;
1362
1545
  const llmId = `llm:${mid}`;
1363
- const errored = !!msg.errorMessage;
1364
- spans.push(
1365
- span({
1366
- traceId,
1367
- spanId: llmId,
1368
- parentSpanId: rootId,
1369
- name: `message.${msg.role ?? "unknown"}`,
1370
- kind: "LLM",
1371
- startTime: ts,
1372
- status: errored ? "ERROR" : "OK",
1373
- statusMessage: errored ? msg.errorMessage.slice(0, 500) : void 0,
1374
- service: SERVICE8,
1375
- agent: SERVICE8,
1376
- model: msg.model ?? null,
1377
- inputTokens: msg.usage?.input ?? null,
1378
- outputTokens: msg.usage?.output ?? null,
1379
- step,
1380
- content: textOf(msg.content).slice(0, 8e3) || null
1381
- })
1382
- );
1383
- step += 1;
1546
+ if (msg.role === "user") {
1547
+ const prompt = textOf8(msg.content);
1548
+ if (prompt) {
1549
+ spans.push(
1550
+ span({
1551
+ traceId,
1552
+ spanId: `${mid}:user`,
1553
+ parentSpanId: rootId,
1554
+ name: "user.prompt",
1555
+ kind: "CHAIN",
1556
+ startTime: ts,
1557
+ service: SERVICE8,
1558
+ agent: SERVICE8,
1559
+ step,
1560
+ content: prompt
1561
+ })
1562
+ );
1563
+ step += 1;
1564
+ }
1565
+ } else {
1566
+ const errored = !!msg.errorMessage;
1567
+ spans.push(
1568
+ span({
1569
+ traceId,
1570
+ spanId: llmId,
1571
+ parentSpanId: rootId,
1572
+ name: `message.${msg.role ?? "unknown"}`,
1573
+ kind: "LLM",
1574
+ startTime: ts,
1575
+ status: errored ? "ERROR" : "OK",
1576
+ statusMessage: errored ? msg.errorMessage.slice(0, 500) : void 0,
1577
+ service: SERVICE8,
1578
+ agent: SERVICE8,
1579
+ model: msg.model ?? null,
1580
+ inputTokens: msg.usage?.input ?? null,
1581
+ outputTokens: msg.usage?.output ?? null,
1582
+ step,
1583
+ content: textOf8(msg.content) || null
1584
+ })
1585
+ );
1586
+ step += 1;
1587
+ }
1384
1588
  for (const b of msg.content ?? []) {
1385
1589
  if (isToolBlock(b)) {
1386
1590
  const name = b.toolName ?? b.name ?? "tool";
@@ -1432,7 +1636,7 @@ function parseLines6(raw) {
1432
1636
  }
1433
1637
  return out;
1434
1638
  }
1435
- function textOf2(parts) {
1639
+ function textOf9(parts) {
1436
1640
  if (!parts) return "";
1437
1641
  return parts.filter((p) => typeof p.text === "string").map((p) => p.text).join("");
1438
1642
  }
@@ -1514,7 +1718,7 @@ var QwenAdapter = class {
1514
1718
  inputTokens: r.usageMetadata?.promptTokenCount ?? null,
1515
1719
  outputTokens: r.usageMetadata?.candidatesTokenCount ?? null,
1516
1720
  step,
1517
- content: textOf2(r.message?.parts).slice(0, 8e3) || null
1721
+ content: textOf9(r.message?.parts).slice(0, 8e3) || null
1518
1722
  })
1519
1723
  );
1520
1724
  lastLlm = llmId;
@@ -1541,6 +1745,25 @@ var QwenAdapter = class {
1541
1745
  openToolsByName.set(name, q);
1542
1746
  step += 1;
1543
1747
  }
1748
+ } else if (r.type === "user") {
1749
+ const prompt = textOf9(r.message?.parts).slice(0, 8e3);
1750
+ if (prompt) {
1751
+ spans.push(
1752
+ span({
1753
+ traceId,
1754
+ spanId: `user:${r.uuid ?? step}`,
1755
+ parentSpanId: rootId,
1756
+ name: "user.prompt",
1757
+ kind: "CHAIN",
1758
+ startTime: ts,
1759
+ service: SERVICE9,
1760
+ agent: SERVICE9,
1761
+ step,
1762
+ content: prompt
1763
+ })
1764
+ );
1765
+ step += 1;
1766
+ }
1544
1767
  } else if (r.type === "tool_result") {
1545
1768
  const err = r.toolCallResult?.status === "error" || r.toolCallResult?.error != null;
1546
1769
  for (const p of r.message?.parts ?? []) {
@@ -1585,9 +1808,45 @@ function resolveAdapter(harness) {
1585
1808
  function knownHarnesses() {
1586
1809
  return [...BY_NAME.keys()].sort();
1587
1810
  }
1811
+ function selectAdapters(opts) {
1812
+ if (opts.adapters && opts.adapters.length > 0) return [...opts.adapters];
1813
+ if (opts.all || !opts.harnesses || opts.harnesses.length === 0) return [...ADAPTERS];
1814
+ return opts.harnesses.map((h) => {
1815
+ const a = resolveAdapter(h);
1816
+ if (!a) throw new Error(`unknown harness "${h}". Known: ${knownHarnesses().join(", ")}`);
1817
+ return a;
1818
+ });
1819
+ }
1820
+
1821
+ // src/session-source.ts
1822
+ async function* scanSessions(opts) {
1823
+ for (const adapter of selectAdapters(opts)) {
1824
+ if (opts.signal?.aborted) return;
1825
+ let refs;
1826
+ try {
1827
+ refs = await adapter.locate({ cwd: opts.cwd, sinceMs: opts.sinceMs });
1828
+ } catch (err) {
1829
+ opts.onError?.(err);
1830
+ continue;
1831
+ }
1832
+ if (opts.last && opts.last > 0) refs = refs.slice(0, opts.last);
1833
+ for (const ref of refs) {
1834
+ if (opts.signal?.aborted) return;
1835
+ let spans;
1836
+ try {
1837
+ spans = await adapter.parse(ref);
1838
+ } catch (err) {
1839
+ opts.onError?.(err, ref);
1840
+ continue;
1841
+ }
1842
+ if (spans.length === 0) continue;
1843
+ yield { adapter, ref, spans };
1844
+ }
1845
+ }
1846
+ }
1588
1847
 
1589
1848
  // src/observer.ts
1590
- var sleep = (ms2, signal) => new Promise((resolve) => {
1849
+ var sleep = (ms, signal) => new Promise((resolve) => {
1591
1850
  const onAbort = () => {
1592
1851
  clearTimeout(t);
1593
1852
  resolve();
@@ -1595,19 +1854,9 @@ var sleep = (ms2, signal) => new Promise((resolve) => {
1595
1854
  const t = setTimeout(() => {
1596
1855
  signal?.removeEventListener("abort", onAbort);
1597
1856
  resolve();
1598
- }, ms2);
1857
+ }, ms);
1599
1858
  signal?.addEventListener("abort", onAbort, { once: true });
1600
1859
  });
1601
- function adaptersFor(opts) {
1602
- if (opts.adapters && opts.adapters.length > 0) return opts.adapters;
1603
- if (opts.all || !opts.harnesses || opts.harnesses.length === 0) return [...listAdapters()];
1604
- const out = [];
1605
- for (const h of opts.harnesses) {
1606
- const a = resolveAdapter(h);
1607
- if (a && !out.includes(a)) out.push(a);
1608
- }
1609
- return out;
1610
- }
1611
1860
  function loopKey(sessionId, toolName, argHash) {
1612
1861
  return `${sessionId}\0${toolName}\0${argHash}`;
1613
1862
  }
@@ -1615,57 +1864,50 @@ async function watchSessions(opts) {
1615
1864
  const windowMs = opts.windowMs ?? 30 * 6e4;
1616
1865
  const intervalMs = Math.max(250, opts.intervalMs ?? 5e3);
1617
1866
  const minLoop = opts.minLoopOccurrences ?? 3;
1618
- const adapters = adaptersFor(opts);
1619
1867
  const seen = /* @__PURE__ */ new Map();
1620
1868
  while (!opts.signal?.aborted) {
1621
- const sinceMs = Date.now() - windowMs;
1622
1869
  let sessionCount = 0;
1623
- for (const adapter of adapters) {
1624
- if (opts.signal?.aborted) break;
1625
- let refs;
1870
+ for await (const { adapter, ref, spans } of scanSessions({
1871
+ adapters: opts.adapters,
1872
+ all: opts.all,
1873
+ harnesses: opts.harnesses,
1874
+ cwd: opts.cwd,
1875
+ sinceMs: Date.now() - windowMs,
1876
+ signal: opts.signal,
1877
+ onError: opts.onError
1878
+ })) {
1879
+ let report;
1626
1880
  try {
1627
- refs = await adapter.locate({ cwd: opts.cwd, sinceMs });
1881
+ report = await runPipelines(spans, { minLoopOccurrences: minLoop });
1628
1882
  } catch (err) {
1629
- opts.onError?.(err);
1883
+ opts.onError?.(err, ref);
1630
1884
  continue;
1631
1885
  }
1632
- for (const ref of refs) {
1633
- if (opts.signal?.aborted) break;
1634
- let report;
1886
+ sessionCount += 1;
1887
+ if (opts.onReport) {
1635
1888
  try {
1636
- const spans = await adapter.parse(ref);
1637
- if (spans.length === 0) continue;
1638
- report = await runPipelines(spans, { minLoopOccurrences: minLoop });
1889
+ await opts.onReport(ref, report);
1639
1890
  } catch (err) {
1640
1891
  opts.onError?.(err, ref);
1641
- continue;
1642
1892
  }
1643
- sessionCount += 1;
1644
- if (opts.onReport) {
1645
- try {
1646
- await opts.onReport(ref, report);
1647
- } catch (err) {
1648
- opts.onError?.(err, ref);
1649
- }
1650
- }
1651
- for (const f of report.stuckLoops.findings) {
1652
- const key = loopKey(ref.sessionId, f.toolName, f.argHash);
1653
- if (f.occurrences > (seen.get(key) ?? 0)) {
1654
- seen.set(key, f.occurrences);
1655
- if (opts.onLoop) {
1656
- try {
1657
- await opts.onLoop({
1658
- harness: adapter.harness,
1659
- sessionId: ref.sessionId,
1660
- cwd: ref.cwd,
1661
- toolName: f.toolName,
1662
- argHash: f.argHash,
1663
- occurrences: f.occurrences,
1664
- windowMs: f.windowMs
1665
- });
1666
- } catch (err) {
1667
- opts.onError?.(err, ref);
1668
- }
1893
+ }
1894
+ for (const f of report.stuckLoops.findings) {
1895
+ const key = loopKey(ref.sessionId, f.toolName, f.argHash);
1896
+ if (f.occurrences > (seen.get(key) ?? 0)) {
1897
+ seen.set(key, f.occurrences);
1898
+ if (opts.onLoop) {
1899
+ try {
1900
+ await opts.onLoop({
1901
+ harness: adapter.harness,
1902
+ sessionId: ref.sessionId,
1903
+ cwd: ref.cwd,
1904
+ toolName: f.toolName,
1905
+ argHash: f.argHash,
1906
+ occurrences: f.occurrences,
1907
+ windowMs: f.windowMs
1908
+ });
1909
+ } catch (err) {
1910
+ opts.onError?.(err, ref);
1669
1911
  }
1670
1912
  }
1671
1913
  }
@@ -1681,6 +1923,28 @@ async function watchSessions(opts) {
1681
1923
  }
1682
1924
  }
1683
1925
 
1926
+ // src/attributes.ts
1927
+ var ATTR = {
1928
+ /** = the trace/session id. Wire contract (dedup linkage). */
1929
+ SESSION_ID: "tangle.sessionId",
1930
+ /** "cli" marks a CLI upload so the platform can drop a re-upload of a session
1931
+ * that also streamed live. Wire contract. */
1932
+ INGEST_SOURCE: "tangle.ingest_source",
1933
+ HARNESS: "tangle.harness",
1934
+ CWD: "tangle.cwd",
1935
+ GIT_BRANCH: "tangle.git_branch",
1936
+ HOST: "tangle.host",
1937
+ /** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
1938
+ * which collided with `tangle.ingest_source`.) */
1939
+ SESSION_FILE: "tangle.session_file",
1940
+ UPLOADED_AT: "tangle.uploaded_at",
1941
+ UPLOADER: "tangle.uploader",
1942
+ REDACTION_VERSION: "redaction.version",
1943
+ REDACTION_COUNT: "redaction.count"
1944
+ };
1945
+ var INGEST_SOURCE_CLI = "cli";
1946
+ var DEFAULT_HARNESS = "claude-code";
1947
+
1684
1948
  // src/redact.ts
1685
1949
  import { DEFAULT_REDACTION_RULES, redactValue } from "@tangle-network/agent-eval/traces";
1686
1950
  var CODING_REDACTION_RULES = [
@@ -1759,42 +2023,19 @@ import { hostedClientFromEnv } from "@tangle-network/agent-eval/hosted";
1759
2023
  import { REDACTION_VERSION } from "@tangle-network/agent-eval/traces";
1760
2024
  var require2 = createRequire(import.meta.url);
1761
2025
  var TRACES_VERSION = require2("../package.json").version;
1762
- function adaptersFor2(opts) {
1763
- if (opts.all) return [...listAdapters()];
1764
- const a = resolveAdapter(opts.harness ?? "claude-code");
1765
- if (!a) throw new Error(`unknown harness "${opts.harness}"`);
1766
- return [a];
1767
- }
1768
- async function planUpload(opts) {
2026
+ async function planUpload(opts = {}) {
1769
2027
  const state = await loadState();
1770
2028
  const items = [];
1771
- for (const adapter of adaptersFor2(opts)) {
1772
- let refs;
1773
- try {
1774
- refs = await adapter.locate({ cwd: opts.cwd, sinceMs: opts.sinceMs });
1775
- } catch (err) {
1776
- opts.onError?.(err);
1777
- continue;
1778
- }
1779
- for (const ref of refs) {
1780
- let raw;
1781
- try {
1782
- raw = await adapter.parse(ref);
1783
- } catch (err) {
1784
- opts.onError?.(err, ref);
1785
- continue;
1786
- }
1787
- if (raw.length === 0) continue;
1788
- const { spans, report } = redactSpans(raw);
1789
- const hash = sessionHash(spans);
1790
- items.push({
1791
- ref,
1792
- spans,
1793
- redaction: report,
1794
- hash,
1795
- isNew: !alreadyUploaded(state, ref.harness, ref.sessionId, hash)
1796
- });
1797
- }
2029
+ for await (const { ref, spans: raw } of scanSessions(opts)) {
2030
+ const { spans, report } = redactSpans(raw);
2031
+ const hash = sessionHash(spans);
2032
+ items.push({
2033
+ ref,
2034
+ spans,
2035
+ redaction: report,
2036
+ hash,
2037
+ isNew: !alreadyUploaded(state, ref.harness, ref.sessionId, hash)
2038
+ });
1798
2039
  }
1799
2040
  return { items, state, sinceMs: opts.sinceMs };
1800
2041
  }
@@ -1810,26 +2051,20 @@ async function gitBranch(cwd) {
1810
2051
  }
1811
2052
  async function sessionMeta(item, uploadedAt) {
1812
2053
  const meta = {
1813
- "tangle.harness": item.ref.harness,
1814
- "tangle.source": basename8(item.ref.path),
1815
- "tangle.host": hostname(),
1816
- "tangle.uploaded_at": uploadedAt,
1817
- "tangle.uploader": `tangle-traces@${TRACES_VERSION}`,
1818
- "redaction.version": REDACTION_VERSION,
1819
- "redaction.count": item.redaction.redactionCount
2054
+ [ATTR.HARNESS]: item.ref.harness,
2055
+ [ATTR.SESSION_FILE]: basename8(item.ref.path),
2056
+ [ATTR.HOST]: hostname(),
2057
+ [ATTR.UPLOADED_AT]: uploadedAt,
2058
+ [ATTR.UPLOADER]: `tangle-traces@${TRACES_VERSION}`,
2059
+ [ATTR.REDACTION_VERSION]: REDACTION_VERSION,
2060
+ [ATTR.REDACTION_COUNT]: item.redaction.redactionCount
1820
2061
  };
1821
- if (item.ref.cwd) meta["tangle.cwd"] = item.ref.cwd;
2062
+ if (item.ref.cwd) meta[ATTR.CWD] = item.ref.cwd;
1822
2063
  const branch = await gitBranch(item.ref.cwd);
1823
- if (branch) meta["tangle.git_branch"] = branch;
2064
+ if (branch) meta[ATTR.GIT_BRANCH] = branch;
1824
2065
  return meta;
1825
2066
  }
1826
- function epochMs(ts) {
1827
- if (!ts) return 0;
1828
- if (/^\d+$/.test(ts)) return Number(ts);
1829
- const n = Date.parse(ts);
1830
- return Number.isNaN(n) ? 0 : n;
1831
- }
1832
- var msToNano = (iso2) => epochMs(iso2) * 1e6;
2067
+ var msToNano = (iso2) => parseIsoToEpochMs(iso2) * 1e6;
1833
2068
  function toTraceSpanEvents(spans, rootMeta) {
1834
2069
  return spans.map((s) => {
1835
2070
  const attributes = {};
@@ -1837,8 +2072,8 @@ function toTraceSpanEvents(spans, rootMeta) {
1837
2072
  if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") attributes[k] = v;
1838
2073
  else if (v != null) attributes[k] = JSON.stringify(v);
1839
2074
  }
1840
- attributes["tangle.sessionId"] = s.trace_id;
1841
- attributes["tangle.ingest_source"] = "cli";
2075
+ attributes[ATTR.SESSION_ID] = s.trace_id;
2076
+ attributes[ATTR.INGEST_SOURCE] = INGEST_SOURCE_CLI;
1842
2077
  if (s.parent_span_id === null) Object.assign(attributes, rootMeta);
1843
2078
  const startNano = msToNano(s.start_time);
1844
2079
  return {
@@ -1907,6 +2142,8 @@ export {
1907
2142
  serializeSpans,
1908
2143
  writeOtlpFile,
1909
2144
  analyzeSpans,
2145
+ parseIsoToEpochMs,
2146
+ parseSince,
1910
2147
  toRuntimeStore,
1911
2148
  runPipelines,
1912
2149
  AmpAdapter,
@@ -1922,7 +2159,12 @@ export {
1922
2159
  listAdapters,
1923
2160
  resolveAdapter,
1924
2161
  knownHarnesses,
2162
+ selectAdapters,
2163
+ scanSessions,
1925
2164
  watchSessions,
2165
+ ATTR,
2166
+ INGEST_SOURCE_CLI,
2167
+ DEFAULT_HARNESS,
1926
2168
  CODING_REDACTION_RULES,
1927
2169
  TRACES_REDACTION_RULES,
1928
2170
  redactSpans,
package/dist/cli.js CHANGED
@@ -3,13 +3,14 @@ import {
3
3
  analyzeSpans,
4
4
  executeUpload,
5
5
  knownHarnesses,
6
- listAdapters,
6
+ parseSince,
7
7
  planUpload,
8
8
  resolveAdapter,
9
9
  runPipelines,
10
+ selectAdapters,
10
11
  watchSessions,
11
12
  writeOtlpFile
12
- } from "./chunk-4PC7MQXT.js";
13
+ } from "./chunk-33NBMIH4.js";
13
14
 
14
15
  // src/cli.ts
15
16
  import { stat, writeFile } from "fs/promises";
@@ -163,18 +164,6 @@ function parseArgs(argv) {
163
164
  }
164
165
  return a;
165
166
  }
166
- function parseSince(s) {
167
- if (!s) return Date.now() - 24 * 60 * 60 * 1e3;
168
- const m = s.match(/^(\d+)\s*([mhd])$/i);
169
- if (m) {
170
- const unit = m[2].toLowerCase();
171
- const ms = unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
172
- return Date.now() - Number(m[1]) * ms;
173
- }
174
- const t = Date.parse(s);
175
- if (Number.isNaN(t)) throw new Error(`--since: expected 30m / 2h / 7d or an ISO date, got "${s}"`);
176
- return t;
177
- }
178
167
  async function confirm(question) {
179
168
  if (!process.stdin.isTTY) return false;
180
169
  const rl = (await import("readline/promises")).createInterface({
@@ -189,12 +178,7 @@ async function confirm(question) {
189
178
  }
190
179
  }
191
180
  function adaptersFor(args) {
192
- if (args.all) return [...listAdapters()];
193
- const adapter = resolveAdapter(args.harness);
194
- if (!adapter) {
195
- throw new Error(`unknown harness "${args.harness}". Known: ${knownHarnesses().join(", ")}`);
196
- }
197
- return [adapter];
181
+ return selectAdapters({ all: args.all, harnesses: [args.harness] });
198
182
  }
199
183
  async function discover(args) {
200
184
  const sinceMs = args.since ? Date.parse(args.since) : void 0;
@@ -304,9 +288,14 @@ async function cmdWatch(args) {
304
288
  });
305
289
  }
306
290
  async function cmdUpload(args) {
307
- const sinceMs = parseSince(args.since);
291
+ const sinceMs = args.since ? parseSince(args.since) : Date.now() - 24 * 60 * 60 * 1e3;
308
292
  const all = args.all || !args.harnessExplicit;
309
- const plan = await planUpload({ all, harness: args.harness, cwd: args.cwd, sinceMs });
293
+ const plan = await planUpload({
294
+ all,
295
+ harnesses: args.harnessExplicit ? [args.harness] : void 0,
296
+ cwd: args.cwd,
297
+ sinceMs
298
+ });
310
299
  const newItems = plan.items.filter((i) => i.isNew);
311
300
  const byRule = {};
312
301
  let totalRedactions = 0;
package/dist/index.d.ts CHANGED
@@ -118,6 +118,49 @@ interface HarnessTraceAdapter {
118
118
  parse(ref: SessionRef): Promise<OtlpSpan[]>;
119
119
  }
120
120
 
121
+ /**
122
+ * Span-attribute keys the upload path stamps. The CLI is the producer; the
123
+ * Tangle Intelligence Platform is the consumer.
124
+ *
125
+ * `SESSION_ID` and `INGEST_SOURCE` are a cross-repo WIRE CONTRACT — the platform
126
+ * keys its cross-source dedup on them (`session.id`/`tangle.sessionId` linkage +
127
+ * `tangle.ingest_source === "cli"`). Do NOT change their string VALUES without a
128
+ * coordinated platform release. The rest are stored-but-not-parsed metadata,
129
+ * free to evolve.
130
+ *
131
+ * `SESSION_ID` stays camelCase because it must match agent-eval's
132
+ * `parseConversationLinkage` key list; the metadata keys are snake_case.
133
+ */
134
+ declare const ATTR: {
135
+ /** = the trace/session id. Wire contract (dedup linkage). */
136
+ readonly SESSION_ID: "tangle.sessionId";
137
+ /** "cli" marks a CLI upload so the platform can drop a re-upload of a session
138
+ * that also streamed live. Wire contract. */
139
+ readonly INGEST_SOURCE: "tangle.ingest_source";
140
+ readonly HARNESS: "tangle.harness";
141
+ readonly CWD: "tangle.cwd";
142
+ readonly GIT_BRANCH: "tangle.git_branch";
143
+ readonly HOST: "tangle.host";
144
+ /** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
145
+ * which collided with `tangle.ingest_source`.) */
146
+ readonly SESSION_FILE: "tangle.session_file";
147
+ readonly UPLOADED_AT: "tangle.uploaded_at";
148
+ readonly UPLOADER: "tangle.uploader";
149
+ readonly REDACTION_VERSION: "redaction.version";
150
+ readonly REDACTION_COUNT: "redaction.count";
151
+ };
152
+ /** `tangle.ingest_source` value for CLI-uploaded traces. Wire contract. */
153
+ declare const INGEST_SOURCE_CLI = "cli";
154
+ /** Harness used when none is specified on a single-harness command. */
155
+ declare const DEFAULT_HARNESS = "claude-code";
156
+
157
+ /** Parse an ISO-8601 timestamp or an epoch-millis string to epoch ms.
158
+ * Returns 0 for empty/unparseable input (callers treat 0 as "unknown"). */
159
+ declare function parseIsoToEpochMs(ts: string): number;
160
+ /** Parse a `--since` window: `30m` / `2h` / `7d` (relative to now) or an ISO
161
+ * date, → epoch ms. Throws on a malformed value (fail-loud, not silent). */
162
+ declare function parseSince(value: string): number;
163
+
121
164
  /**
122
165
  * Harness adapter registry — resolves a harness id (or alias) to the
123
166
  * adapter that knows its on-disk session format.
@@ -134,6 +177,41 @@ interface HarnessTraceAdapter {
134
177
  declare function listAdapters(): readonly HarnessTraceAdapter[];
135
178
  declare function resolveAdapter(harness: string): HarnessTraceAdapter | undefined;
136
179
  declare function knownHarnesses(): string[];
180
+ /** Options for selecting which adapters a command/SDK call operates on. */
181
+ interface AdapterSelection {
182
+ /** Use these adapters verbatim (e.g. a caller's own harness) — wins over all. */
183
+ adapters?: readonly HarnessTraceAdapter[];
184
+ /** Every known harness. Implied when no `adapters`/`harnesses` are given. */
185
+ all?: boolean;
186
+ /** Specific harness ids/aliases. Unknown ids throw (fail-loud). */
187
+ harnesses?: readonly string[];
188
+ }
189
+ /** Resolve a selection to the adapters to operate on. Precedence:
190
+ * explicit `adapters` → named `harnesses` → everything (`all` or unspecified).
191
+ * A named harness that doesn't resolve throws rather than silently dropping. */
192
+ declare function selectAdapters(opts: AdapterSelection): HarnessTraceAdapter[];
193
+
194
+ interface ScanOptions extends AdapterSelection {
195
+ /** Filter by working directory (exact/prefix). */
196
+ cwd?: string;
197
+ /** Only sessions modified at/after this epoch ms. */
198
+ sinceMs?: number;
199
+ /** Cap to the most-recent N sessions per harness. */
200
+ last?: number;
201
+ /** Cancel the scan; iteration stops between sessions. */
202
+ signal?: AbortSignal;
203
+ /** Per-adapter locate/parse failure (the scan continues). */
204
+ onError?: (error: unknown, ref?: SessionRef) => void;
205
+ }
206
+ interface ScannedSession {
207
+ adapter: HarnessTraceAdapter;
208
+ ref: SessionRef;
209
+ /** Non-empty parsed spans for the session. */
210
+ spans: OtlpSpan[];
211
+ }
212
+ /** Yield every non-empty session across the selected adapters. Locate/parse
213
+ * errors route to `onError` and skip that adapter/session, never aborting. */
214
+ declare function scanSessions(opts: ScanOptions): AsyncGenerator<ScannedSession>;
137
215
 
138
216
  /**
139
217
  * Claude Code adapter — `~/.claude/projects/<encoded-cwd>/<session>.jsonl`.
@@ -537,22 +615,9 @@ declare function redactSpans(spans: readonly OtlpSpan[], rules?: RedactionRule[]
537
615
  * Redaction is ON by default — the safe posture when traces leave the box.
538
616
  */
539
617
 
540
- interface CollectOptions {
541
- /** Harness ids/aliases. Omit (or `all: true`) → every harness. */
542
- harnesses?: string[];
543
- all?: boolean;
544
- /** Collect from these adapters instead of the built-in registry. */
545
- adapters?: HarnessTraceAdapter[];
546
- /** Filter by working directory (exact/prefix). */
547
- cwd?: string;
548
- /** Only sessions modified at/after this epoch ms. */
549
- sinceMs?: number;
550
- /** Cap to the most-recent N sessions per harness. */
551
- last?: number;
618
+ interface CollectOptions extends ScanOptions {
552
619
  /** Redact PII/secrets (default true). Set false to get raw spans. */
553
620
  redact?: boolean;
554
- /** Called on a per-adapter locate/parse failure (collection continues). */
555
- onError?: (error: unknown, ref?: SessionRef) => void;
556
621
  }
557
622
  interface SessionBatch {
558
623
  ref: SessionRef;
@@ -617,16 +682,9 @@ interface UploadPlan {
617
682
  state: UploadState;
618
683
  sinceMs?: number;
619
684
  }
620
- interface PlanOptions {
621
- harness?: string;
622
- all?: boolean;
623
- cwd?: string;
624
- sinceMs?: number;
625
- /** Called on a per-adapter locate/parse failure (planning continues). */
626
- onError?: (error: unknown, ref?: SessionRef) => void;
627
- }
685
+ type PlanOptions = ScanOptions;
628
686
  /** Read-only: select sessions in the window, redact, and mark which are new. */
629
- declare function planUpload(opts: PlanOptions): Promise<UploadPlan>;
687
+ declare function planUpload(opts?: PlanOptions): Promise<UploadPlan>;
630
688
  /** Map redacted OtlpSpan[] → the hosted TraceSpanEvent[] wire shape, attaching
631
689
  * session metadata to the root span(s). */
632
690
  declare function toTraceSpanEvents(spans: readonly OtlpSpan[], rootMeta: Record<string, string | number | boolean>): TraceSpanEvent[];
@@ -656,4 +714,4 @@ interface ExecuteOptions {
656
714
  /** Send the NEW items (or, on dryRun, write the redacted OTLP that would send). */
657
715
  declare function executeUpload(plan: UploadPlan, opts?: ExecuteOptions): Promise<UploadResult>;
658
716
 
659
- export { AmpAdapter, type AnalyzeOptions, type AnalyzeResult, CODING_REDACTION_RULES, ClaudeAdapter, CodexAdapter, type CollectOptions, CopilotAdapter, type ExecuteOptions, FactoryAdapter, ForgeAdapter, GeminiAdapter, type HarnessTraceAdapter, type LocateOptions, type ObservedLoop, type ObserverOptions, OpencodeAdapter, type OtlpSpan, type OtlpSpanKind, type OtlpStatusCode, PiAdapter, type PipelineOptions, type PipelineReport, type PlanOptions, QwenAdapter, type RuntimeTrace, type SessionBatch, type SessionRef, type SpanInput, type SpanRedaction, TRACES_REDACTION_RULES, type UploadBackend, type UploadItem, type UploadPlan, type UploadRecord, type UploadResult, type UploadState, alreadyUploaded, analyzeSpans, collectSessions, executeUpload, knownHarnesses, listAdapters, loadState, planUpload, redactSpans, resolveAdapter, runPipelines, saveState, serializeSpans, sessionHash, span, toRuntimeStore, toTraceSpanEvents, uploadKey, watchSessions, writeOtlpFile };
717
+ export { ATTR, AmpAdapter, type AnalyzeOptions, type AnalyzeResult, CODING_REDACTION_RULES, ClaudeAdapter, CodexAdapter, type CollectOptions, CopilotAdapter, DEFAULT_HARNESS, type ExecuteOptions, FactoryAdapter, ForgeAdapter, GeminiAdapter, type HarnessTraceAdapter, INGEST_SOURCE_CLI, type LocateOptions, type ObservedLoop, type ObserverOptions, OpencodeAdapter, type OtlpSpan, type OtlpSpanKind, type OtlpStatusCode, PiAdapter, type PipelineOptions, type PipelineReport, type PlanOptions, QwenAdapter, type RuntimeTrace, type ScanOptions, type ScannedSession, type SessionBatch, type SessionRef, type SpanInput, type SpanRedaction, TRACES_REDACTION_RULES, type UploadBackend, type UploadItem, type UploadPlan, type UploadRecord, type UploadResult, type UploadState, alreadyUploaded, analyzeSpans, collectSessions, executeUpload, knownHarnesses, listAdapters, loadState, parseIsoToEpochMs, parseSince, planUpload, redactSpans, resolveAdapter, runPipelines, saveState, scanSessions, selectAdapters, serializeSpans, sessionHash, span, toRuntimeStore, toTraceSpanEvents, uploadKey, watchSessions, writeOtlpFile };
package/dist/index.js CHANGED
@@ -1,12 +1,15 @@
1
1
  import {
2
+ ATTR,
2
3
  AmpAdapter,
3
4
  CODING_REDACTION_RULES,
4
5
  ClaudeAdapter,
5
6
  CodexAdapter,
6
7
  CopilotAdapter,
8
+ DEFAULT_HARNESS,
7
9
  FactoryAdapter,
8
10
  ForgeAdapter,
9
11
  GeminiAdapter,
12
+ INGEST_SOURCE_CLI,
10
13
  OpencodeAdapter,
11
14
  PiAdapter,
12
15
  QwenAdapter,
@@ -17,11 +20,15 @@ import {
17
20
  knownHarnesses,
18
21
  listAdapters,
19
22
  loadState,
23
+ parseIsoToEpochMs,
24
+ parseSince,
20
25
  planUpload,
21
26
  redactSpans,
22
27
  resolveAdapter,
23
28
  runPipelines,
24
29
  saveState,
30
+ scanSessions,
31
+ selectAdapters,
25
32
  serializeSpans,
26
33
  sessionHash,
27
34
  span,
@@ -30,45 +37,17 @@ import {
30
37
  uploadKey,
31
38
  watchSessions,
32
39
  writeOtlpFile
33
- } from "./chunk-4PC7MQXT.js";
40
+ } from "./chunk-33NBMIH4.js";
34
41
 
35
42
  // src/collect.ts
36
- function adaptersFor(opts) {
37
- if (opts.adapters && opts.adapters.length > 0) return opts.adapters;
38
- if (opts.all || !opts.harnesses || opts.harnesses.length === 0) return [...listAdapters()];
39
- const out = [];
40
- for (const h of opts.harnesses) {
41
- const a = resolveAdapter(h);
42
- if (a && !out.includes(a)) out.push(a);
43
- }
44
- return out;
45
- }
46
43
  async function collectSessions(opts = {}) {
47
44
  const out = [];
48
- for (const adapter of adaptersFor(opts)) {
49
- let refs;
50
- try {
51
- refs = await adapter.locate({ cwd: opts.cwd, sinceMs: opts.sinceMs });
52
- } catch (err) {
53
- opts.onError?.(err);
54
- continue;
55
- }
56
- if (opts.last && opts.last > 0) refs = refs.slice(0, opts.last);
57
- for (const ref of refs) {
58
- let spans;
59
- try {
60
- spans = await adapter.parse(ref);
61
- } catch (err) {
62
- opts.onError?.(err, ref);
63
- continue;
64
- }
65
- if (spans.length === 0) continue;
66
- if (opts.redact === false) {
67
- out.push({ ref, spans });
68
- } else {
69
- const r = redactSpans(spans);
70
- out.push({ ref, spans: r.spans, redaction: r.report });
71
- }
45
+ for await (const { ref, spans } of scanSessions(opts)) {
46
+ if (opts.redact === false) {
47
+ out.push({ ref, spans });
48
+ } else {
49
+ const r = redactSpans(spans);
50
+ out.push({ ref, spans: r.spans, redaction: r.report });
72
51
  }
73
52
  }
74
53
  return out;
@@ -83,16 +62,19 @@ import {
83
62
  import { createHostedClient, hostedClientFromEnv } from "@tangle-network/agent-eval/hosted";
84
63
  import { DEFAULT_REDACTION_RULES, redactString, redactValue } from "@tangle-network/agent-eval/traces";
85
64
  export {
65
+ ATTR,
86
66
  AmpAdapter,
87
67
  AnalystRegistry,
88
68
  CODING_REDACTION_RULES,
89
69
  ClaudeAdapter,
90
70
  CodexAdapter,
91
71
  CopilotAdapter,
72
+ DEFAULT_HARNESS,
92
73
  DEFAULT_REDACTION_RULES,
93
74
  FactoryAdapter,
94
75
  ForgeAdapter,
95
76
  GeminiAdapter,
77
+ INGEST_SOURCE_CLI,
96
78
  OpencodeAdapter,
97
79
  PiAdapter,
98
80
  QwenAdapter,
@@ -108,6 +90,8 @@ export {
108
90
  listAdapters,
109
91
  loadState,
110
92
  makeFinding,
93
+ parseIsoToEpochMs,
94
+ parseSince,
111
95
  planUpload,
112
96
  redactSpans,
113
97
  redactString,
@@ -115,6 +99,8 @@ export {
115
99
  resolveAdapter,
116
100
  runPipelines,
117
101
  saveState,
102
+ scanSessions,
103
+ selectAdapters,
118
104
  serializeSpans,
119
105
  sessionHash,
120
106
  span,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/traces",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "Point it at your coding-agent session traces (Claude Code, Codex, OpenCode, Gemini, Pi, …) and get failure-mode + efficiency findings. CLI + SDK over the @tangle-network/agent-eval analyst suite — observe live sessions, run your own analysts, redact, and upload to the Tangle Intelligence Platform.",
5
5
  "type": "module",
6
6
  "license": "MIT",