npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@escouade/graph-trace

v0.2.0

Published

Structured tracing model for graph agent runs.

Readme

@escouade/graph-trace

npm version License: MIT TypeScript Zero dependencies

Structured tracing for graph agent runs — capture an LLM/tool run as a flat event stream, project it into a self-contained, versioned trace document, and export it anywhere.

  • Runtime-agnostic core — zero framework dependency. Events flow through a TraceRecorder port; any engine (or an adapter like @escouade/graph-trace-langchain) can feed it.
  • Pipeline provider → collector → exporter — a TraceCollector owns the exporters; every finished run is projected to TraceDocument and exported automatically.
  • Step projection isomorphic to graph traversal — each step = one pass through a graph node, carrying its events and the real edge taken on exit. Loops are preserved, not collapsed.
  • Portable document format (graph-trace/v1) — exportable, re-importable, and consolidatable run after run.

Install

npm install @escouade/graph-trace

Zero runtime dependencies (other than tslib). Ships ESM + type declarations.

Concepts

| Piece | Role | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | TraceRecorder | Port a provider feeds: record(event, span), succeed(), fail(err). | | RunTrace | In-memory ledger of a single run; self-stamps the run.start/run.end envelope. | | TraceCollector | Reusable pipeline: opens runs, projects each to TraceDocument on close, hands it to exporters. | | TraceExporter | Terminal sink for a TraceDocument (the assembled run). @escouade/graph-trace-file-exporter ships a Node.js file exporter; bring your own for HTTP/console. | | TraceStreamSink | Live sink fed each record as it happens (record({ event, span })), for relaying a run event-by-event. Additive to TraceExporter — see Streaming records live. | | WorkflowDefinition | Declarative topology (nodes + edges) your host writes once, fused with its own execution fields. toWorkflowDescriptor strips it to the trace-facing shape embedded in every run. | | TraceWorkflowDescriptor | Trace-facing topology (id / kind / label per node, from/to/condition/loop per edge) embedded in run.start and drawn by the viewer. | | TraceDocument | The versioned, self-contained format a conversation's trace is exported as. |

Quick start

Wire a collector once, then open a trace per run and feed it events.

import { TraceCollector } from '@escouade/graph-trace';
import { FileExporter } from '@escouade/graph-trace-file-exporter';

// Singleton pipeline — projects every run to TraceDocument and exports it.
const collector = new TraceCollector({
  exporters: [
    new FileExporter({
      resolvePath: (doc) => `./traces/${doc.meta.traceId}.trace.json`,
    }),
  ],
  // traceId: 'my-session'  // optional: override the auto-generated trace id
});

// Per run: `run.start` is posed from this metadata.
const trace = collector.createTrace({
  provider: 'anthropic',
  model: 'claude-sonnet-4-6',
  workflow, // a TraceWorkflowDescriptor (see below)
  systemPrompt: 'You are…',
  context: '',
  history: [{ role: 'user', content: 'Hello' }],
  tags: { agent: 'assistant' }, // optional free-form metadata
});

// Feed events through the recorder port (or let an adapter do it — see graph-trace-langchain).
trace.record(
  { type: 'llm.start', messages: [{ role: 'user', content: 'Hello' }], tools: [] },
  { node: 'agent', spanId: 'span-1' },
);
trace.record(
  { type: 'llm.end', text: 'Hi!', toolCalls: [], usage: { input_tokens: 8, output_tokens: 3 } },
  { node: 'agent', spanId: 'span-1' },
);

trace.succeed(); // closes the run → projected + buffered

// Drain the buffer at graceful shutdown:
await collector.flush();

Tracing a LangGraph run

You rarely feed events by hand. With LangGraph (or any LangChain runnable), install @escouade/graph-trace-langchain and attach the adapter to the run — it captures LLM turns and tools, attributes each event to its langgraph_node, and closes the run from the root chain end/error.

import { TraceCollector, toWorkflowDescriptor } from '@escouade/graph-trace';
import { FileExporter } from '@escouade/graph-trace-file-exporter';
import { langChainTraceHandler } from '@escouade/graph-trace-langchain';

const collector = new TraceCollector({
  exporters: [new FileExporter({ resolvePath: (doc) => `./traces/${doc.meta.traceId}.json` })],
});

// At app shutdown — drain all buffered runs:
process.on('SIGTERM', async () => {
  await collector.shutdown();
  process.exit(0);
});

const trace = collector.createTrace({
  provider: 'anthropic', // known at LLM client init, not in input
  model: 'claude-sonnet-4-6', // idem
  workflow: toWorkflowDescriptor(def),
  systemPrompt: input.systemPrompt,
  context: input.context ?? '',
  history: input.messages, // your graph state's message list
});

try {
  await graph.invoke(input, { callbacks: [langChainTraceHandler(trace)] });
} catch (err) {
  trace.fail(err); // safety net if the error was thrown before any chain ran
  throw err;
}

Tagging runs

Pass a tags object to createTrace to attach free-form metadata to a run. Tags flow through to TraceTurn.tags in the projected document — the viewer and your tooling can read them.

const trace = collector.createTrace({
  // …
  tags: {
    serviceName: 'my-service', // identifies the originating service
    env: 'production',
    agent: 'planner',
  },
});

// Read them back from a projected turn:
for (const turn of doc.turns) {
  console.log(turn.tags?.serviceName); // 'my-service'
}

Tags are string → string. For structured values, serialize to JSON before passing.

Individual events can carry their own tags via record() — useful for correlation ids or event-level context:

