Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ It reads the transcripts your harness leaves on disk, reconstructs the run as sp
- [What it finds](#what-it-finds)
- [Supported harnesses](#supported-harnesses)
- [CLI reference](#cli-reference)
- [Policy-mining evidence](#policy-mining-evidence)
- [Upload to the Intelligence Platform](#upload-to-the-intelligence-platform)
- [External engines (bring your own)](#external-engines-bring-your-own)
- [Library (SDK)](#library-sdk)
Expand Down Expand Up @@ -85,6 +86,7 @@ traces list --harness claude-code --last 20 # discover sessions
traces analyze --harness codex --last 1 # $0 deterministic report
traces analyze --all --since 2026-06-18 --out report.md
traces convert --harness claude-code --last 1 --otlp spans.jsonl # OTLP only
traces evidence --harness codex --last 20 --out policy-evidence.jsonl
traces watch --all # live observer; notify on stuck loops
traces upload --since 1h --dry-run # redact + dedup + preview, no network
traces upload --since 24h # upload last day to the Intelligence Platform
Expand All @@ -99,13 +101,31 @@ traces upload --since 24h # upload last day to the Inte
| `--cwd <dir>` | Filter by working directory |
| `--since <t>` | `upload`: window — `30m`/`2h`/`7d` or ISO (default 24h); `analyze`: ISO cutoff |
| `--out <path>` | Write the report to a file |
| `--otlp <path>` | OTLP artifact path (also the dry-run upload preview) |
| `--otlp <path>` | OTLP artifact path (also evidence provenance / dry-run upload preview) |
| `--llm` / `--budget <usd>` | Enable agentic analysts (needs `OPENAI_API_KEY`) / cap their spend |
| `--interval <s>` / `--window <m>` | `watch`: poll seconds (default 5) / active-session window minutes (default 30) |
| `--min-loop <n>` | Identical repeated calls before flagging a loop (default 3) |
| `--no-content` | `upload`: send metadata only — strip all prompt/response text |
| `--dry-run` / `--yes` | `upload`: preview without sending / skip the confirm prompt |

## Policy-mining evidence

`traces` does **not** emit benchmark campaign cells. It emits normalized coding-agent session evidence that another system can mine.

```bash
traces evidence --all --since 24h --out policy-evidence.jsonl --otlp spans.otlp.jsonl
```

Each JSONL row is one session:

- session provenance: harness, session id, cwd, path, mtime
- repo labels: `tangle.subject.key`, `git.repository`, branch, commit
- behavior metrics: span counts, LLM turns, tool calls, errored tool calls, tokens, models, tool histogram
- mining signals: stuck loops and tool error rate
- provenance marker: `notCampaignCell: true`

That boundary matters. `agent-lab` campaign `cells.jsonl` says "arm X beat arm Y on task Z." `traces evidence` says "this real agent session had this repo/model/tool/failure shape." A downstream policy compiler can cluster these rows, propose candidate policies, then validate those policies in a separate eval campaign.

## Upload to the Intelligence Platform

`upload` **redacts locally before anything leaves the machine**, dedups against already-uploaded sessions, and tags each with metadata (harness, cwd, git branch, host).
Expand Down Expand Up @@ -163,6 +183,8 @@ The CLI is a thin consumer of these exports.
|---|---|---|
| `analyzeSpans` | `(spans, { registry?, ai?, budgetUsd? }) → AnalyzeResult` | run analysts — built-in, or **your own** via `registry` |
| `watchSessions` | `(ObserverOptions) → Promise<void>` | live observer; `onLoop` / `onReport` / `signal` / `adapters` |
| `buildPolicyEvidenceRecord` | `(ref, spans, opts?) → PolicyEvidenceRecord` | summarize one session for downstream policy mining |
| `collectPolicyEvidence` | `(ScanOptions) → PolicyEvidenceRecord[]` | scan harness sessions and emit policy-evidence rows |
| `scanSessions` | `(ScanOptions) → AsyncIterable<ScannedSession>` | the shared locate→parse iterator |
| `collectSessions` | `(CollectOptions) → SessionBatch[]` | redacted per-session batches for your own pipeline |
| `redactSpans` | `(spans, rules?) → { spans, report }` | PII/secret redaction (`TRACES_REDACTION_RULES`) |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
},
"dependencies": {
"@ax-llm/ax": "^19.0.45",
"@tangle-network/agent-eval": "0.95.1"
"@tangle-network/agent-eval": "^0.99.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ export const ATTR = {
INGEST_SOURCE: 'tangle.ingest_source',
HARNESS: 'tangle.harness',
CWD: 'tangle.cwd',
/** THE per-session grouping key the spine derives subjects from
* (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
* remote (host/owner/repo) when readable, else the cwd path basename. */
SUBJECT_KEY: 'tangle.subject.key',
/** Normalized git remote (e.g. `github.com/tangle-network/agent-dev-container`). */
GIT_REPOSITORY: 'git.repository',
/** Current branch at conversion time. */
GIT_BRANCH_NAME: 'git.branch',
/** HEAD short sha at conversion time. */
GIT_COMMIT: 'git.commit',
GIT_BRANCH: 'tangle.git_branch',
HOST: 'tangle.host',
/** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
Expand Down
54 changes: 51 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* traces list [--harness claude-code] [--last 20] [--all]
* traces analyze [--harness claude-code] [--last 1] [--out report.md] [--llm]
* traces convert [--harness claude-code] [--last 1] --otlp spans.jsonl
* traces evidence [--harness claude-code] [--last 20] --out policy-evidence.jsonl
* traces watch [--all] [--interval 5] [--window 30] [--min-loop 3]
*
* `analyze` runs the agent-eval analyst suite (deterministic + the shipped
Expand All @@ -17,13 +18,15 @@
import { stat, writeFile } from 'node:fs/promises'
import { analyzeAdoption } from './adoption.js'
import { analyzeSpans } from './analyze.js'
import { buildPolicyEvidenceRecord, serializePolicyEvidence, writePolicyEvidenceFile } from './evidence.js'
import { commandAnalyzer, commandRedactor, haloAnalyzer, runExternalAnalyzers } from './external.js'
import type { OtlpSpan } from './otlp.js'
import { writeOtlpFile } from './otlp.js'
import { watchSessions } from './observer.js'
import { runPipelines } from './pipelines.js'
import { knownHarnesses, resolveAdapter, selectAdapters } from './registry.js'
import { analyzeReactions } from './reactions.js'
import { parseSession } from './session-source.js'
import { renderAdoption, renderPipelines, renderReactions, renderReport } from './report.js'
import { parseSince } from './time.js'
import type { HarnessTraceAdapter, SessionRef } from './types.js'
Expand Down Expand Up @@ -171,7 +174,7 @@ async function collectSpans(args: Args): Promise<{ spans: OtlpSpan[]; harness: s
cwd: args.cwd ?? null,
mtimeMs: st.mtimeMs,
}
return { spans: await adapter.parse(ref), harness: adapter.harness, sessionCount: 1, cwds: ref.cwd ? [ref.cwd] : [] }
return { spans: await parseSession(adapter, ref), harness: adapter.harness, sessionCount: 1, cwds: ref.cwd ? [ref.cwd] : [] }
}
const groups = await discover({ ...args, last: args.last || 1 })
const spans: OtlpSpan[] = []
Expand All @@ -181,7 +184,7 @@ async function collectSpans(args: Args): Promise<{ spans: OtlpSpan[]; harness: s
for (const { adapter, refs } of groups) {
if (refs.length > 0) harnesses.push(adapter.harness)
for (const ref of refs) {
spans.push(...(await adapter.parse(ref)))
spans.push(...(await parseSession(adapter, ref)))
if (ref.cwd) cwds.push(ref.cwd)
sessionCount += 1
}
Expand All @@ -196,6 +199,49 @@ async function cmdConvert(args: Args): Promise<void> {
console.log(`wrote ${spans.length} spans → ${path}`)
}

async function collectSessionRows(args: Args): Promise<Array<{ ref: SessionRef; spans: OtlpSpan[] }>> {
if (args.session) {
const adapter = resolveAdapter(args.harness)
if (!adapter) throw new Error(`unknown harness "${args.harness}"`)
const st = await stat(args.session)
const ref: SessionRef = {
harness: adapter.harness,
sessionId: args.session,
path: args.session,
cwd: args.cwd ?? null,
mtimeMs: st.mtimeMs,
}
return [{ ref, spans: await parseSession(adapter, ref) }]
}
const groups = await discover({ ...args, last: args.last || 20 })
const rows: Array<{ ref: SessionRef; spans: OtlpSpan[] }> = []
for (const { adapter, refs } of groups) {
for (const ref of refs) rows.push({ ref, spans: await parseSession(adapter, ref) })
}
return rows
}

async function cmdEvidence(args: Args): Promise<void> {
const rows = (await collectSessionRows(args)).filter((row) => row.spans.length > 0)
if (rows.length === 0) throw new Error('no spans found for the given selection')
const otlpPath = args.otlp ? await writeOtlpFile(rows.flatMap((row) => row.spans), args.otlp) : undefined
const generatedAt = new Date().toISOString()
const records = await Promise.all(rows.map((row) =>
buildPolicyEvidenceRecord(row.ref, row.spans, {
generatedAt,
minLoopOccurrences: args.minLoop,
maxLoopExamples: 25,
otlpPath,
}),
))
if (args.out) {
const path = await writePolicyEvidenceFile(records, args.out)
console.log(`policy evidence → ${path} (${records.length} session rows${otlpPath ? `, OTLP: ${otlpPath}` : ''})`)
} else {
process.stdout.write(serializePolicyEvidence(records))
}
}

async function cmdAnalyze(args: Args): Promise<void> {
const { spans, harness, sessionCount, cwds } = await collectSpans(args)
if (spans.length === 0) throw new Error('no spans found for the given selection')
Expand Down Expand Up @@ -324,6 +370,7 @@ Commands:
list List discovered sessions
analyze Run analyst suite + loop/waste pipelines, write a markdown report
convert Emit OTLP-JSONL only (HALO: use analyze --analyzer halo)
evidence Emit compact session-evidence JSONL for downstream policy miners
watch Online observer: tail active sessions, notify on stuck loops (read-only)
upload Redact + upload sessions in a time window to the Tangle Intelligence Platform

Expand All @@ -335,7 +382,7 @@ Options:
--cwd <dir> Filter sessions by working directory
--since <t> upload: window — 30m / 2h / 7d or an ISO date (default 24h); analyze: ISO cutoff
--out <path> Write report to a file
--otlp <path> OTLP artifact path (also the dry-run upload preview path)
--otlp <path> OTLP artifact path (also evidence provenance / dry-run upload preview)
--llm Enable agentic RLM analysts (needs OPENAI_API_KEY / OPENAI_BASE_URL)
--model <id> --llm model id (e.g. a router model like glm-5.2); default is agent-eval's
--budget <usd> USD cap for agentic analysts
Expand All @@ -358,6 +405,7 @@ async function main(): Promise<void> {
case 'list': await cmdList(args); break
case 'analyze': await cmdAnalyze(args); break
case 'convert': await cmdConvert(args); break
case 'evidence': await cmdEvidence(args); break
case 'watch': await cmdWatch(args); break
case 'upload': await cmdUpload(args); break
default: usage()
Expand Down
Loading
Loading