When an agent gives a wrong answer, the first question is always the same: what did it actually do? Which tools did it call, in what order, and where did the time go?
If the only record is the chat transcript, you're guessing. A small trace logger fixes that, and you don't need a vendor to start. The code below is plain Node.js with no dependencies. It runs against a fake agent loop, so you can try it without an API key. I ran it on Node 24 and the assertions pass.
What a trace needs
Three things make a trace useful. Every step of one request shares an ID, so you can pull the whole story with one search. Every step records how long it took and whether it worked. And errors keep their message, because "something failed" isn't a clue.
I'd also log one line per step as JSON. Structured lines are easy to filter now and easy to ship to a real log system later.
Keep the trace ID visible to the user, too. When a customer writes in about a bad answer, a support person who can paste that ID into a search finds the whole run in seconds. Without it, they're matching timestamps by hand.
I work on AI Hive's agent platform, but this logger doesn't depend on it. It's the minimum I'd want in any agent before launch.
The logger
traced wraps any async step. It measures the time, records the outcome and rethrows the error, so wrapping a step never changes what your code does.
const { randomUUID } = require("node:crypto"); const events = []; function logEvent(event) { events.push({ ts: new Date().toISOString(), ...event }); } async function traced(traceId, step, fn) { const start = performance.now(); try { const result = await fn(); logEvent({ traceId, step, ms: Math.round(performance.now() - start), status: "ok" }); return result; } catch (err) { logEvent({ traceId, step, ms: Math.round(performance.now() - start), status: "error", error: String(err.message || err), }); throw err; } } function slowestSteps(traceId, top = 3) { return events .filter((e) => e.traceId === traceId) .sort((a, b) => b.ms - a.ms) .slice(0, top) .map((e) => `${e.step}: ${e.ms} ms (${e.status})`); } In a real service you'd write each event to a log stream instead of an array. The array keeps the example short and lets the test inspect it.
Two details are worth copying. The error branch is deliberate: it logs and then rethrows, so tracing never hides a failure from the caller. And performance.now() is used for durations because it isn't affected by clock changes the way Date.now() can be.
A fake agent to trace
The agent below plans, looks up an order, checks a policy that times out, and answers. Each step is wrapped, and the failing policy check doesn't stop the run.
const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function runAgent(question) { const traceId = randomUUID(); await traced(traceId, "plan", () => sleep(30)); const order = await traced(traceId, "tool:lookup_order", async () => { await sleep(120); return { id: "A1", status: "shipped" }; }); try { await traced(traceId, "tool:check_policy", async () => { await sleep(15); throw new Error("policy service timeout"); }); } catch (_) { // the agent carries on; the trace keeps the evidence } await traced(traceId, "final_answer", () => sleep(40)); return { traceId, answer: `Order ${order.id} is ${order.status}.` }; } Check that it works
This test runs the agent and confirms four steps were logged, one of them as an error, and that the slowest step is the order lookup.
const assert = require("node:assert"); (async () => { const { traceId, answer } = await runAgent("Where is my order?"); assert.equal(answer, "Order A1 is shipped."); const mine = events.filter((e) => e.traceId === traceId); assert.equal(mine.length, 4); assert.equal(mine.filter((e) => e.status === "error").length, 1); assert.ok(slowestSteps(traceId, 1)[0].startsWith("tool:lookup_order")); console.log(slowestSteps(traceId)); })(); What to look for in real traces
Once real traffic flows through, a few patterns show up quickly. One tool that's slow on every request is usually a quick win. A step that errors but gets swallowed, like the policy check above, is a bug waiting to surface. And a trace with many repeated calls to the same tool often means the agent is stuck in a loop.
Add the user's role and the tool arguments to each event when your data rules allow it. The more of the decision you can see, the faster you can explain it.
Be careful with what you write down, though. Tool arguments and model outputs can contain personal data, so redact before logging and set a retention period. A trace store that keeps everything forever becomes its own risk.
You can also turn the slowest-steps helper into an alert. If the median time for one tool doubles overnight, you'll want to hear about it before the first complaint arrives.
Where to go next
When you run several agents that call each other, one trace ID per request isn't enough, and you'll want parent and child relationships between steps. I covered that, along with how to read the results, in tracing and debugging multi-agent systems.
What's the first thing you check when an agent misbehaves?
