'use strict'; /** * @community/trace — dead-simple structured run logging + replay for AI agents. * * Agent debugging is "still in the stone age" because plain logs are flat, * unstructured, and non-deterministic. `trace` gives you append-only structured * events (JSONL) with runs, nested spans, and timings — greppable, replayable, * and dependency-free. Point a sink at it live, or flush to a file and replay. */ const crypto = require('crypto'); const fs = require('fs'); const rid = () => crypto.randomBytes(6).toString('hex'); class Span { constructor(tracer, name, data, parent) { this.tracer = tracer; this.name = name; this.parent = parent || null; this.id = rid(); this.startedAt = tracer.clock(); tracer._emit({ type: 'span:start', span: this.id, parent: this.parent, name, data }); } event(type, name, data) { this.tracer._emit({ type, span: this.id, name, data }); return this; } child(name, data) { return new Span(this.tracer, name, data, this.id); } end(data) { const ms = this.tracer.clock() - this.startedAt; this.tracer._emit({ type: 'span:end', span: this.id, name: this.name, ms, data }); return ms; } } class Tracer { /** opts: { run?, sink?(event), clock?() } — clock is injectable for tests. */ constructor(opts = {}) { this.run = opts.run || rid(); this.events = []; this.sink = opts.sink || null; this.clock = opts.clock || Date.now; } _emit(e) { const rec = { ts: this.clock(), run: this.run, ...e }; this.events.push(rec); if (typeof this.sink === 'function') { try { this.sink(rec); } catch { /* never let logging throw */ } } return rec; } event(type, name, data) { return this._emit({ type, name, data }); } step(name, data) { return this.event('step', name, data); } error(name, data) { return this.event('error', name, data); } start(name, data) { return new Span(this, name, data, null); } /** Wrap an async fn in a span: auto-times, auto-captures a thrown error, re-throws. */ async span(name, fn, data) { const s = this.start(name, data); try { const out = await fn(s); s.end({ ok: true }); return out; } catch (err) { s.event('error', 'threw', { message: err && err.message }); s.end({ ok: false }); throw err; } } toJSONL() { return this.events.map((e) => JSON.stringify(e)).join('\n') + (this.events.length ? '\n' : ''); } flush(file) { fs.writeFileSync(file, this.toJSONL()); return file; } } /** Load events from a JSONL file path OR a raw JSONL string. */ function replay(source) { let text = source; try { if (typeof source === 'string' && fs.existsSync(source)) text = fs.readFileSync(source, 'utf8'); } catch { /* treat as text */ } return String(text).split('\n').filter(Boolean).map((line) => JSON.parse(line)); } /** One-glance health of a run: counts, total duration, slowest spans, errors. */ function summarize(events) { const ends = events.filter((e) => e.type === 'span:end'); const errors = events.filter((e) => e.type === 'error'); const slowest = ends.slice().sort((a, b) => (b.ms || 0) - (a.ms || 0)).slice(0, 5) .map((e) => ({ name: e.name, ms: e.ms })); const total = events.length ? events[events.length - 1].ts - events[0].ts : 0; return { runs: new Set(events.map((e) => e.run)).size, events: events.length, spans: ends.length, errors: errors.length, total_ms: total, slowest, }; } module.exports = { Tracer, Span, replay, summarize };