Skip to main content
Use manual instrumentation when you have a custom agent framework, need precise control over every span, or want to add structured child spans that OpenInference does not emit automatically. For setup (registerOTel()) and the run boundary (agent()), see Overview and Quickstart.
The @uselemma/tracing and uselemma_tracing packages export typed helpers that wrap functions with child spans automatically. Use these instead of raw startActiveSpan calls — they handle error recording and span lifecycle for you.
HelperSpan name prefixUse for
trace(name, fn)none (bare name)General-purpose child span
tool(name, fn)tool.Tool / function execution
llm(name, fn)llm.LLM call (when OpenInference is not used)
retrieval(name, fn)retrieval.Vector search, document retrieval
All helpers create child spans under the currently active context — they automatically nest under the enclosing agent() span without any extra wiring. In TypeScript, helpers are always called as tool("name", fn). In Python, they also work as decorators (@tool("name")).
import { agent, tool, llm, retrieval, trace } from "@uselemma/tracing";

const search = retrieval("vector-search", async (query: string) => {
  return vectorDB.search(query, { topK: 5 });
});

const lookup = tool("lookup-order", async (orderId: string) => {
  return db.orders.findById(orderId);
});

const generate = llm("gpt-4o", async (prompt: string) => {
  return openai.chat.completions.create({ model: "gpt-4o", messages: [...] });
});

const formatOutput = trace("format-output", async (raw: string) => raw.trim());

const myAgent = agent("my-agent", async (input: string) => {
  const docs = await search(input);         // span: retrieval.vector-search
  const order = await lookup("123");         // span: tool.lookup-order
  const response = await generate(input);   // span: llm.gpt-4o
  return formatOutput(response.text);        // span: format-output
});

LLM step spans (raw OTel)

For full control over step-level attributes (model, tokens, cost, finish reason), create child spans manually using the OpenTelemetry API:
import { trace } from "@opentelemetry/api";
import { agent } from "@uselemma/tracing";

const tracer = trace.getTracer("my-agent");

const wrapped = agent("support-agent", async (input: string) => {
  const answer = await tracer.startActiveSpan("llm.step.generate", async (stepSpan) => {
    const response = await llmCall(input);
    stepSpan.setAttribute("llm.model.requested", "gpt-4o");
    stepSpan.setAttribute("llm.tokens.prompt_uncached", 320);
    stepSpan.setAttribute("llm.tokens.completion", 140);
    stepSpan.setAttribute("llm.finish_reason", "stop");
    stepSpan.end();
    return response;
  });

  return answer;
});
If you use OpenInference instrumentation for your provider, these attributes are emitted automatically.

Tool call spans (raw OTel)

async function callWeatherTool(city: string) {
  return tracer.startActiveSpan("tool.call", async (toolSpan) => {
    toolSpan.setAttribute("tool.name", "get_weather");
    toolSpan.setAttribute("tool.args", JSON.stringify({ city }));
    try {
      const result = await getWeather(city);
      toolSpan.setAttribute("tool.result", JSON.stringify(result));
      toolSpan.end();
      return result;
    } catch (error) {
      toolSpan.recordException(error as Error);
      toolSpan.setAttribute("tool.status", "error");
      toolSpan.end();
      throw error;
    }
  });
}
For most tool-call use cases, the tool() helper above is simpler — use raw startActiveSpan only when you need explicit control over attributes like tool.args and tool.result.

Next Steps