Run log — structured execution timeline¶
A run log is an ordered, timestamped record of everything that happened during a graph run: which nodes ran, how long they took, transitions, and — crucially — the governance events (BudgetPolicy, StatePolicy, ApprovalGate). It answers the question production teams actually have: "what did my agent do, and why?"
It is the data foundation for replay, debugging, and any timeline/observability UI.
Enabling it¶
Attach a RunLogStore to the graph. The default InMemoryRunLogStore keeps events keyed by run id:
InMemoryRunLogStore log = new InMemoryRunLogStore();
AgentGraph graph = AgentGraph.builder()
.addNode("triage", triage)
.addNode("refund", refund)
.addEdge("triage", "refund")
.runLog(log)
.build();
graph.invoke(AgentContext.of("refund my order"), "ticket-4521");
Then read the timeline back by run id:
#0 NODE_ENTER node=triage
#1 NODE_EXIT node=triage (412ms)
#2 TRANSITION node=triage — triage→refund
#3 NODE_ENTER node=refund
#4 NODE_EXIT node=refund (1203ms)
#5 GRAPH_COMPLETE — completed
What gets recorded¶
| Event | When |
|---|---|
NODE_ENTER |
before a node executes |
NODE_EXIT |
after a node executes (carries durationNanos) |
NODE_ERROR |
a node failed (carries the message) |
TRANSITION |
the graph moved between nodes (detail from→to) |
APPROVAL_REQUIRED |
an ApprovalGate paused the run |
BUDGET_EXCEEDED |
a BudgetPolicy denied a call |
STATE_DENIED |
a StatePolicy blocked a write |
GRAPH_COMPLETE |
the run finished (completed, failed, interrupted) |
Each AgentRunEvent is a flat record (runId, sequence, epochMillis, type, node, detail, durationNanos) — trivially serializable to JSON, a log pipeline, or a UI.
Run ids¶
The runId is a general run identifier. graph.invoke(ctx, runId) uses it for both the run log and checkpointing — and now requires neither store:
- with a
RunLogStore→ events are queryable viagraph.runLog(runId) - with a
CheckpointStore→ the run is checkpointed/resumable - with both → correlated by the same id
- with neither → behaves like
invoke(ctx)but with your chosen id
Plain invoke(ctx) still records events under an internally generated id; use the runId overload when you want to query the log afterward.
Custom sinks¶
Implement RunLogStore to stream events somewhere durable:
class JsonlRunLogStore implements RunLogStore {
private final Path file;
@Override public void append(AgentRunEvent e) {
Files.writeString(file, toJson(e) + "\n", CREATE, APPEND);
}
// events(runId) / runIds() read them back
}
Implementations must be thread-safe — one graph instance can run concurrently on multiple threads, each with its own run id.
Relationship to observability¶
The run log is intentionally a small, in-process primitive. It is the substrate for:
- a replay/debugger view (re-read a finished run step by step)
- a live trace UI (render the event stream as nodes light up)
- OpenTelemetry export (map each event to a span)
Those build on top of this; the log itself stays dependency-free.