@tangle-network/traces 0.9.11 → 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/README.md +62 -19
- package/dist/{chunk-ELWOJS65.js → chunk-H47X6QWV.js} +4016 -750
- package/dist/cli.js +162 -68
- package/dist/index.d.ts +325 -30
- package/dist/index.js +23 -1
- package/docs/demo.gif +0 -0
- package/docs/trace-analysts.md +222 -0
- package/examples/custom-analyst.ts +179 -0
- package/examples/custom-backend.ts +21 -0
- package/examples/external-engines.ts +49 -0
- package/examples/improvement-config.mjs +63 -0
- package/examples/observe-and-alert.ts +19 -0
- package/examples/register-harness.ts +26 -0
- package/package.json +9 -8
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-
|
|
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
|
+
})
|