@tangle-network/traces 0.8.25 → 0.8.27

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.
@@ -22,9 +22,12 @@ function skillNameOf(input) {
22
22
  return typeof v === "string" && v.length > 0 ? v : "?";
23
23
  }
24
24
  function subagentTypeOf(input) {
25
- const v = input.subagent_type;
25
+ const v = input.subagent_type ?? input.agent_type ?? input.type ?? input.name;
26
26
  return typeof v === "string" && v.length > 0 ? v : "?";
27
27
  }
28
+ function isSpawnAgentTool(name) {
29
+ return name === "spawn_agent" || name.endsWith("__spawn_agent");
30
+ }
28
31
  function countSkillRunsJsonl(raw) {
29
32
  const out = {};
30
33
  for (const line of raw.split("\n")) {
@@ -73,6 +76,8 @@ async function analyzeAdoption(spans, opts = {}) {
73
76
  const sessionsWithSubagent = /* @__PURE__ */ new Set();
74
77
  const skillInvocations = {};
75
78
  const subagentSpawns = {};
79
+ const canonicalSubagentSessions = /* @__PURE__ */ new Set();
80
+ const fallbackSubagents = /* @__PURE__ */ new Map();
76
81
  for (const s of spans) {
77
82
  sessions.add(s.trace_id);
78
83
  const tn = toolName(s);
@@ -84,6 +89,18 @@ async function analyzeAdoption(spans, opts = {}) {
84
89
  const type = subagentTypeOf(parseInput(s));
85
90
  subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
86
91
  sessionsWithSubagent.add(s.trace_id);
92
+ canonicalSubagentSessions.add(s.trace_id);
93
+ } else if (tn && isSpawnAgentTool(tn)) {
94
+ const types = fallbackSubagents.get(s.trace_id) ?? [];
95
+ types.push(subagentTypeOf(parseInput(s)));
96
+ fallbackSubagents.set(s.trace_id, types);
97
+ }
98
+ }
99
+ for (const [traceId, types] of fallbackSubagents) {
100
+ if (canonicalSubagentSessions.has(traceId)) continue;
101
+ sessionsWithSubagent.add(traceId);
102
+ for (const type of types) {
103
+ subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
87
104
  }
88
105
  }
89
106
  const { counts: loopDispatchedRuns, filesRead } = await readLoopRuns(opts.cwds ?? []);
@@ -297,7 +314,9 @@ import { computeToolUseMetrics } from "@tangle-network/agent-eval";
297
314
  import { stuckLoopView } from "@tangle-network/agent-eval/pipelines";
298
315
  async function runPipelines(spans, opts = {}) {
299
316
  const { store, runIds } = await toRuntimeStore(spans);
300
- const stuckLoops = await stuckLoopView(store, { minOccurrences: opts.minLoopOccurrences ?? 3 });
317
+ const loopEligible = spans.filter((item) => item.attributes["traces.expected_blocking"] !== true);
318
+ const { store: loopStore } = await toRuntimeStore(loopEligible);
319
+ const stuckLoops = await stuckLoopView(loopStore, { minOccurrences: opts.minLoopOccurrences ?? 3 });
301
320
  const toolUse = await Promise.all(runIds.map((runId) => computeToolUseMetrics(store, runId)));
302
321
  return { stuckLoops, toolUse };
303
322
  }
@@ -469,6 +488,10 @@ var INJECT_MARKERS = [
469
488
  // not a person. (Observed in Claude Code task/loop sessions.)
470
489
  "<task-notification>",
471
490
  "<task-prompt>",
491
+ // Prefix match: Codex adds attributes such as source="goal" to this tag.
492
+ "<codex_internal_context",
493
+ "<subagent_notification>",
494
+ "# AGENTS.md instructions for ",
472
495
  "<local-command-stdout>",
473
496
  "This session is being continued from a previous conversation",
474
497
  // Slash-command skill bodies are expanded into a user turn (no <command-name>
@@ -808,6 +831,33 @@ function outputIsError(output) {
808
831
  const error = /"success"\s*:\s*false|exit(?:_| )code["\s:]+[1-9]|\bcommand failed\b|\bENOENT\b|\berror:/i.test(s);
809
832
  return { error, message: error ? s.slice(0, 500) : "" };
810
833
  }
834
+ function singleNestedToolName(input) {
835
+ if (!input) return null;
836
+ const names = [...input.matchAll(/\btools\.([A-Za-z][A-Za-z0-9_]*)\s*\(/g)].map((match) => match[1]);
837
+ const unique = [...new Set(names)];
838
+ return unique.length === 1 ? unique[0] : null;
839
+ }
840
+ function toolInputToString(input) {
841
+ if (typeof input === "string") return input;
842
+ if (input == null) return void 0;
843
+ return JSON.stringify(input);
844
+ }
845
+ var verificationCommand = /\b(?:pnpm|npm|yarn|bun)\s+(?:run\s+)?(?:test|typecheck|lint|build|check)(?::[A-Za-z0-9:_-]+)?\b|\b(?:vitest|jest|pytest|tsc|biome|eslint|sha256sum|pdfinfo|pdftotext)\b|\bgo\s+test\b|\bcargo\s+(?:test|check|clippy|build)\b|\bgit\s+(?:status|diff|show|merge-tree)\b|\bgh-drew\s+pr\s+(?:view|checks)\b/i;
846
+ function hasReadOnlyCurl(input) {
847
+ if (!/\bcurl\b/i.test(input)) return false;
848
+ const method = input.match(/(?:^|\s)-X(?:=|\s*)([A-Za-z]+)\b/)?.[1] ?? input.match(/(?:^|\s)--request(?:=|\s+)([A-Za-z]+)\b/i)?.[1];
849
+ if (method && !/^(?:GET|HEAD)$/i.test(method)) return false;
850
+ return !/(?:^|\s)-(?:d|F|T)(?:\S*|\s+\S+)|(?:^|\s)--(?:data(?:-ascii|-binary|-raw|-urlencode)?|form(?:-string)?|json|upload-file)(?:=|\s)/i.test(input);
851
+ }
852
+ function classifyNestedTool(name, input) {
853
+ return name === "exec_command" && input && (verificationCommand.test(input) || hasReadOnlyCurl(input)) ? "exec_command.verify" : name;
854
+ }
855
+ function isExpectedBlockingTool(name, input) {
856
+ if (!input) return false;
857
+ if (name === "wait") return /\bcell_id\b["']?\s*:/.test(input);
858
+ if (name === "write_stdin") return /\bsession_id\b["']?\s*:/.test(input);
859
+ return false;
860
+ }
811
861
  async function* walkRollouts(root) {
812
862
  let years;
813
863
  try {
@@ -901,6 +951,7 @@ var CodexAdapter = class {
901
951
  })
902
952
  ];
903
953
  const toolByCallId = /* @__PURE__ */ new Map();
954
+ const subagentByThreadId = /* @__PURE__ */ new Map();
904
955
  let step = 0;
905
956
  let lastLlm = rootId;
906
957
  let sawUserTurn = false;
@@ -929,9 +980,14 @@ var CodexAdapter = class {
929
980
  lastLlm = id;
930
981
  step += 1;
931
982
  }
932
- } else if (l.type === "response_item" && l.payload?.type === "function_call") {
933
- const name = l.payload.name ?? "tool";
983
+ } else if (l.type === "response_item" && (l.payload?.type === "function_call" || l.payload?.type === "custom_tool_call")) {
984
+ const outerName = l.payload.name ?? "tool";
934
985
  const callId = l.payload.call_id ?? `${step}`;
986
+ const input = toolInputToString(
987
+ l.payload.type === "custom_tool_call" ? l.payload.input : l.payload.arguments
988
+ );
989
+ const nestedName = l.payload.type === "custom_tool_call" ? singleNestedToolName(input) : null;
990
+ const name = classifyNestedTool(nestedName ?? outerName, input);
935
991
  const toolSpan = span({
936
992
  traceId,
937
993
  spanId: `tool:${callId}`,
@@ -943,18 +999,68 @@ var CodexAdapter = class {
943
999
  agent: SERVICE3,
944
1000
  tool: name,
945
1001
  step,
946
- content: l.payload.arguments ?? null
1002
+ content: input ?? null,
1003
+ extra: {
1004
+ "traces.codex.call_type": l.payload.type,
1005
+ ...name !== outerName ? { "traces.codex.outer_tool_name": outerName } : {},
1006
+ ...nestedName ? { "traces.codex.nested_tool_name": nestedName } : {},
1007
+ ...isExpectedBlockingTool(name, input) ? { "traces.expected_blocking": true } : {}
1008
+ }
947
1009
  });
948
1010
  spans.push(toolSpan);
949
1011
  toolByCallId.set(callId, toolSpan);
950
1012
  step += 1;
951
- } else if (l.type === "response_item" && l.payload?.type === "function_call_output") {
1013
+ } else if (l.type === "response_item" && (l.payload?.type === "function_call_output" || l.payload?.type === "custom_tool_call_output")) {
952
1014
  const t = toolByCallId.get(l.payload.call_id ?? "");
953
1015
  if (t) {
954
1016
  const { error, message } = outputIsError(l.payload.output);
955
1017
  t.end_time = ts;
956
1018
  t.status = error ? { code: "ERROR", message } : { code: "OK" };
957
1019
  }
1020
+ } else if (l.type === "event_msg" && l.payload?.type === "sub_agent_activity") {
1021
+ const threadId = l.payload.agent_thread_id;
1022
+ const occurredAtMs = l.payload.occurred_at_ms;
1023
+ const eventTime = typeof occurredAtMs === "number" && Number.isFinite(occurredAtMs) ? new Date(occurredAtMs).toISOString() : ts;
1024
+ if (l.payload.kind === "started" && threadId && !subagentByThreadId.has(threadId)) {
1025
+ const agentPath = l.payload.agent_path ?? "subagent";
1026
+ const subagentType = agentPath.split("/").filter(Boolean).at(-1) ?? "subagent";
1027
+ const toolSpan = span({
1028
+ traceId,
1029
+ spanId: `subagent:${threadId}`,
1030
+ parentSpanId: lastLlm,
1031
+ name: "tool.Agent",
1032
+ kind: "TOOL",
1033
+ startTime: eventTime,
1034
+ service: SERVICE3,
1035
+ agent: SERVICE3,
1036
+ tool: "Agent",
1037
+ step,
1038
+ content: JSON.stringify({
1039
+ subagent_type: subagentType,
1040
+ agent_path: agentPath,
1041
+ agent_thread_id: threadId
1042
+ }),
1043
+ extra: {
1044
+ "traces.codex.subagent_path": agentPath,
1045
+ "traces.codex.subagent_thread_id": threadId
1046
+ }
1047
+ });
1048
+ spans.push(toolSpan);
1049
+ subagentByThreadId.set(threadId, toolSpan);
1050
+ step += 1;
1051
+ } else if (l.payload.kind === "completed" && threadId) {
1052
+ const toolSpan = subagentByThreadId.get(threadId);
1053
+ if (toolSpan) {
1054
+ toolSpan.end_time = eventTime;
1055
+ toolSpan.status = { code: "OK" };
1056
+ }
1057
+ } else if (["interrupted", "failed", "timed_out"].includes(l.payload.kind ?? "") && threadId) {
1058
+ const toolSpan = subagentByThreadId.get(threadId);
1059
+ if (toolSpan) {
1060
+ toolSpan.end_time = eventTime;
1061
+ toolSpan.status = { code: "ERROR", message: `subagent ${l.payload.kind}` };
1062
+ }
1063
+ }
958
1064
  } else if (l.type === "response_item" && l.payload?.type === "message" && l.payload.role === "user") {
959
1065
  const prompt = textOf3(l.payload.content);
960
1066
  if (prompt) {
package/dist/cli.js CHANGED
@@ -44,7 +44,7 @@ import {
44
44
  writePolicyEvidenceFile,
45
45
  writeSessionIndexFile,
46
46
  writeTraceEvidenceExportFile
47
- } from "./chunk-SV7GW5LB.js";
47
+ } from "./chunk-EZDMNNW4.js";
48
48
 
49
49
  // src/cli.ts
50
50
  import { readFileSync } from "fs";
package/dist/index.d.ts CHANGED
@@ -314,7 +314,8 @@ declare class ClaudeAdapter implements HarnessTraceAdapter {
314
314
  * Line types: `session_meta` (id + cwd), `turn_context`, `event_msg`
315
315
  * (carries `token_count` with per-turn `last_token_usage`), and
316
316
  * `response_item` (the OpenAI Responses items: `message`, `reasoning`,
317
- * `function_call`, `function_call_output`).
317
+ * function/custom tool calls, and their outputs). Current Codex builds also
318
+ * emit `sub_agent_activity` events for delegated agents.
318
319
  *
319
320
  * Token trajectory comes from the `token_count` deltas (Codex puts usage
320
321
  * on events, not on the message). Tools come from `function_call`, with
@@ -629,7 +630,9 @@ declare const CORRECTIVE_REACTIONS: readonly Reaction[];
629
630
  * error rates above, so adding it would only emit a misleading waste %.
630
631
  *
631
632
  * Both are cheap ($0, deterministic), so they're safe to run continuously in
632
- * `watch` mode, over the OTLP spans traces already produces.
633
+ * `watch` mode, over the OTLP spans traces already produces. Calls explicitly
634
+ * marked as expected blocking stay in usage totals but are excluded from
635
+ * stuck-loop findings.
633
636
  */
634
637
 
635
638
  interface PipelineReport {
package/dist/index.js CHANGED
@@ -99,7 +99,7 @@ import {
99
99
  writeSessionIndexFile,
100
100
  writeTraceEvidenceExportFile,
101
101
  writeTraceImprovementArtifacts
102
- } from "./chunk-SV7GW5LB.js";
102
+ } from "./chunk-EZDMNNW4.js";
103
103
 
104
104
  // src/collect.ts
105
105
  async function collectSessions(opts = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/traces",
3
- "version": "0.8.25",
3
+ "version": "0.8.27",
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",