Skip to content

Observability

Observability is built into the core. State automatically writes trace.jsonl to the session directory whenever you use SessionStore. There is nothing to enable or configure — tracing is always on.

trace.jsonl format

Every significant event during a session is appended as a JSON line to trace.jsonl. Each line has a type field and a timestamp.

Event types

Type Description
run_start A new session.run() call begins
run_end A session.run() call completes (includes status)
message A message added to conversation (role: user, assistant, tool, system)
llm_start LLM API call initiated
llm_end LLM API call completed (includes usage)
tool_start Tool execution begins (includes tool name and args)
tool_end Tool execution completes (includes result)
tool_blocked Tool call denied by permissions or approval
compact Context compaction occurred (includes tokens before/after)
usage Cumulative usage snapshot

Sample trace

{"type": "run_start", "run_id": 1, "prompt": "List all tables.", "timestamp": "2026-02-21T10:00:00Z"}
{"type": "message", "role": "user", "content": "List all tables.", "timestamp": "2026-02-21T10:00:00Z"}
{"type": "llm_start", "model": "gpt-5-mini", "context_size": 150, "timestamp": "2026-02-21T10:00:00Z"}
{"type": "llm_end", "model": "gpt-5-mini", "input_tokens": 150, "output_tokens": 45, "cache_read_tokens": 0, "cost_usd": 0.0003, "timestamp": "2026-02-21T10:00:01Z"}
{"type": "tool_start", "tool_name": "bash", "tool_call_id": "call_abc", "tool_args": {"command": "psql -c '\\dt'"}, "timestamp": "2026-02-21T10:00:01Z"}
{"type": "tool_end", "tool_name": "bash", "tool_call_id": "call_abc", "success": true, "timestamp": "2026-02-21T10:00:01Z"}
{"type": "llm_start", "model": "gpt-5-mini", "context_size": 280, "timestamp": "2026-02-21T10:00:02Z"}
{"type": "llm_end", "model": "gpt-5-mini", "input_tokens": 280, "output_tokens": 90, "cache_read_tokens": 0, "cost_usd": 0.0006, "timestamp": "2026-02-21T10:00:03Z"}
{"type": "message", "role": "assistant", "content": "The database has 12 tables...", "timestamp": "2026-02-21T10:00:03Z"}
{"type": "run_end", "run_id": 1, "status": "completed", "timestamp": "2026-02-21T10:00:03Z"}

Session directory layout

Each session directory at ~/.config/rho-agent/sessions/<session_id>/ contains:

File Purpose
config.yaml AgentConfig snapshot (model, profile, system prompt, etc.)
trace.jsonl Append-only event log — source of truth for the session
meta.json Session metadata (id, status, timestamps, model, profile, first prompt)
cancel Sentinel file — presence signals cancellation request
pause Sentinel file — presence signals pause request
directives.jsonl Operator directives queued for the agent (JSON lines)

Monitor

For live observation and control of running agents, see the Monitor guide.

Offline inspection

trace.jsonl files are plain JSON lines and can be inspected with standard tools.

Using jq

# Count tool calls in a session
jq -s '[.[] | select(.type == "tool_start")] | length' trace.jsonl

# Show all LLM usage events
jq 'select(.type == "llm_end") | .usage' trace.jsonl

# List distinct tool names used
jq -r 'select(.type == "tool_start") | .tool_name' trace.jsonl | sort -u

# Get total input tokens across all LLM calls
jq -s '[.[] | select(.type == "llm_end") | .usage.input_tokens] | add' trace.jsonl

Using State.from_jsonl

Replay a trace file to restore full conversation state in Python:

from pathlib import Path
from rho_agent import State

trace = Path("~/.config/rho-agent/sessions/abc123/trace.jsonl").expanduser()
state = State.from_jsonl(trace.read_bytes())
print(f"Messages: {len(state.messages)}")
print(f"Usage: {state.usage}")
print(f"Status: {state.status}")
print(f"Runs completed: {state.run_count}")

Observers

The StateObserver protocol lets you attach custom side channels to receive events in real time. Any object with an on_event(event: dict) method satisfies the protocol.

from rho_agent import Agent, AgentConfig, Session

class MetricsCollector:
    def on_event(self, event: dict) -> None:
        if event["type"] == "llm_end":
            usage = event.get("usage", {})
            print(f"LLM call: {usage.get('input_tokens', 0)} in, {usage.get('output_tokens', 0)} out")
        elif event["type"] == "tool_end":
            print(f"Tool {event['tool_name']} completed in {event.get('duration_ms', 0)}ms")

agent = Agent(AgentConfig(profile="developer"))
session = Session(agent)
session.state.add_observer(MetricsCollector())

result = await session.run(prompt="Analyze the codebase.")

Observers receive every event that is written to trace.jsonl, making them suitable for live dashboards, metrics collection, alerting, or custom logging pipelines.

ATIF export

Session traces can be exported to ATIF (Agent Trajectory Interchange Format), a structured JSON schema for agent trajectories designed for SFT/RL training data.

# Export a session to stdout
rho-agent export <session-id> --dir ~/.config/rho-agent/sessions

# Write to a file
rho-agent export <session-id> -d ~/.config/rho-agent/sessions -o trajectory.json
from rho_agent.export.atif import trace_to_atif

trajectory = trace_to_atif(
    "path/to/trace.jsonl",
    session_id="abc123",
    model_name="gpt-5-mini",
)

The export maps trace events to ATIF steps, groups tool calls with their results, and populates per-step and session-level metrics. See rho_agent/export/ for details and the CLI reference for all flags.