Skip to content

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.

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",
});

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:

Terminal window
LANGSMITH_TRACING=true
LANGSMITH_ENDPOINT=https://api.smith.langchain.com
LANGSMITH_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.

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:

Terminal window
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 host
Terminal window
npm 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.

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.

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.

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.

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;
},
});