The agent changed a customer’s shipping address, then told support it had only updated the ticket note. The tool call succeeded. The answer sounded plausible. The support team found the problem after the package went to the wrong place.
When an AI agent makes a bad decision, “log the prompt and response” is not enough. You need to know what the agent was allowed to do, what context it received, which tool it selected, what arguments it generated, what the tool actually changed, and whether the system checked the result. You also need that evidence without turning your telemetry platform into a second database of customer secrets.
The goal of AI agent logging is not to preserve every token forever. It is to reconstruct the causal path from request to outcome, distinguish model behavior from application behavior, and give an incident reviewer enough safe evidence to fix the right layer.
Start with three records, not one transcript
Teams often call every event a “log,” but agent incidents need three related records:
Application logs answer operational questions: which request failed, how long it took, what error code occurred, and which deployment handled it.
Traces show the nested path of a single task: model call, retrieval, tool call, handoff, guardrail, retry, and final response. A trace helps explain timing and causality.
Audit events record consequential actions: who authorized the action, what resource changed, which policy allowed it, and the result. Audit events need stronger integrity and retention controls than ordinary debug logs.
These may share a correlation ID, but they should not be collapsed into one unstructured transcript. A prompt transcript is often the most sensitive record and the least useful way to answer “who approved this address change?”
OpenAI’s Agents SDK documentation illustrates the trace shape: its built-in tracing records agent runs, model generations, function calls, guardrails, handoffs, and custom events as spans under a trace. That is a useful model for any agent runtime, even if you export to another backend. (OpenAI Agents SDK, tracing)
Log the decision path, not just the final text
For a bad action, the final response can be misleading. The agent may claim it updated a record when the API rejected the write. Or it may say “done” after the tool succeeded against the wrong record. Record a structured event at each important boundary.
A minimal decision record should include:
| Field | Why it matters |
|---|---|
operation_id and trace_id |
Connect retries, spans, and audit events for one logical task |
tenant_id and actor reference |
Verify isolation and whose authority applied |
| application, prompt, model, and tool-schema versions | Identify the code and behavioral contract in effect |
| context source IDs and versions | Reconstruct which facts were available without copying all raw content |
| model decision or proposed action | Show what the agent intended to do |
| policy decision and rule version | Distinguish model proposal from application authorization |
| tool name, sanitized arguments, and result status | Reconstruct the attempted side effect |
| resource version before and after | Detect stale reads and concurrent writes |
| latency, token usage, retry count, and errors | Find timeouts, loops, and unusual cost |
| human approval or override | Establish whether a person reviewed the action |
| final outcome | Track whether the task actually succeeded |
The key distinction is proposal versus disposition. The model proposes an action. The policy layer allows, denies, or requests approval. The tool executes or fails. The audit event records the effect. If those are collapsed into agent_action=update_address, an incident review cannot tell whether the model chose the wrong target, authorization accepted an unsafe request, or the tool wrote to the wrong row.
1. Give every logical task a stable identity
An agent task may involve several model calls, a queue retry, a human approval, and a later continuation. A request ID generated independently for each API call will fragment the story.
Create a stable operation_id when the user starts the logical task. Reuse it across:
- model attempts and provider fallbacks;
- queue deliveries and workflow retries;
- tool calls and their compensations;
- human review and approval;
- the final application response.
Give individual attempts their own attempt_id and span_id, but keep them attached to the operation. This lets you answer both “what happened on attempt three?” and “what did the user’s operation eventually do?”
Do not put email addresses, names, or other personal identifiers into trace IDs. IDs propagate to analytics, metrics, and support tools; they should be random or opaque. Store the identity mapping in a system with appropriate access controls, and expose only a reference in general telemetry.
For writes, use the operation identity in your idempotency design as well as your logs. A trace ID alone is not an idempotency key, but both should let the application connect an action to the logical request that caused it.
2. Record the exact versions that shaped behavior
An incident cannot be reproduced from “model: current” and “prompt: support agent.” Record immutable identifiers:
- application build or commit;
- prompt ID, version, and content hash;
- requested and served model identifiers;
- sampling and generation settings relevant to the call;
- tool definitions and output-schema versions;
- retrieval corpus or index version;
- policy ruleset version;
- feature-flag assignment and rollout cohort.
If the provider supplies a request ID or backend fingerprint, record it. That metadata may help support escalation, but it is not a promise that the same output can be replayed later. Providers update serving infrastructure, and some systems are intentionally nondeterministic.
The version tuple is often enough to isolate the change. If the bad action began after a tool-schema update but before the prompt version changed, inspect argument interpretation first. If only one tenant is affected, inspect configuration and authorization scope before blaming the model.
Do not log an entire system prompt in every event just to identify it. An artifact hash plus a restricted prompt registry can answer which text ran while keeping the telemetry store smaller and safer.
3. Capture context provenance without dumping private content
When an agent makes a wrong decision, the context may have been wrong. The model may have received an outdated order status, an irrelevant document, or a summary that omitted a user correction.
Log provenance fields such as:
- document or record IDs;
- source version, timestamp, or content hash;
- retrieval rank or retrieval score where useful;
- truncation and summarization decisions;
- conversation branch or message IDs selected;
- state snapshot version;
- whether a field was absent, redacted, or defaulted.
This often answers the important question without storing raw text. If the review shows that source policy-42 at version v7 was used, an authorized investigator can retrieve the source from the system of record. A hash proves the content identity if it later changes.
There are cases where a short excerpt is necessary to explain a failure. Use a narrow allowlist and redact before export. Do not make raw prompt capture the default simply because it is convenient during development. Production logs tend to have broader access and longer retention than the source application.
4. Separate model proposal from policy verdict
For every tool call, log three distinct events:
- Proposed: the model selected a tool and produced arguments.
- Authorized: application policy evaluated actor, resource, action, and current state.
- Executed: the tool returned success, failure, or unknown outcome.
Example:
{
"event": "tool_action",
"operation_id": "op_6f2d8a",
"attempt_id": "att_02",
"tool": "update_shipping_address",
"proposal": {
"order_ref": "ord_3819",
"address_ref": "addr_18"
},
"policy": {
"decision": "allowed",
"ruleset": "shipping-write-v4",
"actor_scope": "customer:self"
},
"execution": {
"status": "succeeded",
"resource_version_before": 12,
"resource_version_after": 13
}
}
This example uses opaque references rather than the full address. If the user later reports a wrong change, the team can see what record and rule were involved, then retrieve protected details through an audited support path.
For a denied tool call, log the policy reason as a stable code such as tenant_mismatch or approval_required, not an unfiltered exception containing personal data. For an allowed call, preserve the rule version and the authorization subject. That is what proves the decision did not come solely from the model’s own text.
5. Record failures, retries, and uncertain outcomes
Agent loops often hide their cost and state transitions. Record each attempt, including:
- error class and provider status;
- whether the request timed out before a response;
- retry decision and reason;
- delay and
Retry-Aftervalue if present; - input and output token usage where available;
- cache read/write usage if relevant;
- cancellation requested and whether it was acknowledged;
- whether a side effect may have completed despite the error.
Do not reduce every timeout to failed. A client can stop waiting after the provider or downstream API has already completed the work. Use explicit states such as succeeded, failed, cancelled, and unknown. Unknown is not a cosmetic status; it tells the recovery path to reconcile before retrying.
The same log shape helps diagnose runaway loops. If the same tool and normalized arguments repeat, count the repetition and record the loop guard that stopped it. If the agent oscillates between two tools, a trace of tool names and argument hashes is more useful than a final summary that says “I could not complete the task.”
8. Preserve causal links across handoffs and queues
Agent work rarely stays in one process. A triage agent may hand off to a billing agent, which enqueues a review task that a worker handles several minutes later. If trace context is lost at the handoff, the final action appears unrelated to the original user request.
Propagate the operation ID and trace context through message headers or workflow metadata. Each agent or worker gets its own span, but all remain connected to the same logical operation. Record the handoff reason as a controlled category, such as billing_intent or needs_human_approval, rather than relying only on a model-generated explanation.
Do not infer causal order solely from wall-clock timestamps. Clock skew across machines can make a child operation appear to precede its parent. Use parent span IDs, sequence numbers within a workflow, or durable event history to reconstruct order. Include the queue job ID as a separate correlation field; a job can be redelivered, so its identity is not the same as the user's operation.
For parallel agent work, record the parent task and child task IDs, plus the artifact each child returned. That lets an investigator see whether a bad final decision came from a bad sub-result, a merge step, or the orchestrator’s choice to trust one result over another.
Measure the pipeline without turning IDs into metrics
Logs answer “which request?” Metrics answer “how often?” Keep high-cardinality values such as user IDs, operation IDs, document IDs, and raw errors in access-controlled logs or traces, not metric labels. Metric systems can multiply those values into enormous time series and expose identifiers to a broader audience.
Useful low-cardinality metrics include:
- tool calls by tool name, result class, and policy decision;
- proposed actions approved, edited, rejected, or timed out;
- retry attempts and unknown outcomes by operation type;
- handoffs by controlled reason;
- loop guard activations and repeated-call detections;
- latency and token usage by prompt/model version;
- compensation attempts and failures;
- user-visible completion and correction rates.
Sample successful traces as well as failed ones. If you inspect only incidents, you cannot tell whether a failure is rare or whether the same path succeeds most of the time. Use separate sampling policies: broad low-cost metrics for all traffic, a representative trace sample for normal operations, and higher-fidelity retention for risk-triggered or explicitly audited actions.
Sampling must not silently remove required audit records. A trace may be sampled; a required audit event for a completed payment should not be. Keep those pipelines separate even if they share an operation ID.
6. Make sensitive-data handling part of the logger
Logs and traces can contain more sensitive information than the database they describe. A function span may capture arguments and return values; a model-generation span may capture input and output. A careless trace exporter can copy private conversations into a third-party platform with a different retention policy.
Use an allowlist of fields for general telemetry. Redact or omit:
- passwords, API keys, bearer tokens, session cookies;
- payment and government identifiers;
- unnecessary names, addresses, and contact details;
- raw uploaded documents;
- secrets returned by tools;
- untrusted content that could contain exploit strings.
Keep detailed records only where an operational or audit purpose exists, restrict access, encrypt transport and storage, and define retention. If you need raw content for a narrow incident investigation, use a controlled, time-limited access path with an audit log rather than leaving the full trace available to every developer.
OpenAI Agents SDK tracing currently documents that generation spans and function spans can capture inputs and outputs, and that trace_include_sensitive_data is enabled by default. It can be disabled through run configuration or an environment setting. The same documentation notes that built-in tracing is unavailable for organizations using OpenAI APIs under Zero Data Retention (ZDR). These are important deployment-specific details, not universal tracing defaults. (OpenAI Agents SDK, sensitive data)
Redaction pipelines need careful composition. The SDK documentation warns that adding a redaction processor alongside the default exporter does not guarantee the exporter receives only redacted data if the redactor fails. If redaction must be a hard boundary, own the export path, fail closed when redaction fails, and do not let the original payload reach another processor. Test the failure path, not just the successful redaction case.
7. Keep observability and audit integrity separate
An observability trace is optimized for debugging and aggregation. It may be sampled, expired, or dropped under backpressure. An audit record for a financial or permission change may need durable storage, restricted mutation, and a defined retention period.
For consequential actions, write an audit event to an application-controlled store after the authorization and execution outcome are known. Include actor, approver if any, resource, operation, policy version, timestamp, and result. Protect it from ordinary application updates. If the event needs to be tamper-evident, use a design appropriate to your regulatory and threat model rather than assuming a JSON log file is immutable.
The trace can point to the audit event using an opaque ID. The audit event should not depend on a vendor trace still existing. Conversely, the audit store should not become a dump of full prompts and tool payloads; keep the decision facts needed to establish what happened.
OpenTelemetry’s GenAI semantic conventions are moving into a dedicated repository, so check the current convention status before standardizing attribute names across services. Semantic conventions improve interoperability, but they do not decide your redaction or retention policy. (OpenTelemetry GenAI semantic conventions)
A compact event schema
One way to make the design concrete is to use an event envelope with a small stable core and typed event-specific data:
from dataclasses import dataclass
from datetime import datetime
from typing import Any
@dataclass(frozen=True)
class AgentEvent:
event_id: str
operation_id: str
trace_id: str
event_type: str
occurred_at: datetime
tenant_ref: str
actor_ref: str
app_version: str
prompt_version: str
model_id: str | None
policy_version: str | None
data: dict[str, Any]
def safe_tool_event(
*, operation_id: str, trace_id: str, tenant_ref: str,
actor_ref: str, app_version: str, prompt_version: str,
tool_name: str, target_ref: str, policy_version: str,
decision: str, result_code: str,
) -> AgentEvent:
return AgentEvent(
event_id=new_event_id(),
operation_id=operation_id,
trace_id=trace_id,
event_type="tool.execution",
occurred_at=utc_now(),
tenant_ref=tenant_ref,
actor_ref=actor_ref,
app_version=app_version,
prompt_version=prompt_version,
model_id=None,
policy_version=policy_version,
data={
"tool": tool_name,
"target_ref": target_ref,
"policy_decision": decision,
"result_code": result_code,
},
)
The helper does not accept arbitrary tool arguments or raw result bodies. That is intentional: callers have to choose what is safe to record. A separate restricted store can retain additional details where required, but the default event is safe for broad operational search.
In a real system, validate the envelope with a schema, use a consistent timestamp source, and make event delivery resilient to process crashes. For actions with audit requirements, persist the event transactionally with the business write where possible, or use an outbox pattern so a successful write cannot lose its audit event.
Reproduce the decision without replaying the side effect
“Reproduce this incident” should not mean running the same production tool call again. Build a replay mode that uses a captured, privacy-approved input snapshot and recorded tool results, while replacing writes with a simulator. That lets an engineer test a prompt or policy change without issuing a second refund, sending another email, or editing the live customer record.
Replay will not always produce identical tokens. The model may have changed, sampling is nondeterministic, and the provider may not preserve the exact serving backend. The useful goal is to inspect whether the same input still selects the same class of action and whether current policy blocks the historical unsafe proposal.
Keep replay fixtures narrow. Store only the messages, state fields, and tool results required to reproduce the decision; mask unrelated personal content. Mark fixtures as syntheticized or restricted, attach an expiry, and delete them when the investigation no longer requires them. A replay dataset can become another long-lived copy of customer data if nobody owns its retention.
For deterministic application layers, replay the exact policy and tool validation code against the recorded proposal. That test can prove whether authorization should have denied the action even when the model itself cannot be reproduced byte for byte.
Incident review: follow the evidence in order
When a bad decision is reported, do not begin by editing the prompt. Walk the chain:
- Find the operation by its stable ID and confirm tenant and actor.
- Identify the application, prompt, model, tool-schema, and policy versions.
- Reconstruct the selected context and its source versions.
- Inspect the model proposal separately from the policy decision.
- Verify the exact tool target, normalized arguments, and resource version.
- Check whether retries, timeouts, or duplicate deliveries occurred.
- Confirm the side effect and any compensation in the system of record.
- Determine whether a human approved, overrode, or never saw the proposal.
- Classify the failure: context, prompt/model, authorization, tool, orchestration, or monitoring.
- Add a regression case and update the control at the layer that failed.
That sequence prevents a common misdiagnosis: the model is blamed for a write that the policy layer should have rejected, or the tool is blamed for a wrong identifier that retrieval supplied. Incident reviews should update prompts and tool contracts only when the evidence points there.
Logging mistakes that make an incident harder
| Mistake | Why it hurts | Better practice |
|---|---|---|
| Only log final answer | Hides tool calls and intermediate decisions | Trace the task with nested spans |
| Store raw prompts forever | Creates broad sensitive-data exposure | Keep version hashes and source references; minimize content |
| Use one request ID per attempt | Splits one logical task across records | Stable operation ID plus attempt IDs |
| Log “tool succeeded” only | Cannot identify target or policy | Record sanitized target, arguments, policy verdict, and result |
| Treat timeout as failure | May retry a completed side effect | Record unknown and reconcile |
| Put actor identity in trace ID | Leaks identifiers into metrics and exports | Use opaque correlation IDs |
| Rely on the tracing vendor as audit store | Sampling and retention may not meet audit needs | Keep application-owned audit events |
| Redact after default export | Original may already have been sent | Redact before every destination; own the export chain |
The incident-ready minimum
Before launching an agent with tools, verify that the system can answer these questions for one operation:
- Which user and tenant initiated it?
- Which prompt, model, tool schema, and policy version ran?
- What context sources were used, and which versions were they?
- What action did the model propose?
- What did authorization decide, and why?
- Which exact resource did the tool touch?
- Did a retry or timeout leave the result uncertain?
- What changed in the system of record?
- Was a person asked to approve or review it?
- Can the team retrieve the evidence without exposing unrelated customer data?
If you can answer those from structured, privacy-aware records, a bad agent decision becomes an engineering incident with a path to diagnosis. If the only evidence is a raw transcript, the team will debate what the model “meant” while the actual side effect remains unexplained.
Log enough to reconstruct the chain, not enough to copy the entire customer conversation into every system. Preserve the distinction between proposal, policy, execution, and outcome. That is the difference between an agent trace that looks detailed and an incident record that can actually tell you what went wrong.