How to Log Every Step of the Agent Loop
Most agent loop logging is useless — walls of text nobody reads until something breaks. Here's how to log an agent so you can actually debug it, with a structured schema that pays off.
def log_step(run_id, step, reply, results, state):
emit({
"run_id": run_id,
"step": step,
"tool_calls": [{"name": c.name, "args": c.args} for c in reply.tool_calls],
"observation_sizes": [len(str(r)) for r in results],
"tokens": reply.usage.total,
"latency_ms": reply.latency_ms,
"stop_reason": state.stop_reason,
"context_tokens": state.context_size,
})Most agent loop logging is worse than useless — it's a false sense of safety. Teams pipe the whole transcript to a log file, feel covered, and then discover when an agent misbehaves in production that they have megabytes of unstructured text and no way to answer the one question that matters: what did the agent actually do, step by step, and where did it go wrong? Dumping everything is not logging. It's hoarding. Useful agent loop logging is structured, queryable, and designed around the questions you'll ask at 2am, not the bytes you can capture.
The contrarian point is that more logging usually makes debugging harder, not easier. A wall of raw text buries the signal. What you want is one structured record per iteration, capturing the few fields that actually explain the agent's behavior, in a form you can filter and aggregate. Less text, more structure.
What Is Agent Loop Logging?
Agent loop logging is the practice of recording what happens on each iteration of the loop in a form you can later inspect and analyze. Done well, it turns an opaque agent into a glass box: for any run, you can see every decision, every tool call, every observation, and every stop condition, and you can query across runs to find patterns.
[object Object], ,[object Object],(,[object Object],):
emit({
,[object Object],: run_id,
,[object Object],: step,
,[object Object],: [{,[object Object],: c.name, ,[object Object],: c.args} ,[object Object], c ,[object Object], reply.tool_calls],
,[object Object],: [,[object Object],(,[object Object],(r)) ,[object Object], r ,[object Object], results],
,[object Object],: reply.usage.total,
,[object Object],: reply.latency_ms,
,[object Object],: state.stop_reason,
,[object Object],: state.context_size,
})What this does: emits one structured record per iteration with the fields that explain behavior — which tools ran, how big the observations were, tokens and latency, context size, and why the step stopped — as queryable data rather than prose.
The key is that each field answers a question you'll actually ask: which tool was called, how big was the result, how much did this step cost, why did the loop stop. Structured fields let you filter and aggregate; a prose dump doesn't.
Why It Matters
You can't debug or improve what you can't see. An agent that fails intermittently, costs more than expected, or occasionally loops is a mystery without per-step records — and a solved case with them. The difference between "the agent sometimes acts weird" and "on 4% of runs it repeats the search tool three times before stalling" is entirely a matter of whether you logged the right structured fields.
Good logging also changes what you can measure across runs. With one structured record per step, you can answer questions no single transcript reveals: what's the p95 step count, which tool fails most often, how does cost distribute, what fraction of runs hit the step cap. Those aggregates drive every real improvement, and they're only possible if the logs are structured data, not text.
There's a mindset shift buried in that. Once your logs are structured, the agent stops being software you operate and becomes a system you study — every run is a data point, every week is a dataset, and improvement becomes an empirical loop instead of a series of hunches. Teams that log well end up making decisions from evidence ("the search tool fails on 6% of runs, here's the pattern") where teams that log poorly argue from anecdote ("it felt slow yesterday"). The schema is what makes that difference possible.
⚡ Pro tip: Log a stable
run_idstepDesigning a Useful Log Schema
The schema is the whole design. Capture the fields that explain behavior and skip the noise.
Per step, you want: which tools were called and with what arguments, how large each observation was, tokens and latency for the step, the context size going in, and the stop reason if the loop ended. Per run, you want: the goal, the final outcome, total steps, total tokens, and total wall-clock time. Together these let you reconstruct any single run and analyze all of them.
[object Object], ,[object Object],(,[object Object],):
emit_summary({
,[object Object],: run_id,
,[object Object],: ,[object Object],(goal), ,[object Object],
,[object Object],: outcome, ,[object Object],
,[object Object],: steps,
,[object Object],: tokens,
,[object Object],: seconds,
})What this does: records one summary row per run with outcome and totals, so you can aggregate success rates, cost, and duration across thousands of runs — and group by task type via a goal hash without logging sensitive prompt content.
The two-level structure — a record per step and a summary per run — is what makes the logs answer both kinds of question. Step records let you replay one run in detail when it misbehaves; run summaries let you spot trends across thousands of runs before any single one becomes a complaint. Log only steps and you can debug individuals but can't see the forest; log only summaries and you can see the trend but can't diagnose the case behind it. You want both, joined by
run_id⚡ Pro tip: Emit logs as one JSON object per line, not multi-line pretty-printed blocks. Line-delimited JSON is what every log tool, query engine, and quick
grep | jq⚠️ Common mistake: Logging full raw observations and prompts on every step. It feels thorough and it wrecks you three ways: storage explodes, sensitive data leaks into logs, and the signal drowns in volume. Log observation sizes and hashes by default, and capture full content only behind a debug flag you switch on for a specific investigation. Thorough logging is about the right fields, not all the bytes.
Turning Logs Into Debugging
Logs only pay off if they answer questions fast. Design a few standard queries up front: show me every step of run X in order; show me all runs that hit the step limit this week; show me the tool with the highest failure rate; show me the cost distribution across runs. If your log format makes those one-liners, you'll debug in minutes; if it takes a script each time, you won't bother.
That last clause is the real design constraint. The test of a logging system isn't whether it captured the answer — a raw dump technically captures everything — but whether you can retrieve the answer faster than you can reproduce the bug. If answering "why did run X stall?" takes longer than just re-running the agent and watching, your logs have failed at their one job. Design the schema and the queries together, so the questions you'll ask are cheap to answer, not just theoretically answerable somewhere in the bytes.
⚡ Pro tip: Add an alert on your run-summary outcomes, not just a dashboard. A dashboard tells you something's wrong when you happen to look; an alert on a rising
step_limiterror⚡ Pro tip: Build a one-command "replay" that prints a single run's steps in order from the logs — step, tool, args, observation size, stop reason. Nine out of ten agent bugs become obvious the moment you see the sequence laid out cleanly, and a replay command means you see it in seconds instead of grepping a text dump.
Common Mistakes
Two more traps recur. Teams log inconsistently across the loop — some steps richly, error paths not at all — so the runs you most need to understand (the failures) are the ones with the least data. And teams never look at the logs until something breaks, missing that the same records make a great dashboard: plotting step counts, costs, and outcomes over time surfaces problems before they become incidents.
Conclusion
Agent loop logging is worth doing only if it's structured around the questions you'll ask: one queryable record per step, one summary per run, the fields that explain behavior and not the bytes that bury it. Log sizes and hashes by default and full content behind a flag; build the standard queries and a replay command up front. Done that way, an agent stops being a mystery you hope works and becomes a system you can actually see. And the payoff compounds: the same structured records that debug today's failure become the dataset that tells you, next quarter, which tool to replace, which task type to tune, and whether your last change actually helped.
The logging schema and the replay query are reusable across every agent you run. I keep them saved and versioned in PromptABCD next to the loop code, so a new agent is observable from its first run — instead of the whole-transcript dump that feels safe right up until you need to answer what actually happened and can't.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
