Five Questions Your Agent Logs Must Answer After an Incident
When a service misbehaves, logs tell you what the code did. When an agent misbehaves, you need more: what the agent saw, what it chose, what it actually did, and whether anyone approved it. Standard application logging captures almost none of that, because the important decisions happen inside model calls your code treats as opaque.
The way to design agent logging is to start from the incident review and work backwards. After an agent does something wrong, the review will ask five questions. The logs should answer each of them without anyone re-running the agent.
1. What did it see?
The exact input to every model call: system prompt version, conversation so far, retrieved documents and tool results. Context is one place to investigate: a retrieved document with stale or injected instructions, a truncated tool result, a system prompt from the wrong version.
Logging full prompts on every call is expensive and risky, so log them by reference:
- a hash of the system prompt and tool definitions, which link back to the versioned files in your repo;
- content hashes and IDs for retrieved documents, plus the text itself stored separately with shorter retention;
- tool results in full, truncated at a limit you record (“truncated at 8,000 chars”), because truncation is itself a common cause of failures.
2. What did it decide?
The model’s output for each step: which tool it called with which arguments, or the text it produced, plus the stop reason. If your model returns reasoning or “thinking” content, decide explicitly whether to keep it. It helps with debugging, but it may also contain sensitive data from the context.
The stop reason matters more than it looks. A response that ended because it hit max_tokens and was treated as complete explains a surprising number of odd actions.
3. What did it actually do?
For every tool call, log the arguments the model produced and what the tool actually executed, as separate fields. These differ more often than you’d think: tools apply defaults, normalise inputs or reject calls. The model saying “delete ticket 123” and the tool deleting ticket 123 are separate facts. The second is the one with consequences.
Log the outcome too: success, definitive failure, unknown external outcome, or rejected by a guard. A timeout is not proof that an external action failed. A tool call your validation layer blocked is one of the most useful signals you have. It means the agent tried something it shouldn’t have, and nothing broke only because a guard held.
4. Who approved it?
For any action behind a human approval gate, record who approved, when, and exactly what they were shown. “Approved refund” means little if the approver saw a summary that left out the amount. Store the rendered approval request alongside the decision.
5. What did it cost?
Tokens in and out per call, model ID, latency, and the running total for the session. Cost belongs in the incident log because runaway cost is an incident class (a loop that makes 400 calls), and because the cost profile of a bad session often shows where it went wrong: the step where tokens per call jumped is usually where it went off track.
One event schema for all five
A single event type per step keeps querying simple. Emit it as structured JSON to whatever log store you already use:
{
"event": "agent_step",
"session_id": "ses_01J9...",
"step": 7,
"agent": "billing-investigator",
"agent_version": "2025-10-20.3",
"model": "claude-sonnet-4-5-20250929",
"prompt_hash": "sha256:4be1...",
"tools_hash": "sha256:9c0e...",
"input_refs": ["doc:kb-2291@sha256:77a1..."],
"input_tokens": 18422,
"output_tokens": 311,
"stop_reason": "tool_use",
"tool_call": {
"name": "issue_refund",
"args_requested": {"invoice": "INV-5531", "amount_cents": 4200},
"args_validated": {"invoice": "INV-5531", "amount_cents": 4200, "currency": "USD"},
"args_executed": null,
"outcome": "blocked",
"blocked_by": "approval_required",
"idempotency_key": "ses_01J9...:7"
},
"approval": null,
"latency_ms": 2140,
"session_cost_usd": 0.41
}With this in place, most incident questions become queries: every session where outcome = blocked, every step where stop_reason = max_tokens, every session over $5, every run of agent_version X.
If you already use OpenTelemetry, the same fields map onto spans and the GenAI semantic conventions. There’s a separate post on tracing. Whether it’s logs or spans matters less than capturing the five answers.
The sensitive-data problem
Agent logs collect more sensitive data than almost anything else you run, because the agent’s job is to read things. Customer messages, retrieved documents and tool results all pass through its context. Decide deliberately:
- Separate the metadata stream from the content stream. Even the schema above contains customer identifiers, amounts and operation keys. Redact or pseudonymize those fields and choose retention deliberately; metadata is not automatically nonsensitive. Full prompts and tool results are content. Keep them in a separate store with a short retention period and tighter access.
- Redact at write time, not read time. Once a secret is in the log store, it’s in backups and exports too. Scan tool results for credentials and obvious identifiers before logging.
- Restrict who can read content logs, and log that access. The people debugging agents shouldn’t automatically have access to every customer’s documents.
Where this stops
Good logs tell you what happened. They don’t tell you why the model chose it, beyond whatever reasoning it produced, and that reasoning isn’t guaranteed to reflect how the output was actually generated. Logs also don’t prevent anything. Their value is that an incident review ends with a specific, reproducible cause and a new test case, instead of “the model did something weird”.