Context Tracing
Context tracing lets a host observe an agent run without changing the agent loop.
The SDK emits structured trace events through a small ContextTracer interface.
Built-in tracers can write JSONL locally or project those events into LangSmith
or Langfuse.
Tracing is append-only observability. It is not resume support: agents still keep
conversation state in memory for the lifetime of the Agent instance.
JSONL tracer
Section titled “JSONL tracer”Use createJsonlContextTracer() when you want local trace files for debugging,
audit logs, or test artifacts.
import { createAgent, createJsonlContextTracer } from "agent-lattice";
const tracer = createJsonlContextTracer({ path: ".agent-runs/session.jsonl",});
const agent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", tracer,});
await agent.prompt("Remember that my name is Ada.", { stream: false,});Each JSONL row is an independent object:
{ "version": 1, "timestamp": "2026-06-28T12:00:00.000Z", "session_id": "session-id", "run_id": "run-id", "seq": 1, "source": { "kind": "agent", "name": "agent" }, "type": "user_message", "data": { "message": { "role": "user", "content": "Remember that my name is Ada." } }}If you pass dir instead of path, the tracer writes one file per session:
const tracer = createJsonlContextTracer({ dir: ".agent-runs",});LangSmith tracer
Section titled “LangSmith tracer”Use createLangSmithContextTracer() when you want SDK runs to appear as
LangSmith traces. The SDK depends on langsmith directly and reuses its
official RunTree types, so the tracer works out of the box. Since 0.17.0
the bundled RunTree is the default; pass RunTree or runTree only to
inject a custom runtime or a test fake.
Configure LangSmith with its standard environment variables:
LANGSMITH_TRACING=trueLANGSMITH_ENDPOINT=https://api.smith.langchain.comLANGSMITH_API_KEY=<your-langsmith-api-key>LANGSMITH_PROJECT=<your-langsmith-project># Required only for org-scoped or multi-workspace API keys.LANGSMITH_WORKSPACE_ID=<your-langsmith-workspace-id>import { createAgent, createLangSmithContextTracer } from "agent-lattice";
const tracer = createLangSmithContextTracer({ projectName: process.env.LANGSMITH_PROJECT, workspaceId: process.env.LANGSMITH_WORKSPACE_ID, tags: ["local-debug"],});
const agent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", tracer,});
try { await agent.prompt("Trace this run.", { stream: false });} finally { await tracer.close?.();}Close or flush the LangSmith tracer before a short-lived test process exits. This waits for LangSmith’s internal trace batch queue, including the final root run patch that marks the run completed.
If you prefer explicit values over environment variables, pass them to the SDK
tracer. workspaceId is optional and only selects a LangSmith workspace; it is
not the tracing project name.
import { createLangSmithContextTracer } from "agent-lattice";
const tracer = createLangSmithContextTracer({ apiKey: process.env.LANGSMITH_API_KEY, apiUrl: process.env.LANGSMITH_ENDPOINT, projectName: process.env.LANGSMITH_PROJECT, // Optional: only when LangSmith requires an explicit workspace. workspaceId: process.env.LANGSMITH_WORKSPACE_ID,});The LangSmith tracer implements the same ContextTracer interface as JSONL, so
the agent loop stays
closed to provider-specific changes.
LangSmith receives:
| SDK event sequence | LangSmith projection |
|---|---|
run_start to result |
chain run; it is a root unless parent_run_id links it to another chain. |
model_request to assistant_message |
Child llm run. |
tool_use to tool_result |
Child tool run. |
team_message and other auxiliary events |
Run events on the active chain. |
Each run’s duration reflects the work itself. A tool run starts when that
handler starts, so sequential calls appear one after another and concurrent
calls overlap. An llm run ends when the model request settles, not when the
host finishes consuming the events yielded from it. Tool runs carry the tool’s
description as tool_description metadata, so a trace shows what the tool
was advertised to do, not only its input and output.
Use createCompositeContextTracer() to send the same trace stream to multiple
sinks:
const tracer = createCompositeContextTracer([ createJsonlContextTracer({ path: ".agent-runs/session.jsonl" }), createLangSmithContextTracer({ projectName: process.env.LANGSMITH_PROJECT, workspaceId: process.env.LANGSMITH_WORKSPACE_ID, }),]);When a composite tracer contains LangSmith, close the composite tracer in the
same finally block; it forwards close() to every child tracer.
Langfuse tracer
Section titled “Langfuse tracer”Requires 0.19.0 or later.
Use createLangfuseContextTracer() when you want SDK runs to appear as
Langfuse traces. The adapter targets the current Langfuse JS SDK generation
(@langfuse/tracing v5), which is OpenTelemetry-based: register the
LangfuseSpanProcessor once at process startup, and the tracer works out of
the box from then on.
Configure Langfuse with its standard environment variables:
LANGFUSE_PUBLIC_KEY=<your-langfuse-public-key>LANGFUSE_SECRET_KEY=<your-langfuse-secret-key>LANGFUSE_BASE_URL=https://us.cloud.langfuse.com # or your self-hosted hostnpm install @langfuse/otel @opentelemetry/sdk-trace-node// instrumentation: register the span processor before agents run.import { LangfuseSpanProcessor } from "@langfuse/otel";import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
export const langfuseSpanProcessor = new LangfuseSpanProcessor();
const tracerProvider = new NodeTracerProvider({ spanProcessors: [langfuseSpanProcessor],});tracerProvider.register();import { createAgent, createLangfuseContextTracer } from "agent-lattice";import { langfuseSpanProcessor } from "./instrumentation";
const tracer = createLangfuseContextTracer({ // Drained by tracer.flush()/close() so spans reach Langfuse before a // short-lived process exits. spanProcessor: langfuseSpanProcessor, tags: ["local-debug"],});
const agent = createAgent({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/anthropic", model: "deepseek-v4-flash", tracer,});
try { await agent.prompt("Trace this run.", { stream: false });} finally { await tracer.close?.();}startObservation defaults to the bundled @langfuse/tracing function; pass
startObservation only to inject a custom runtime or a test fake.
Langfuse receives one trace per SDK query:
| SDK event sequence | Langfuse projection |
|---|---|
run_start to result |
chain observation; it is the trace root unless parent_run_id nests it under another chain. |
model_request to assistant_message |
Child generation observation with the model attribute. |
tool_use to tool_result |
Child tool observation. |
team_message and other auxiliary events |
Auto-ended event observations on the active chain. |
The trace root observation carries the trace name, session.id, and
langfuse.trace.tags attributes — the same attributes
propagateAttributes writes on the active span — so traces stay grouped and
filterable in the Langfuse UI. Error results and tool errors set the
observation level to ERROR with a statusMessage.
Event types
Section titled “Event types”| Type | Description |
|---|---|
run_start |
Agent run metadata, including model and tool names. |
user_message |
User prompt or tool-result message added to the model context. |
model_request |
Model request context, including selected messages, tools, and stream mode. |
assistant_message |
Assistant message returned by the model client. |
tool_use |
Tool call requested by the assistant, including the tool’s description (since 0.17.0). |
tool_result |
Tool result produced by the SDK and sent back to the model. |
result |
Final query result, including success and error subtypes. |
The tracer uses a monotonically increasing seq per tracer instance. Use
session_id, run_id, and source to group entries in UIs or log pipelines.
Team tracing
Section titled “Team tracing”Pass a tracer in team.query() or createTeamRunner().query() options to
propagate the same trace sink into delegated agents.
for await (const event of team.query("Ask engineering to inspect tracing.", { tracer,})) { console.log(event);}One team.query() or team.prompt() call creates one Team-level root run. The
Lead’s initial work, each delegated Member run, and every later Lead run are
children of that root. They share one trace session_id, so LangSmith displays
the complete handoff as one trace instead of separate top-level traces.
The shared trace session does not replace an Agent’s own SDK session. Trace
metadata keeps that original value as agent_session_id, and emitted SDK
messages continue to use the Agent’s existing session_id.
Delegated agents include their runtime source:
{ "source": { "kind": "team_member", "name": "engineering", "member": "engineering", "mailbox": "engineering" }, "type": "result", "data": { "subtype": "success", "result": "Engineering result" }}This keeps the JSONL stream readable even when a root agent delegates through teams or nested teams.
Custom sinks
Section titled “Custom sinks”ContextTracer is intentionally small so hosts can write to their own storage or
observability pipeline. The port object exposes methods only — failOnError is
bound when the factory creates the tracer. Implement a custom sink with
defineContextTracer(). Requires 0.17.0 or later.
import { defineContextTracer } from "agent-lattice";
const tracer = defineContextTracer({ async onEvent(event) { // TODO: Replace with your own database insert. }, async flush() { // TODO: Replace with your own buffered-write flush. },});Pass failOnError: true to the factory only when trace durability is part of
your product contract. By default, tracing should be observational and should
not make agent runs fail.
Redaction
Section titled “Redaction”Trace entries can contain prompts, model context, tool inputs, and tool outputs.
Use redact for local policy before writing JSONL.
redact is a callback that you provide. Before the JSONL tracer writes each
trace event, the SDK passes the event to this callback. The callback can return
the original event, return a modified copy, or return undefined to skip the
event. This only changes what is written to the trace file; it does not change
the request sent to the model.
const tracer = createJsonlContextTracer({ path: ".agent-runs/session.jsonl", redact(event) { if (event.type === "model_request") { return { ...event, data: { ...event.data, messages: "[redacted]", }, }; } return event; },});In this example, model_request.data.messages is replaced with "[redacted]"
before persistence. That field can contain the user prompt, conversation
history, and tool results, so it is often the first place to apply a local
privacy policy. Other event types are returned unchanged.
Return undefined from redact to skip an event entirely:
const tracer = createJsonlContextTracer({ path: ".agent-runs/session.jsonl", redact(event) { if (event.type === "tool_result") { return undefined; } return event; },});