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, span lifecycle, and automatic I/O capture for you.
Helperspan.typeUse for
trace(name, fn)(none)General-purpose child span
tool(name, fn)toolTool / function execution
llm(name, fn)generationLLM call (when OpenInference is not used)
retrieval(name, fn)retrieverVector 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. Automatic I/O capture: every helper serializes the function’s input as input.value and its return value as output.value on the span. These appear as “Input” and “Output” in the Lemma trace view without any manual setAttribute calls. 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: vector-search  (span.type: retriever)
  const order = await lookup("123");         // span: lookup-order   (span.type: tool)
  const response = await generate(input);   // span: gpt-4o         (span.type: generation)
  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