@tangle-network/traces 0.9.12 → 0.9.13

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
@@ -9,6 +9,7 @@ import {
9
9
  CodexAdapter,
10
10
  CodexExecAdapter,
11
11
  CodexExecStreamError,
12
+ CodexTaskScopeError,
12
13
  CopilotAdapter,
13
14
  DEFAULT_HARNESS,
14
15
  EmptySessionError,
@@ -23,6 +24,7 @@ import {
23
24
  PiAdapter,
24
25
  QwenAdapter,
25
26
  REACTIONS,
27
+ SessionWorkflowError,
26
28
  TRACES_REDACTION_RULES,
27
29
  alreadyUploaded,
28
30
  analyzeAdoption,
@@ -35,25 +37,32 @@ import {
35
37
  buildSessionIndexFromRows,
36
38
  buildTraceFindingPacket,
37
39
  capText,
40
+ chatTrajectoryToSpans,
38
41
  classifyLiveActions,
39
42
  classifyReaction,
40
43
  claudeActor,
41
44
  codexActor,
42
45
  collectPolicyEvidence,
43
46
  collectSessionIndex,
47
+ collectSessionSelection,
48
+ collectSessionWorkflow,
44
49
  commandAnalyzer,
45
50
  commandRedactor,
46
51
  countSkillRunsJsonl,
47
52
  cwdMatchesSelection,
48
53
  defaultTraceLiveAnalysts,
54
+ describeSessionRelationship,
49
55
  equivalentGitCwds,
50
56
  executeUpload,
51
57
  exportTraceEvidenceFile,
52
58
  exportTraceEvidenceRows,
53
59
  exportTraceEvidenceText,
54
60
  haloAnalyzer,
61
+ hodoscopeAnalyzer,
55
62
  indexSessionIdsByTrace,
56
63
  inspectSessionIndex,
64
+ isChatTrajectory,
65
+ isChatTrajectoryMessage,
57
66
  knownHarnesses,
58
67
  listAdapters,
59
68
  loadState,
@@ -112,14 +121,16 @@ import {
112
121
  traceStreamEventsFromSpans,
113
122
  uploadKey,
114
123
  userPromptSpan,
124
+ validateOtlpSpans,
115
125
  watchSessions,
126
+ writeHodoscopeInput,
116
127
  writeInspectionReportFile,
117
128
  writeOtlpFile,
118
129
  writePolicyEvidenceFile,
119
130
  writeSessionIndexFile,
120
131
  writeTraceEvidenceExportFile,
121
132
  writeTraceImprovementArtifacts
122
- } from "./chunk-YDUN3M5J.js";
133
+ } from "./chunk-H47X6QWV.js";
123
134
 
124
135
  // src/collect.ts
125
136
  async function collectSessions(opts = {}) {
@@ -155,6 +166,7 @@ export {
155
166
  CodexAdapter,
156
167
  CodexExecAdapter,
157
168
  CodexExecStreamError,
169
+ CodexTaskScopeError,
158
170
  CopilotAdapter,
159
171
  DEFAULT_HARNESS,
160
172
  DEFAULT_REDACTION_RULES,
@@ -170,6 +182,7 @@ export {
170
182
  PiAdapter,
171
183
  QwenAdapter,
172
184
  REACTIONS,
185
+ SessionWorkflowError,
173
186
  TRACES_REDACTION_RULES,
174
187
  alreadyUploaded,
175
188
  analyzeAdoption,
@@ -183,12 +196,15 @@ export {
183
196
  buildSessionIndexFromRows,
184
197
  buildTraceFindingPacket,
185
198
  capText,
199
+ chatTrajectoryToSpans,
186
200
  classifyLiveActions,
187
201
  classifyReaction,
188
202
  claudeActor,
189
203
  codexActor,
190
204
  collectPolicyEvidence,
191
205
  collectSessionIndex,
206
+ collectSessionSelection,
207
+ collectSessionWorkflow,
192
208
  collectSessions,
193
209
  commandAnalyzer,
194
210
  commandRedactor,
@@ -196,15 +212,19 @@ export {
196
212
  createHostedClient,
197
213
  cwdMatchesSelection,
198
214
  defaultTraceLiveAnalysts,
215
+ describeSessionRelationship,
199
216
  equivalentGitCwds,
200
217
  executeUpload,
201
218
  exportTraceEvidenceFile,
202
219
  exportTraceEvidenceRows,
203
220
  exportTraceEvidenceText,
204
221
  haloAnalyzer,
222
+ hodoscopeAnalyzer,
205
223
  hostedClientFromEnv,
206
224
  indexSessionIdsByTrace,
207
225
  inspectSessionIndex,
226
+ isChatTrajectory,
227
+ isChatTrajectoryMessage,
208
228
  knownHarnesses,
209
229
  listAdapters,
210
230
  loadState,
@@ -266,7 +286,9 @@ export {
266
286
  traceStreamEventsFromSpans,
267
287
  uploadKey,
268
288
  userPromptSpan,
289
+ validateOtlpSpans,
269
290
  watchSessions,
291
+ writeHodoscopeInput,
270
292
  writeInspectionReportFile,
271
293
  writeOtlpFile,
272
294
  writePolicyEvidenceFile,
package/docs/demo.gif ADDED
Binary file
@@ -0,0 +1,222 @@
1
+ # Trace analysts
2
+
3
+ Use `traces` to answer three different questions.
4
+ Keeping them separate prevents an exploratory model output from being reported as a confirmed defect.
5
+
6
+ | Question | Tool | Output |
7
+ | --- | --- | --- |
8
+ | What happened? | Built-in local checks | Findings from explicit trace facts |
9
+ | Why might it have happened? | `--llm`, HALO, or a custom analyst | Findings or a diagnosis report with cited spans |
10
+ | What behavior should we inspect? | Hodoscope | Samples marked `needs_review` |
11
+
12
+ ## Start here
13
+
14
+ ```bash
15
+ # Free local checks over the active Codex coordinator and connected workers.
16
+ traces analyze --harness codex --current --latest-turn --workflow
17
+
18
+ # Limit a resumed Claude Code session to its latest task and subagents.
19
+ traces analyze --harness claude-code --session <path> --latest-turn
20
+
21
+ # Write reusable findings, evidence, report, and OpenInference spans.
22
+ traces improve --harness codex --last 5 --dir .traces/improvement
23
+
24
+ # Add the built-in model-assisted analysts with a spending limit.
25
+ traces analyze --harness codex --last 5 --llm --budget 0.50
26
+ ```
27
+
28
+ Without `--workflow`, each selected Codex file is analyzed independently.
29
+ With it, `traces` follows stable parent and child session IDs, up to 100 files by default.
30
+ `--latest-turn` keeps a long-lived resumed Codex or Claude Code session scoped to its most recent task and the workers spawned from that task.
31
+ For Codex, when the selected session is a child, `traces` finds the last parent task that structurally spawned or targeted that stable child ID and reads only that task.
32
+ The Codex parent log must carry both the child ID and a stable turn ID on the relevant spawn, send, follow-up, or lifecycle event.
33
+ Without those fields, the result is explicitly incomplete; names, nicknames, and timestamps are never used as substitutes.
34
+ It reports missing files, duplicate IDs, contradictory parents, and cycles instead of guessing.
35
+ Use `--max-workflow-sessions <n>` to set a different bound.
36
+ Claude Code already folds nested subagent files into its parent trace.
37
+ Ordinary Claude subagents use their parent tool call ID.
38
+ Claude Workflow subagents use the run ID and transcript directory returned by the parent `Workflow` call.
39
+ If the same Workflow run is resumed, each child attaches to the latest matching call that started before it.
40
+ The selected task includes only children attached to its calls.
41
+ Returned directories from another resumed Claude session are parsed and included in source hashes.
42
+
43
+ `traces improve` writes:
44
+
45
+ ```text
46
+ .traces/improvement/
47
+ evidence.jsonl
48
+ findings.json
49
+ report.md
50
+ result.json
51
+ traces.otlp.jsonl
52
+ ```
53
+
54
+ The OpenInference file is the shared input for external engines.
55
+ The original trace and exact cited span remain available for review.
56
+
57
+ ## External engines
58
+
59
+ External engines are optional tools that you install separately.
60
+ One engine failing does not discard the other results.
61
+
62
+ ```bash
63
+ # Recursive diagnosis over the same OpenInference trace.
64
+ traces analyze --last 1 --analyzer halo --analyzer-prompt "find unsupported completion claims"
65
+
66
+ # Sample distinct actions across a larger set of sessions.
67
+ traces analyze --all --last 20 --analyzer hodoscope
68
+
69
+ # Run any installed command that accepts an OpenInference file path.
70
+ traces analyze --last 1 --analyzer my-trace-tool
71
+ ```
72
+
73
+ `--model` is forwarded to the built-in model-assisted analysts, HALO, and Hodoscope.
74
+ HALO and Hodoscope use their own provider clients and credentials.
75
+ The Hodoscope adapter pins version `0.2.4` and uses Python 3.11 through `uvx`.
76
+
77
+ External results have one of three explicit kinds:
78
+
79
+ ```ts
80
+ type ExternalAnalysisResult =
81
+ | { kind: 'report'; output: string }
82
+ | { kind: 'findings'; findings: AnalystFinding[] }
83
+ | { kind: 'discovery'; candidates: ExternalDiscoveryCandidate[] }
84
+ ```
85
+
86
+ Raw JSON is still a `report`.
87
+ Only an adapter that validates the full finding shape may return `findings`.
88
+ Hodoscope always returns `discovery`, and each candidate has `status: 'needs_review'` plus its source trace and span.
89
+
90
+ ## Write one analyst
91
+
92
+ An analyst receives a paged trace store and returns typed findings.
93
+ Start with deterministic logic when the trace contains enough facts.
94
+
95
+ ```ts
96
+ import {
97
+ buildDefaultAnalystRegistry,
98
+ makeFinding,
99
+ } from '@tangle-network/traces'
100
+ import { defineTraceAnalyst } from '@tangle-network/agent-eval/analyst'
101
+
102
+ const failedTools = defineTraceAnalyst({
103
+ id: 'failed-tools',
104
+ description: 'Reports repeated tool failure signatures.',
105
+ async analyze(store) {
106
+ const overview = await store.getOverview({ has_errors: true })
107
+ return overview.error_clusters.map((cluster) => makeFinding({
108
+ analyst_id: 'failed-tools',
109
+ area: 'tool-use',
110
+ subject: cluster.signature,
111
+ claim: `${cluster.span_count} spans share this failure`,
112
+ severity: cluster.span_count >= 3 ? 'high' : 'medium',
113
+ evidence_refs: [{
114
+ kind: 'span',
115
+ uri: `trace://${cluster.exemplar_trace_ids[0]}/span/${cluster.exemplar_span_ids[0]}`,
116
+ }],
117
+ recommended_action: 'Fix the operation or change its retry policy.',
118
+ validation_plan: 'Rerun the task and confirm the signature is absent.',
119
+ confidence: 1,
120
+ id_basis: cluster.signature,
121
+ }))
122
+ },
123
+ })
124
+
125
+ const registry = buildDefaultAnalystRegistry()
126
+ registry.register(failedTools)
127
+
128
+ export default { registry }
129
+ ```
130
+
131
+ Run it without another wrapper:
132
+
133
+ ```bash
134
+ traces improve --last 5 --config traces.config.mjs --dir .traces/improvement
135
+ ```
136
+
137
+ The full runnable example is [`examples/custom-analyst.ts`](../examples/custom-analyst.ts).
138
+
139
+ Run its fixed input to inspect the complete JSON:
140
+
141
+ ```bash
142
+ pnpm tsx examples/custom-analyst.ts --fixture
143
+ ```
144
+
145
+ The current fixture returns one finding.
146
+ Its stable fields are:
147
+
148
+ ```json
149
+ {
150
+ "analyst_id": "failed-tool-clusters",
151
+ "area": "tool-use",
152
+ "claim": "3 failed span(s) share the error: Command failed with exit code 127 on attempt 1",
153
+ "severity": "high",
154
+ "evidence_refs": [
155
+ {
156
+ "kind": "span",
157
+ "uri": "trace://fixture-three-failures/span/failed-1"
158
+ }
159
+ ],
160
+ "recommended_action": "Fix or change the retry policy for exec.",
161
+ "validation_plan": "Rerun the same task and confirm this error signature is absent.",
162
+ "confidence": 1
163
+ }
164
+ ```
165
+
166
+ `pnpm tsx examples/custom-analyst.ts --good-fixture` returns `[]`.
167
+
168
+ ## Test an analyst
169
+
170
+ A useful analyst must beat a trivial no-findings baseline on labeled trajectories.
171
+ It must also avoid inventing issues on clean trajectories.
172
+
173
+ `agent-eval` provides adapters for two public datasets:
174
+
175
+ | Dataset | Labels used |
176
+ | --- | --- |
177
+ | AgentRx | Failure category, failed step, and root-cause step |
178
+ | CodeTraceBench | Incorrect and unhelpful action steps, including clean trajectories |
179
+
180
+ Use `runAnalystBenchmark` to report recall, precision, F1, root-step accuracy, citation coverage, citation validity, clean-case false positives, repeat agreement, latency, calls, tokens, and known cost.
181
+ Use `compareAnalystRunners` for paired baseline and candidate comparisons.
182
+
183
+ ```ts
184
+ import {
185
+ codeTraceBenchCase,
186
+ registryBenchmarkRunner,
187
+ renderAnalystBenchmarkMarkdown,
188
+ runAnalystBenchmark,
189
+ } from '@tangle-network/agent-eval/analyst'
190
+ import { otlpTextToTraceAnalysisStore } from '@tangle-network/agent-eval/traces'
191
+ import { chatTrajectoryToSpans, serializeSpans } from '@tangle-network/traces'
192
+
193
+ const cases = rows.map(({ label, trajectory }) => {
194
+ const spans = chatTrajectoryToSpans(trajectory, { traceId: label.traj_id })
195
+ const traceStore = otlpTextToTraceAnalysisStore(serializeSpans(spans))
196
+ return codeTraceBenchCase(label, { traceStore })
197
+ })
198
+
199
+ const result = await runAnalystBenchmark({
200
+ cases,
201
+ runners: [registryBenchmarkRunner({ id: 'candidate', registry })],
202
+ repetitions: 3,
203
+ maxConcurrency: 4,
204
+ })
205
+
206
+ console.log(renderAnalystBenchmarkMarkdown(result))
207
+ ```
208
+
209
+ Public labels test the measurement code, not the quality of every built-in analyst automatically.
210
+ A real quality claim requires running the analyst over the corresponding trajectories, retaining all rows, and comparing it with named alternatives at equal model and request limits.
211
+
212
+ ## Turn findings into improvement
213
+
214
+ Do not train or rewrite policy from an analyst's own prose alone.
215
+ Attach independent feedback or a measured task outcome to the trace first.
216
+ Then promote the reviewed failure into an eval case, make one targeted change, and compare before and after on fresh cases.
217
+
218
+ The required review path is:
219
+
220
+ ```text
221
+ production trace -> finding -> reviewed feedback -> eval case -> candidate change -> comparison
222
+ ```
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Run a deterministic custom analyst over a real session, or use --fixture for
3
+ * a known three-error input whose output is stable enough to inspect.
4
+ *
5
+ * pnpm tsx examples/custom-analyst.ts
6
+ * pnpm tsx examples/custom-analyst.ts --fixture
7
+ */
8
+ import {
9
+ AnalystRegistry,
10
+ type Analyst,
11
+ analyzeSpans,
12
+ collectSessions,
13
+ makeFinding,
14
+ type OtlpSpan,
15
+ span,
16
+ type TraceAnalysisStore,
17
+ } from '@tangle-network/traces'
18
+
19
+ export const failedToolAnalyst: Analyst<TraceAnalysisStore> = {
20
+ id: 'failed-tool-clusters',
21
+ description: 'Groups failed spans by stable error signature and cites one exact span per group.',
22
+ inputKind: 'trace-store',
23
+ cost: { kind: 'deterministic' },
24
+ version: '1.0.0',
25
+ async analyze(store) {
26
+ const overview = await store.getOverview({ has_errors: true })
27
+ return overview.error_clusters.map((cluster) => {
28
+ const traceId = cluster.exemplar_trace_ids[0]
29
+ const spanId = cluster.exemplar_span_ids[0]
30
+ return makeFinding({
31
+ analyst_id: 'failed-tool-clusters',
32
+ area: 'tool-use',
33
+ subject: cluster.signature,
34
+ claim: `${cluster.span_count} failed span(s) share the error: ${cluster.status_message_sample}`,
35
+ rationale: `The error affected ${cluster.trace_count}/${overview.errors.trace_count} trace(s) with errors.`,
36
+ severity: cluster.span_count >= 3 ? 'high' : 'medium',
37
+ evidence_refs: traceId && spanId
38
+ ? [{
39
+ kind: 'span',
40
+ uri: `trace://${encodeURIComponent(traceId)}/span/${encodeURIComponent(spanId)}`,
41
+ excerpt: cluster.status_message_sample,
42
+ }]
43
+ : [],
44
+ recommended_action: `Fix or change the retry policy for ${cluster.tool_name ?? cluster.span_name ?? 'the failing operation'}.`,
45
+ validation_plan: 'Rerun the same task and confirm this error signature is absent.',
46
+ confidence: 1,
47
+ metadata: {
48
+ deterministic: true,
49
+ signature: cluster.signature,
50
+ trace_count: cluster.trace_count,
51
+ span_count: cluster.span_count,
52
+ },
53
+ id_basis: cluster.signature,
54
+ })
55
+ })
56
+ },
57
+ }
58
+
59
+ export function knownBadSpans(): OtlpSpan[] {
60
+ const root = 'fixture-root'
61
+ return [
62
+ span({
63
+ traceId: 'fixture-three-failures',
64
+ spanId: root,
65
+ name: 'session',
66
+ kind: 'AGENT',
67
+ startTime: '2026-01-01T00:00:00.000Z',
68
+ service: 'example',
69
+ }),
70
+ ...[1, 2, 3].map((attempt) =>
71
+ span({
72
+ traceId: 'fixture-three-failures',
73
+ spanId: `failed-${attempt}`,
74
+ parentSpanId: root,
75
+ name: 'tool.exec',
76
+ kind: 'TOOL',
77
+ startTime: `2026-01-01T00:00:0${attempt}.000Z`,
78
+ endTime: `2026-01-01T00:00:0${attempt}.100Z`,
79
+ status: 'ERROR',
80
+ statusMessage: `Command failed with exit code 127 on attempt ${attempt}`,
81
+ service: 'example',
82
+ tool: 'exec',
83
+ step: attempt,
84
+ extra: { 'input.value': JSON.stringify({ command: 'missing-command' }) },
85
+ }),
86
+ ),
87
+ span({
88
+ traceId: 'fixture-three-failures',
89
+ spanId: 'unsupported-claim',
90
+ parentSpanId: root,
91
+ name: 'message.assistant',
92
+ kind: 'CHAIN',
93
+ startTime: '2026-01-01T00:00:04.000Z',
94
+ service: 'example',
95
+ step: 4,
96
+ content: 'Implemented successfully. Everything works.',
97
+ }),
98
+ ]
99
+ }
100
+
101
+ export function knownGoodSpans(): OtlpSpan[] {
102
+ const root = 'fixture-root'
103
+ return [
104
+ span({
105
+ traceId: 'fixture-verified-change',
106
+ spanId: root,
107
+ name: 'session',
108
+ kind: 'AGENT',
109
+ startTime: '2026-01-01T00:00:00.000Z',
110
+ service: 'example',
111
+ }),
112
+ span({
113
+ traceId: 'fixture-verified-change',
114
+ spanId: 'edit-1',
115
+ parentSpanId: root,
116
+ name: 'tool.apply_patch',
117
+ kind: 'TOOL',
118
+ startTime: '2026-01-01T00:00:01.000Z',
119
+ endTime: '2026-01-01T00:00:01.100Z',
120
+ service: 'example',
121
+ tool: 'apply_patch',
122
+ step: 1,
123
+ extra: { 'input.value': 'change code' },
124
+ }),
125
+ span({
126
+ traceId: 'fixture-verified-change',
127
+ spanId: 'verify-1',
128
+ parentSpanId: root,
129
+ name: 'tool.exec',
130
+ kind: 'TOOL',
131
+ startTime: '2026-01-01T00:00:02.000Z',
132
+ endTime: '2026-01-01T00:00:02.100Z',
133
+ service: 'example',
134
+ tool: 'exec',
135
+ step: 2,
136
+ extra: {
137
+ 'input.value': 'pnpm test',
138
+ 'output.value': '12 tests passed',
139
+ },
140
+ }),
141
+ span({
142
+ traceId: 'fixture-verified-change',
143
+ spanId: 'answer-1',
144
+ parentSpanId: root,
145
+ name: 'message.assistant',
146
+ kind: 'CHAIN',
147
+ startTime: '2026-01-01T00:00:03.000Z',
148
+ service: 'example',
149
+ step: 3,
150
+ content: 'Implemented the change. pnpm test passed: 12/12.',
151
+ }),
152
+ ]
153
+ }
154
+
155
+ async function main(): Promise<void> {
156
+ const registry = new AnalystRegistry()
157
+ registry.register(failedToolAnalyst)
158
+
159
+ let spans: readonly OtlpSpan[]
160
+ if (process.argv.includes('--fixture')) {
161
+ spans = knownBadSpans()
162
+ } else if (process.argv.includes('--good-fixture')) {
163
+ spans = knownGoodSpans()
164
+ } else {
165
+ const [session] = await collectSessions({ all: true, last: 1, redact: false })
166
+ if (!session) throw new Error('no recent sessions found')
167
+ spans = session.spans
168
+ }
169
+
170
+ const { result } = await analyzeSpans(spans, { registry })
171
+ console.log(JSON.stringify(result.findings, null, 2))
172
+ }
173
+
174
+ if (import.meta.url === new URL(process.argv[1] ?? '', 'file:').href) {
175
+ void main().catch((error: unknown) => {
176
+ console.error(error instanceof Error ? error.message : String(error))
177
+ process.exitCode = 1
178
+ })
179
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Redact + dedup local sessions, then upload to your OWN sink instead of the
3
+ * Tangle Intelligence Platform. The backend is any object with `ingestTraces`.
4
+ *
5
+ * tsx examples/custom-backend.ts
6
+ */
7
+ import { executeUpload, planUpload } from '@tangle-network/traces'
8
+
9
+ const plan = await planUpload({ all: true, sinceMs: Date.now() - 24 * 60 * 60 * 1000 })
10
+
11
+ const result = await executeUpload(plan, {
12
+ backend: {
13
+ async ingestTraces(spans, idempotencyKey) {
14
+ // `spans` is already redacted. POST it anywhere: your DB, a vector store, S3.
15
+ console.log(`send ${spans.length} spans (idempotency-key ${idempotencyKey})`)
16
+ return { accepted: spans.length }
17
+ },
18
+ },
19
+ })
20
+
21
+ console.log(`uploaded ${result.uploadedSessions} sessions, ${result.acceptedSpans} spans`)
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Run trace analyzers and PII scrubbers that traces does NOT bundle. You install
3
+ * the tool, traces drives it. Same pattern for any future engine/model.
4
+ *
5
+ * pnpm tsx examples/external-engines.ts
6
+ */
7
+ import {
8
+ applyRedactor,
9
+ collectSessions,
10
+ commandRedactor,
11
+ haloAnalyzer,
12
+ hodoscopeAnalyzer,
13
+ writeOtlpFile,
14
+ } from '@tangle-network/traces'
15
+
16
+ const [batch] = await collectSessions({ harnesses: ['claude-code'], last: 1 })
17
+ if (!batch) {
18
+ console.log('no claude-code sessions found')
19
+ process.exit(0)
20
+ }
21
+
22
+ // 1) External ANALYZER: emit the OTLP artifact, then run HALO over it as a peer
23
+ // to the built-in analysts. (Install HALO: github.com/context-labs/halo)
24
+ const otlp = await writeOtlpFile(batch.spans, '/tmp/session.otlp.jsonl')
25
+ const halo = haloAnalyzer({
26
+ defaultPrompt: 'diagnose stuck loops and wasted tokens',
27
+ maxDepth: 0,
28
+ maxTurns: 3,
29
+ maxParallel: 1,
30
+ reasoningEffort: 'low',
31
+ })
32
+ const analysis = await halo.analyze(otlp)
33
+ console.log(analysis.ok ? analysis.output : `halo unavailable: ${analysis.error}`)
34
+
35
+ // 2) DISCOVERY ENGINE: sample distinct behavior for human review.
36
+ // Hodoscope requires Python 3.11, uv, and model credentials.
37
+ const discovery = await hodoscopeAnalyzer().analyze(otlp)
38
+ console.log(
39
+ discovery.ok
40
+ ? `${discovery.candidates?.length ?? 0} behavior candidates need review`
41
+ : `hodoscope unavailable: ${discovery.error}`,
42
+ )
43
+
44
+ // 3) External REDACTOR: scrub prose with your own PII model before upload. The
45
+ // command reads a JSON array of strings on stdin and writes the scrubbed array
46
+ // on stdout (a 3-line wrapper adapts openai/privacy-filter's `opf`).
47
+ const redactor = commandRedactor({ command: 'my-pii-scrubber' })
48
+ const { spans, changed } = await applyRedactor(batch.spans, redactor)
49
+ console.log(`scrubbed ${changed} content field(s) across ${spans.length} spans`)
@@ -0,0 +1,63 @@
1
+ /**
2
+ * BYO analyst config for:
3
+ *
4
+ * traces improve --last 5 --config examples/improvement-config.mjs --dir .traces/improvement
5
+ * traces stream --mode agent --config examples/improvement-config.mjs
6
+ */
7
+ import { buildDefaultAnalystRegistry, makeFinding } from '@tangle-network/traces'
8
+
9
+ const registry = buildDefaultAnalystRegistry()
10
+
11
+ registry.register({
12
+ id: 'example-error-summary',
13
+ description: 'Reports the most frequent error signature in the selected traces.',
14
+ inputKind: 'trace-store',
15
+ cost: { kind: 'deterministic' },
16
+ version: '1.0.0',
17
+ async analyze(store) {
18
+ const overview = await store.getOverview({ has_errors: true })
19
+ const cluster = overview.error_clusters[0]
20
+ if (!cluster) return []
21
+ return [makeFinding({
22
+ analyst_id: 'example-error-summary',
23
+ area: 'tool-use',
24
+ subject: cluster.signature,
25
+ claim: `${cluster.span_count} failed span(s) share the error: ${cluster.status_message_sample}`,
26
+ severity: cluster.span_count >= 3 ? 'high' : 'medium',
27
+ evidence_refs: [{
28
+ kind: 'span',
29
+ uri: `trace://${encodeURIComponent(cluster.exemplar_trace_ids[0])}/span/${encodeURIComponent(cluster.exemplar_span_ids[0])}`,
30
+ excerpt: cluster.status_message_sample,
31
+ }],
32
+ recommended_action: `Fix or change the retry policy for ${cluster.tool_name ?? cluster.span_name ?? 'the failing operation'}.`,
33
+ validation_plan: 'Rerun the same task and confirm this error signature is absent.',
34
+ confidence: 1,
35
+ id_basis: cluster.signature,
36
+ })]
37
+ },
38
+ })
39
+
40
+ export default {
41
+ registry,
42
+ liveAnalysts: [{
43
+ id: 'example-live-claim-check',
44
+ analyze(context) {
45
+ if (!context.actions.some((action) => action.kind === 'claim')) return []
46
+ return [{
47
+ schemaVersion: 1,
48
+ kind: 'traces.live_finding',
49
+ id: `live.example-live-claim-check.${context.session.sessionId}`,
50
+ ruleId: 'example-live-claim-check',
51
+ fingerprint: `example-live-claim-check.${context.session.sessionId}`,
52
+ severity: 'info',
53
+ title: 'Custom live analyst saw a completion claim',
54
+ claim: 'The trace contains a claim-like assistant message.',
55
+ action: 'Keep this custom rule narrow; require evidence before alerting.',
56
+ check: 'Confirm the next stream event includes any required verification signal.',
57
+ evidence: [{ kind: 'metric', label: 'actions', value: String(context.actions.length) }],
58
+ session: context.session,
59
+ observedAt: context.generatedAt,
60
+ }]
61
+ },
62
+ }],
63
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Live observer → alert. Tails active sessions across all harnesses and fires
3
+ * when an agent gets stuck in a loop. Read-only; Ctrl-C to stop.
4
+ *
5
+ * tsx examples/observe-and-alert.ts
6
+ */
7
+ import { watchSessions } from '@tangle-network/traces'
8
+
9
+ const controller = new AbortController()
10
+ process.on('SIGINT', () => controller.abort())
11
+
12
+ await watchSessions({
13
+ all: true,
14
+ signal: controller.signal,
15
+ onLoop: (loop) => {
16
+ // Swap console.log for Slack / PagerDuty / your alerter.
17
+ console.log(`[${loop.harness}] ${loop.sessionId.slice(0, 8)} stuck: ${loop.toolName} ×${loop.occurrences}`)
18
+ },
19
+ })