Agent Loop docs
Event trace
EventTraceObserver records each conversation event as one line of JSON (JSONL). It shows
requests, responses, retries, and tool calls in order with their timing. Use it for debugging
or audit workflows when a conversation summary does not provide enough
detail.
With OpenAI configured as in Hello World, this example writes a trace to a temporary file and prints the event names from its JSONL records:
import json
import os
from pathlib import Path
from tempfile import TemporaryDirectory
from agentloop import Agent, EventTraceObserver
with TemporaryDirectory() as directory:
path = Path(directory) / "trace.jsonl"
with path.open("w") as log:
trace = EventTraceObserver(log.write)
with Agent.from_env(os.environ, tools=[], observer=trace) as agent:
agent.run("Reply with exactly ready.", correlation_id="example-1")
for line in path.read_text().splitlines():
record = json.loads(line)
shown = {"seq": record["seq"], "event": record["event"]}
if "summary" in record:
shown["status"] = record["summary"]["status"]
print(json.dumps(shown))
Output:
{"seq": 1, "event": "conversation_start"}
{"seq": 2, "event": "request"}
{"seq": 3, "event": "response"}
{"seq": 4, "event": "conversation_end", "status": "completed"}
The library never opens a file or selects a logging service. You provide the function that
receives each line, such as file.write or a queue's put. For a logger, pass its info
method and set terminator="" on EventTraceObserver.
What a line contains
By default, each line contains metadata only: sequence number, timestamp, elapsed time, event name, conversation identifiers, and relevant counts, names, sizes, or status values. It does not contain prompts, model responses, tool arguments, tool results, or error text.
The event names are conversation_start, request, response, retry, model_error,
tool_start, tool_end, stall, budget_warning, and conversation_end. retry reports an
attempt that will be retried. model_error reports a call that failed for good, with fields
such as error_type, status_code, phase, request_sent, request_id, attempts, and
attempt_log when available. Unknown top-level event fields are omitted, so a missing
request_sent means unknown, not false. The final event embeds the complete
ConversationSummary, whose optional fields may be null.
Capturing content
Pass a redaction function to capture prompts and responses. The example uses a simple tag to show where your application's redactor belongs:
import json
import os
from agentloop import Agent, EventTraceObserver
lines = []
def redact(text: str) -> str:
return text.replace("private-tag", "[REDACTED]")
trace = EventTraceObserver(lines.append, capture_content=True, redact=redact)
with Agent.from_env(os.environ, tools=[], observer=trace) as agent:
agent.run("Reply with exactly ready. My tag is private-tag.")
request = next(json.loads(line) for line in lines if '"event": "request"' in line)
print(request["new_messages"][0]["parts"][0]["content"]["text"])
print("private tag in trace:", "private-tag" in "".join(lines))
Output:
Reply with exactly ready. My tag is [REDACTED].
private tag in trace: False
With capture_content=True, the trace also carries the system prompt once, messages added
since the previous request, response content, tool
arguments and results, and error messages.
- Every captured string passes through
redact, then is clipped aftermax_string_chars(default 2000 characters), with a truncation marker appended. Redaction runs first, so a cut cannot expose part of a secret. - A redactor is required.
capture_content=Truewithout one raisesConfigurationErrorunless you passallow_unredacted=True. - The library ships no patterns. A built-in list of things that look like secrets would promise more than it can keep; plug in your organization's scrubber.
- It fails closed. If
redactraises an exception, that event is dropped and logged. It is never written without redaction. - Structural fields such as tool names, call IDs, roles, and part types are not passed through
redact. Review those fields before retaining a trace.
Using it with others
One EventTraceObserver supports one active conversation at a time. Create one instance for
each concurrent conversation. Combine it with other observers through MultiObserver.
Sequence numbers and output order remain consistent when tools run in parallel.