@tangle-network/traces 0.9.6 → 0.9.7

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.
@@ -923,7 +923,14 @@ var CORRECTIVE_REACTIONS = ["correction", "frustration", "jargon", "structure"];
923
923
  import { createHash as createHash4 } from "crypto";
924
924
  import { readdir as readdir3, stat as stat2 } from "fs/promises";
925
925
  import { homedir as homedir2 } from "os";
926
- import { basename as basename2, join as join2 } from "path";
926
+ import { basename as basename2, join as join2, relative } from "path";
927
+ import {
928
+ applyLlmSpanOtlpAttributes as applyLlmSpanOtlpAttributes2,
929
+ LLM_CACHED_TOKENS,
930
+ LLM_CACHE_WRITE_TOKENS,
931
+ LLM_INPUT_TOKENS,
932
+ LLM_OUTPUT_TOKENS
933
+ } from "@tangle-network/agent-eval/trace-attributes";
927
934
  var SERVICE2 = "claude-code";
928
935
  var EPOCH = (/* @__PURE__ */ new Date(0)).toISOString();
929
936
  function asBlocks(content) {
@@ -943,7 +950,15 @@ function stringifyToolResult(content) {
943
950
  return "";
944
951
  }
945
952
  function createClaudeStream(startStep) {
946
- return { spans: [], toolSpanByUseId: /* @__PURE__ */ new Map(), step: startStep, sawUserTurn: false };
953
+ return {
954
+ spans: [],
955
+ toolSpanByUseId: /* @__PURE__ */ new Map(),
956
+ llmSpanByMessageId: /* @__PURE__ */ new Map(),
957
+ llmContentByMessageId: /* @__PURE__ */ new Map(),
958
+ toolCountByMessageId: /* @__PURE__ */ new Map(),
959
+ step: startStep,
960
+ sawUserTurn: false
961
+ };
947
962
  }
948
963
  var ClaudeEventConflictError = class extends Error {
949
964
  sourcePath;
@@ -974,6 +989,7 @@ function projectClaudeEvent(event) {
974
989
  return {
975
990
  kind: "assistant",
976
991
  timestamp,
992
+ messageId: event.message.id ?? null,
977
993
  model: event.message.model ?? null,
978
994
  inputTokens: event.message.usage?.input_tokens ?? null,
979
995
  outputTokens: event.message.usage?.output_tokens ?? null,
@@ -1027,16 +1043,17 @@ function consumeDistinctClaudeEvent(event, ctx, state, seen, sourcePath) {
1027
1043
  }
1028
1044
  seen.set(event.uuid, fingerprint);
1029
1045
  }
1030
- consumeClaudeEvent(projection, event.uuid ?? `${ctx.idPrefix}step${state.step}`, ctx, state);
1046
+ consumeClaudeEvent(projection, event.uuid ?? `step${state.step}`, ctx, state);
1031
1047
  return true;
1032
1048
  }
1033
1049
  function consumeClaudeEvent(event, uid, ctx, state) {
1034
1050
  if (event.kind === "assistant") {
1035
- const llmId = `${ctx.idPrefix}${uid}`;
1036
- state.spans.push(
1037
- span({
1051
+ const messageId = event.messageId ?? uid;
1052
+ let llmSpan = state.llmSpanByMessageId.get(messageId);
1053
+ if (!llmSpan) {
1054
+ llmSpan = span({
1038
1055
  traceId: ctx.traceId,
1039
- spanId: llmId,
1056
+ spanId: `${ctx.idPrefix}${uid}`,
1040
1057
  parentSpanId: ctx.rootParent,
1041
1058
  name: "llm.turn",
1042
1059
  kind: "LLM",
@@ -1048,17 +1065,46 @@ function consumeClaudeEvent(event, uid, ctx, state) {
1048
1065
  outputTokens: event.outputTokens,
1049
1066
  cachedInputTokens: event.cachedInputTokens,
1050
1067
  cacheWriteInputTokens: event.cacheWriteInputTokens,
1051
- step: state.step,
1052
- content: event.content
1053
- })
1054
- );
1055
- state.step += 1;
1056
- let toolIdx = 0;
1068
+ step: state.step
1069
+ });
1070
+ state.spans.push(llmSpan);
1071
+ state.llmSpanByMessageId.set(messageId, llmSpan);
1072
+ state.step += 1;
1073
+ } else {
1074
+ includeSpanTimestamp(llmSpan, event.timestamp);
1075
+ }
1076
+ applyLlmSpanOtlpAttributes2(llmSpan.attributes, {
1077
+ model: event.model ?? void 0,
1078
+ inputTokens: maxCapturedUsage(
1079
+ llmSpan.attributes[LLM_INPUT_TOKENS],
1080
+ event.inputTokens
1081
+ ),
1082
+ outputTokens: maxCapturedUsage(
1083
+ llmSpan.attributes[LLM_OUTPUT_TOKENS],
1084
+ event.outputTokens
1085
+ ),
1086
+ cachedTokens: maxCapturedUsage(
1087
+ llmSpan.attributes[LLM_CACHED_TOKENS],
1088
+ event.cachedInputTokens
1089
+ ),
1090
+ cacheWriteTokens: maxCapturedUsage(
1091
+ llmSpan.attributes[LLM_CACHE_WRITE_TOKENS],
1092
+ event.cacheWriteInputTokens
1093
+ )
1094
+ });
1095
+ mergeMessageContent(llmSpan, messageId, event.content, state);
1057
1096
  for (const tool of event.tools) {
1097
+ const existingTool = tool.id ? state.toolSpanByUseId.get(tool.id) : void 0;
1098
+ if (existingTool) {
1099
+ Object.assign(existingTool.attributes, tool.attributes);
1100
+ includeSpanTimestamp(existingTool, event.timestamp);
1101
+ continue;
1102
+ }
1103
+ const toolIdx = state.toolCountByMessageId.get(messageId) ?? 0;
1058
1104
  const toolSpan = span({
1059
1105
  traceId: ctx.traceId,
1060
- spanId: `${ctx.idPrefix}${uid}:tool:${toolIdx}`,
1061
- parentSpanId: llmId,
1106
+ spanId: `${llmSpan.span_id}:tool:${toolIdx}`,
1107
+ parentSpanId: llmSpan.span_id,
1062
1108
  name: `tool.${tool.name}`,
1063
1109
  kind: "TOOL",
1064
1110
  startTime: event.timestamp,
@@ -1070,7 +1116,7 @@ function consumeClaudeEvent(event, uid, ctx, state) {
1070
1116
  });
1071
1117
  state.spans.push(toolSpan);
1072
1118
  if (tool.id) state.toolSpanByUseId.set(tool.id, toolSpan);
1073
- toolIdx += 1;
1119
+ state.toolCountByMessageId.set(messageId, toolIdx + 1);
1074
1120
  state.step += 1;
1075
1121
  }
1076
1122
  } else if (event.kind === "user") {
@@ -1104,6 +1150,28 @@ function consumeClaudeEvent(event, uid, ctx, state) {
1104
1150
  backfillResult(state.toolSpanByUseId.get(event.result.toolUseId), event.timestamp, event.result);
1105
1151
  }
1106
1152
  }
1153
+ function maxCapturedUsage(current, next) {
1154
+ const captured = typeof current === "number" && Number.isFinite(current) ? current : void 0;
1155
+ if (next == null) return captured;
1156
+ return captured === void 0 ? next : Math.max(captured, next);
1157
+ }
1158
+ function includeSpanTimestamp(target, timestamp) {
1159
+ if (!timestamp) return;
1160
+ const time = Date.parse(timestamp);
1161
+ if (!Number.isFinite(time)) return;
1162
+ const start = Date.parse(target.start_time);
1163
+ const end = Date.parse(target.end_time);
1164
+ if (!Number.isFinite(start) || time < start) target.start_time = timestamp;
1165
+ if (!Number.isFinite(end) || time > end) target.end_time = timestamp;
1166
+ }
1167
+ function mergeMessageContent(llmSpan, messageId, content, state) {
1168
+ if (!content) return;
1169
+ const parts = state.llmContentByMessageId.get(messageId) ?? /* @__PURE__ */ new Set();
1170
+ if (parts.has(content)) return;
1171
+ parts.add(content);
1172
+ state.llmContentByMessageId.set(messageId, parts);
1173
+ llmSpan.attributes.content = capText([...parts].join("\n"));
1174
+ }
1107
1175
  function finishClaudeStream(state) {
1108
1176
  return { spans: state.spans, toolSpanByUseId: state.toolSpanByUseId, nextStep: state.step };
1109
1177
  }
@@ -1131,6 +1199,30 @@ function backfillResult(s, endTime, result) {
1131
1199
  Object.assign(s.attributes, result.attributes);
1132
1200
  if (result.isError && result.message) s.status.message = result.message;
1133
1201
  }
1202
+ async function listSubagentFiles(root) {
1203
+ const pending = [root];
1204
+ const files = [];
1205
+ while (pending.length > 0) {
1206
+ const dir = pending.pop();
1207
+ if (!dir) continue;
1208
+ let entries;
1209
+ try {
1210
+ entries = await readdir3(dir, { withFileTypes: true });
1211
+ } catch (error) {
1212
+ if (isMissingPathError(error)) continue;
1213
+ throw error;
1214
+ }
1215
+ for (const entry of entries) {
1216
+ const path = join2(dir, entry.name);
1217
+ if (entry.isDirectory()) {
1218
+ pending.push(path);
1219
+ } else if (entry.isFile() && /^agent-.*\.jsonl$/.test(entry.name)) {
1220
+ files.push(path);
1221
+ }
1222
+ }
1223
+ }
1224
+ return files.sort();
1225
+ }
1134
1226
  var ClaudeAdapter = class {
1135
1227
  harness = "claude-code";
1136
1228
  aliases = ["claude", "claudish", "openclaw", "nanoclaw"];
@@ -1215,47 +1307,59 @@ var ClaudeAdapter = class {
1215
1307
  setRootTimeBounds(root, spans);
1216
1308
  return spans;
1217
1309
  }
1218
- /** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
1310
+ /** Parse nested `subagents/.../agent-*.jsonl`, parenting each under its Agent call. */
1219
1311
  async foldSubagents(ref, traceId, main, out, options) {
1220
1312
  const subDir = join2(ref.path.replace(/\.jsonl$/, ""), "subagents");
1221
- let files;
1222
- try {
1223
- files = await readdir3(subDir);
1224
- } catch (error) {
1225
- if (isMissingPathError(error)) return;
1226
- throw error;
1227
- }
1313
+ const files = await listSubagentFiles(subDir);
1228
1314
  let step = main.nextStep;
1315
+ const parsedAgents = [];
1229
1316
  for (const file of files) {
1230
- if (!file.endsWith(".jsonl")) continue;
1231
- const hash2 = basename2(file, ".jsonl");
1232
- const metaPath = join2(subDir, `${hash2}.meta.json`);
1317
+ const sourceKey = relative(subDir, file).replace(/\.jsonl$/, "").split(/[\\/]/).map((segment) => encodeURIComponent(segment)).join(":");
1318
+ const metaPath = file.replace(/\.jsonl$/, ".meta.json");
1233
1319
  let meta = {};
1234
1320
  try {
1235
1321
  meta = await readJsonFile(metaPath);
1236
1322
  } catch (error) {
1237
1323
  if (!isMissingJsonSource(error)) throw error;
1238
1324
  }
1239
- const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
1240
1325
  const ctx = {
1241
1326
  traceId,
1242
1327
  agent: meta.agentType ? `subagent:${meta.agentType}` : "subagent",
1243
1328
  startStep: step,
1244
- idPrefix: `${hash2}:`,
1245
- rootParent: parent
1329
+ idPrefix: `${sourceKey}:`,
1330
+ rootParent: `root:${traceId}`
1246
1331
  };
1247
1332
  const state = createClaudeStream(ctx.startStep);
1248
1333
  const seen = /* @__PURE__ */ new Map();
1249
1334
  for await (const event of readJsonl(
1250
- join2(subDir, file),
1335
+ file,
1251
1336
  sessionJsonlOptions(ref, options)
1252
1337
  )) {
1253
- consumeDistinctClaudeEvent(event, ctx, state, seen, join2(subDir, file));
1338
+ consumeDistinctClaudeEvent(event, ctx, state, seen, file);
1254
1339
  }
1255
1340
  const parsed = finishClaudeStream(state);
1256
- appendAll(out, parsed.spans);
1341
+ parsedAgents.push({
1342
+ agentId: basename2(file, ".jsonl").replace(/^agent-/, ""),
1343
+ meta,
1344
+ parsed
1345
+ });
1257
1346
  step = parsed.nextStep;
1258
1347
  }
1348
+ const byAgentId = /* @__PURE__ */ new Map();
1349
+ for (const agent of parsedAgents) {
1350
+ if (byAgentId.has(agent.agentId)) {
1351
+ throw new Error(`Duplicate Claude subagent id: ${agent.agentId}`);
1352
+ }
1353
+ byAgentId.set(agent.agentId, agent.parsed);
1354
+ }
1355
+ for (const agent of parsedAgents) {
1356
+ const parentTools = agent.meta.parentAgentId ? byAgentId.get(agent.meta.parentAgentId)?.toolSpanByUseId : main.toolSpanByUseId;
1357
+ const parent = agent.meta.toolUseId && parentTools?.get(agent.meta.toolUseId)?.span_id || `root:${traceId}`;
1358
+ for (const item of agent.parsed.spans) {
1359
+ if (item.parent_span_id === `root:${traceId}`) item.parent_span_id = parent;
1360
+ }
1361
+ appendAll(out, agent.parsed.spans);
1362
+ }
1259
1363
  }
1260
1364
  };
1261
1365
 
@@ -3574,15 +3678,15 @@ import { join as join13 } from "path";
3574
3678
  import {
3575
3679
  firstNumberAttr,
3576
3680
  LLM_CACHED_TOKEN_ATTR_KEYS,
3577
- LLM_CACHED_TOKENS,
3681
+ LLM_CACHED_TOKENS as LLM_CACHED_TOKENS2,
3578
3682
  LLM_CACHE_WRITE_TOKEN_ATTR_KEYS,
3579
- LLM_CACHE_WRITE_TOKENS,
3683
+ LLM_CACHE_WRITE_TOKENS as LLM_CACHE_WRITE_TOKENS2,
3580
3684
  LLM_COST_ATTR_KEYS,
3581
3685
  LLM_COST_USD,
3582
3686
  LLM_INPUT_TOKEN_ATTR_KEYS,
3583
- LLM_INPUT_TOKENS,
3687
+ LLM_INPUT_TOKENS as LLM_INPUT_TOKENS2,
3584
3688
  LLM_OUTPUT_TOKEN_ATTR_KEYS,
3585
- LLM_OUTPUT_TOKENS,
3689
+ LLM_OUTPUT_TOKENS as LLM_OUTPUT_TOKENS2,
3586
3690
  LLM_REASONING_TOKEN_ATTR_KEYS,
3587
3691
  LLM_REASONING_TOKENS
3588
3692
  } from "@tangle-network/agent-eval/trace-attributes";
@@ -3971,11 +4075,11 @@ var COMPUTED_SPAN_ATTRIBUTES = /* @__PURE__ */ new Set([
3971
4075
  "tool.name",
3972
4076
  "llm.input_tokens",
3973
4077
  "llm.output_tokens",
3974
- LLM_INPUT_TOKENS,
3975
- LLM_OUTPUT_TOKENS,
4078
+ LLM_INPUT_TOKENS2,
4079
+ LLM_OUTPUT_TOKENS2,
3976
4080
  LLM_REASONING_TOKENS,
3977
- LLM_CACHED_TOKENS,
3978
- LLM_CACHE_WRITE_TOKENS,
4081
+ LLM_CACHED_TOKENS2,
4082
+ LLM_CACHE_WRITE_TOKENS2,
3979
4083
  LLM_COST_USD,
3980
4084
  "step",
3981
4085
  "content"
package/dist/cli.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  writePolicyEvidenceFile,
41
41
  writeSessionIndexFile,
42
42
  writeTraceEvidenceExportFile
43
- } from "./chunk-NP3ZIUR2.js";
43
+ } from "./chunk-NHXPTFKC.js";
44
44
 
45
45
  // src/cli.ts
46
46
  import { readFileSync } from "fs";
package/dist/index.d.ts CHANGED
@@ -399,7 +399,7 @@ declare function stampSpanWorkdirRepoAttrs(spans: readonly OtlpSpan[]): Promise<
399
399
  * (token trajectory) and `message.content[]` tool_use blocks; tool results
400
400
  * arrive as `tool_result` blocks in the following user message or as a
401
401
  * `tool_result` attachment. Subagent runs live in a sibling
402
- * `<session>/subagents/agent-*.jsonl` with a `.meta.json` carrying the
402
+ * nested `<session>/subagents/.../agent-*.jsonl` files with a `.meta.json` carrying the
403
403
  * spawning `toolUseId`, so we parent each subagent under its `Agent` call.
404
404
  *
405
405
  * Shared by the claudish / openclaw / nanoclaw forks via aliases — they
@@ -412,7 +412,7 @@ declare class ClaudeAdapter implements HarnessTraceAdapter {
412
412
  private root;
413
413
  locate(opts?: LocateOptions): Promise<SessionRef[]>;
414
414
  parse(ref: SessionRef, options?: ParseOptions): Promise<OtlpSpan[]>;
415
- /** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
415
+ /** Parse nested `subagents/.../agent-*.jsonl`, parenting each under its Agent call. */
416
416
  private foldSubagents;
417
417
  }
418
418
 
package/dist/index.js CHANGED
@@ -117,7 +117,7 @@ import {
117
117
  writeSessionIndexFile,
118
118
  writeTraceEvidenceExportFile,
119
119
  writeTraceImprovementArtifacts
120
- } from "./chunk-NP3ZIUR2.js";
120
+ } from "./chunk-NHXPTFKC.js";
121
121
 
122
122
  // src/collect.ts
123
123
  async function collectSessions(opts = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/traces",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
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",