trace.record(
  { type: 'tool.start', name: 'search', input: query, tags: { correlationId: req.id } },
  { node: 'tools' },
);

Per-event tags are accessible in step.events[n].tags in the projected document.

Describing the graph

A run carries a TraceWorkflowDescriptor (nodes + edges) so a viewer can draw the executed topology. Build one from a declarative WorkflowDefinition to keep a single source of truth — the same object is both runnable by your engine and traceable by the library:

import { toWorkflowDescriptor, type WorkflowDefinition } from '@escouade/graph-trace';

const def: WorkflowDefinition = {
  id: 'agent-loop',
  label: 'Agent loop',
  entry: 'agent', // first node executed (draws the implicit START → agent edge)
  nodes: [
    { id: 'agent', kind: 'agent', label: 'Agent' }, // `kind` is a free string — viewer uses it for icons
    { id: 'tools', kind: 'tools', label: 'Tools' },
  ],
  edges: [
    { from: 'agent', to: 'tools', condition: 'tool calls' }, // `condition` is a display label, not a predicate
    { from: 'agent', to: 'END', condition: 'no tool calls' },
    { from: 'tools', to: 'agent', condition: 'continue', loop: true }, // `loop: true` draws a back-arc
  ],
};

const workflow = toWorkflowDescriptor(def); // strips execution fields, keeps trace shape

WorkflowDefinition is generic (<TNode extends TraceNode, TEdge extends TraceEdge>): fuse it with your engine's execution augmentation (node factory, routing predicate) so the same object builds your graph and produces the trace descriptor — they can never diverge.

See docs/workflow-definition.md for the field reference and docs/fused-workflow-definition.md for the fusion pattern.

Collector options and run ids

const collector = new TraceCollector({
  projector: projectTraceDocument,
  exporters: [...],
  traceId: 'my-session', // optional: override the auto-generated trace id (e.g. to resume a persisted session)
});

// collector.traceId — the active trace id (auto-generated or the one you passed)

const trace = collector.createTrace(meta, {
  runId: externalCorrelationId, // optional: override the auto-generated run id
});

TraceCollector implements the RunTracePipeline interface — use that type when injecting the collector into your orchestration layer to avoid coupling to the concrete class.

import type { RunTracePipeline } from '@escouade/graph-trace';

class Orchestrator {
  constructor(private readonly pipeline: RunTracePipeline) {}
}

Streaming records live

The exporters above fire once per run, at run.end, with the whole assembled TraceDocument. To relay a run as it happens — event by event — attach a TraceStreamSink to the run. The recorder pushes every record to it at the moment it is captured, in emission order (run.startllm.*/tool.*run.end included). This is additive: with no sink, tracing behaves exactly as before; the terminal exporters still fire on run.end.

import type { TraceStreamSink } from '@escouade/graph-trace';

// A downstream relay: forward each record over your transport (SSE, WebSocket, queue…).
const relay: TraceStreamSink = {
  record({ event, span }) {
    // Synchronous and non-blocking — do not await here. Serialize (e.g. JSONL) and enqueue.
    channel.post(JSON.stringify({ event, span }));
  },
};

// Scoped to the run — the relay naturally follows the run/turn it streams.
const trace = collector.createTrace(meta, { streamSink: relay });

record(entry) receives a TraceStreamEntry = { event, span }: the fully stamped TraceEvent (ids/seq/ts already set) and the TraceSpan it was recorded with. A sink must not block or throw — any async delivery is the consumer's responsibility.

Reading a trace

projectTraceDocument(events, label?) turns a flat TraceEvent[] into a TraceDocument (turns → steps, aggregated tokens/duration/errors). parseTraceDocument(raw) validates a re-imported file and throws if traceFormat is not 'graph-trace/v1' (exposed as TRACE_FORMAT); mergeTraceDocuments(base, addition) consolidates runs.

From a JSONL stream

Records relayed through a TraceStreamSink and serialized one-per-line (JSONL) fold back into a TraceDocument — equivalent to what the terminal exporter would have produced for the same runs. parseTraceJsonl(text) splits the lines, JSON-parses each { event, span } record and delegates to foldTraceRecords(entries), which runs the projection over the flattened events. Multiple runs in the input yield multiple turns. Both are best-effort: a malformed or truncated line (an unterminated final run, a cut last line, a stray non-JSON line) is skipped, keeping the complete runs.

import { parseTraceJsonl } from '@escouade/graph-trace';

const doc = parseTraceJsonl(await fs.readFile('conv-1.jsonl', 'utf8'));
// doc.turns — one per run, ready to render like any TraceDocument
import { parseTraceDocument, TRACE_FORMAT } from '@escouade/graph-trace';

const doc = parseTraceDocument(await fs.readFile('conv-1.trace.json', 'utf8'));
// doc.traceFormat === TRACE_FORMAT ('graph-trace/v1')
for (const turn of doc.turns) {
  console.log(
    turn.userMessage.content,
    turn.steps.map((s) => s.node),
  );
}

projectTurns(events) is the lower-level primitive: projects a flat event stream into TraceTurn[] without the conversation-level envelope. Useful when you don't need the full TraceDocument (e.g. streaming partial results to a viewer).

Events

A run is a flat, ordered stream of discriminated events: run.start, llm.start, llm.end, tool.start, tool.end, tool.error, run.end. Each is stamped with runId/seq/ts and an optional span (node/spanId/parentSpanId). Secrets (API keys, provider config) never appear in a trace by construction.

Resources

License

MIT