Skip to main content
Anvil’s tracing system captures every agent run as a tree of spans — one span per model call, tool call, retrieval, cache hit, or judge evaluation — and rolls up token usage and cost into the parent trace. Pass a Tracer instance to defineAgent and every request handled by that route is automatically instrumented, with no changes required inside individual handlers or tools.

Stores: where traces live

Anvil ships two TraceStore implementations. Choose one based on your environment.

MemoryTraceStore

Zero-dependency in-memory store. Traces survive only for the lifetime of the process — ideal for unit tests and local experimentation where you don’t need a database.

SqliteTraceStore

SQLite-backed store powered by better-sqlite3 (optional peer dependency). Persists traces across restarts. The recommended default for production. Default path: .anvil/traces.db.

MemoryTraceStore

import { MemoryTraceStore } from 'anvil/trace';

const store = new MemoryTraceStore();
Call store.clear() between test cases to reset state.

SqliteTraceStore

import { SqliteTraceStore } from 'anvil/trace';

const store = await SqliteTraceStore.open('.anvil/traces.db');
SqliteTraceStore.open creates the file and schema on first call. Pass ':memory:' for a process-scoped SQLite database that behaves like MemoryTraceStore but uses the same SQL interface.
better-sqlite3 is an optional peer dependency. Install it before using SqliteTraceStore:
npm install better-sqlite3

Creating a Tracer

Tracer wraps a TraceStore and optionally accepts an onExport hook that fires when each trace ends — this is the seam used by the OpenTelemetry exporter.
import { Tracer, SqliteTraceStore } from 'anvil/trace';

const store = await SqliteTraceStore.open('.anvil/traces.db');

const tracer = new Tracer(store, {
  // Optional: called with the finished Trace object after each run ends.
  onExport: (trace) => console.log('trace finished', trace.id),
});
TracerOptions accepts one field:
onExport
(trace: Trace) => void
Callback invoked with the completed Trace after traceHandle.end() is called. Use this to pipe traces to an external system without coupling your store to the export logic.

Wiring the Tracer into defineAgent

Pass the tracer as an option to defineAgent. Anvil opens one TraceHandle per incoming request and closes it when the response stream ends.
import { defineAgent } from 'anvil/agent';
import { Tracer, SqliteTraceStore } from 'anvil/trace';
import { openai } from 'anvil/llm';

const store = await SqliteTraceStore.open('.anvil/traces.db');
const tracer = new Tracer(store);

export const post = defineAgent({
  client: openai({ model: 'gpt-4o' }),
  tracer,
  tools: [/* ... */],
});

Naming traces

By default, Anvil names each trace agent <route-path>. Override this with traceName:
defineAgent({
  tracer,
  traceName: 'customer-support-agent',
  // ...
});

The Trace and Span types

Every finished trace is a Trace object containing a flat list of Span records that form a tree via parentId references.

Trace

interface Trace {
  id: string;
  name: string;
  startedAt: number;      // Unix ms
  endedAt?: number;       // Unix ms; set when the run ends
  status: SpanStatus;     // 'running' | 'ok' | 'error' | 'aborted'
  spans: Span[];          // full span tree
  totalCostUsd: number;   // rolled-up cost across all model spans
  totalInputTokens: number;
  totalOutputTokens: number;
  attributes: Record<string, unknown>;
}

Span

interface Span {
  id: string;
  traceId: string;
  parentId?: string;      // links into the tree; absent on root spans
  name: string;
  kind: SpanKind;         // 'agent' | 'model' | 'tool' | 'retrieval' | 'cache' | 'judge' | 'http'
  startedAt: number;
  endedAt?: number;
  status: SpanStatus;
  attributes: Record<string, unknown>; // model, provider, usage, cost, tool args/result, etc.
  error?: string;
}
SpanKind identifies what produced the span:
KindDescription
agentThe top-level agent loop span
modelA single LLM generate call, including usage and cost
toolOne tool execution
retrievalA ctx.retrieve() call
cacheA semantic cache hit or miss
judgeAn LLM-as-judge evaluation
httpAn outbound HTTP call
Retrieval spans from ctx.retrieve() are recorded in the same trace tree as the agent run. They appear as kind: 'retrieval' children of the enclosing agent span, so you can see the full RAG context alongside model calls in one view.

TraceStore interface

If you need a custom backend (Redis, Postgres, etc.) implement TraceStore directly:
interface TraceStore {
  saveTrace(trace: Trace): void | Promise<void>;
  saveSpan(span: Span): void | Promise<void>;
  getTrace(id: string): (Trace | undefined) | Promise<Trace | undefined>;
  listTraces(opts?: ListTracesOptions): Trace[] | Promise<Trace[]>;
}
ListTracesOptions accepts limit and offset for pagination.

Full example

import { defineAgent } from 'anvil/agent';
import { Tracer, SqliteTraceStore } from 'anvil/trace';
import { anthropic } from 'anvil/llm';

const store = await SqliteTraceStore.open('.anvil/traces.db');
const tracer = new Tracer(store);

export const post = defineAgent({
  client: anthropic({ model: 'claude-3-5-sonnet-latest' }),
  tracer,
  traceName: (ctx) => `billing-agent:${ctx.query.customerId}`,
  tools: [lookupOrder, issueRefund],
  guardrails: [/* ... */],
});
Every request to this route produces a trace persisted to SQLite. Open /_anvil in the browser to inspect the span tree, token usage, and cost in real time.