Use @uselemma/tracing with the Anthropic Node SDK to get a top-level run trace from agent plus a child span for every messages.create call — with prompts, completions, model, token usage, and timing.
How It Works
registerOTel() sets up the OTel transport. The AnthropicInstrumentation from OpenInference patches the anthropic package so every API call emits a child span inside whatever span is currently active — including inside agent.
Getting Started
Install
npm install @uselemma/tracing anthropic @opentelemetry/instrumentation @arizeai/openinference-instrumentation-anthropic
Register at startup
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { registerOTel } = await import("@uselemma/tracing");
const { registerInstrumentations } = await import("@opentelemetry/instrumentation");
const { AnthropicInstrumentation } = await import(
"@arizeai/openinference-instrumentation-anthropic"
);
const provider = registerOTel();
registerInstrumentations({
instrumentations: [new AnthropicInstrumentation()],
tracerProvider: provider,
});
}
}
// tracer.ts — import this first in your entry point
import { registerOTel } from "@uselemma/tracing";
import { registerInstrumentations } from "@opentelemetry/instrumentation";
import { AnthropicInstrumentation } from "@arizeai/openinference-instrumentation-anthropic";
const provider = registerOTel();
registerInstrumentations({
instrumentations: [new AnthropicInstrumentation()],
tracerProvider: provider,
});
// index.ts
import "./tracer"; // must be first
Set LEMMA_API_KEY and LEMMA_PROJECT_ID environment variables. Find them in your Lemma project settings.
Example
Single-turn completion
import { agent } from "@uselemma/tracing";
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
const supportAgent = agent("support-agent", async (input: { message: string }) => {
const response = await anthropic.messages.create({
model: "claude-haiku-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: input.message }],
});
return response.content[0].type === "text" ? response.content[0].text : "";
});
const { result, runId } = await supportAgent({ message: "Explain async/await in one sentence." });
import { agent, tool } from "@uselemma/tracing";
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
const getWeather = tool("get-weather", async (city: string) => {
return { city, temperature: "72°F", condition: "sunny" };
});
const weatherAgent = agent("weather-agent", async (input: { message: string }, ctx) => {
const toolSpec: Anthropic.Tool[] = [
{
name: "get_weather",
description: "Get current weather for a city",
input_schema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
];
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: input.message },
];
let response = await anthropic.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 1024,
tools: toolSpec,
messages,
});
while (response.stop_reason === "tool_use") {
const toolUseBlocks = response.content.filter((b) => b.type === "tool_use");
messages.push({ role: "assistant", content: response.content });
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of toolUseBlocks) {
if (block.type !== "tool_use") continue;
const weatherData = await getWeather(block.input.city as string);
toolResults.push({ type: "tool_result", tool_use_id: block.id, content: JSON.stringify(weatherData) });
}
messages.push({ role: "user", content: toolResults });
response = await anthropic.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 1024,
tools: toolSpec,
messages,
});
}
const textBlock = response.content.find((b) => b.type === "text");
const text = textBlock?.type === "text" ? textBlock.text : "";
return text;
});
const { result, runId } = await weatherAgent({ message: "What's the weather in Paris?" });
Streaming with ctx.complete()
For streaming, use messages.create({ stream: true }) rather than the higher-level messages.stream() helper — the latter can break with AnthropicInstrumentation in some versions. Consume the async iterable inside the wrapper and call ctx.complete() once the full text is assembled:
import { agent } from "@uselemma/tracing";
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
const streamingAgent = agent("streaming-agent", async (input: string, ctx) => {
let fullText = "";
const stream = await anthropic.messages.create({
model: "claude-haiku-4-5",
max_tokens: 512,
messages: [{ role: "user", content: input }],
stream: true,
});
for await (const event of stream) {
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
fullText += event.delta.text;
}
}
ctx.complete(fullText); // store assembled text as output, close span
return fullText;
}, { streaming: true });
What You’ll See in Lemma
| Span | Source | Contains |
|---|
ai.agent.run | agent | Full run input, output, timing, run ID |
gen_ai.chat | OpenInference | Model name, prompt messages, completion, token usage |
tool.get-weather | tool() helper | Tool input and return value |
Next Steps