Skip to main content
Use this for agents that loop — model calls tools, tools return results, model calls again — until a stop condition is reached. The span stays open for the entire loop and closes when the wrapped function returns.
import { trace } from "@opentelemetry/api";
import { registerOTel, wrapAgent } from "@uselemma/tracing";

registerOTel();

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

async function executeTool(name: string, args: Record<string, unknown>) {
  return await tracer.startActiveSpan("tool.call", async (span) => {
    span.setAttribute("tool.name", name);
    span.setAttribute("tool.args", JSON.stringify(args));
    try {
      const result = await runTool(name, args);
      span.setAttribute("tool.result", String(result));
      return result;
    } catch (error) {
      span.recordException(error as Error);
      span.setAttribute("tool.status", "error");
      throw error;
    } finally {
      span.end();
    }
  });
}

const wrapped = wrapAgent("my-agent", async ({ onComplete }, input: { userMessage: string }) => {
  const history = [{ role: "user", content: input.userMessage }];

  while (true) {
    const response = await callModel(history);

    if (response.stopReason === "end_turn") {
      onComplete(response.text);
      return response.text;
    }

    for (const toolCall of response.toolCalls) {
      const result = await executeTool(toolCall.name, toolCall.args);
      history.push({ role: "tool", name: toolCall.name, content: String(result) });
    }
  }
});

const { result, runId } = await wrapped({ userMessage: "What is the weather in London?" });
Key points:
  • Each tool execution gets its own tool.call child span with name, args, and result.
  • The run span closes only after the whole loop finishes, so all turns are captured.
  • If the loop throws, the run span is ended and marked as errored automatically. Call recordError(err) / ctx.record_error(err) before re-raising if you want to attach the error explicitly.