Run events¶
Purpose¶
Observe a run while it executes: every provider call attempt — retried attempts included —, every tool execution, and every decoder correction boundary, delivered as typed events in execution order.
When to use¶
Structured logging, tracing spans, progress reporting, or metering that
needs to see a run as it happens rather than after it ends. Not when
post-hoc inspection is enough — Result.Messages remains the canonical
record, and text streaming already covers showing generation progress.
How it works¶
WithRunEvents registers one observer per agent. The run invokes it
synchronously on the caller's goroutine, inline with execution: no
goroutines, no buffers, no dropped events. The observer returns nothing,
so observation can never fail a run; an observer that must stop the run
cancels the run context. Run, RunWithHistory, and both streaming
variants emit the same events.
Two kinds mark lifecycle boundaries rather than executions:
EventDeferred replaces a deferred call's tool end when the run pauses
on it, and EventCanceled follows the tool end of the call that ended
the run with &tool.Canceled — the boundary after which nothing else
executes, and calls the stop prevented never emit at all.
Run-scoped observers¶
WithRunObserver registers the same observation for a single run, as a
run option — the routing a shared agent needs, where one agent serves
many requests and each request's events belong to that request:
result, err := agent.Run(ctx, runCtx, prompt,
golem.WithRunObserver(func(event golem.RunEvent) {
logger.Trace(requestID, event.Kind, event.ToolName)
}),
)
A run's observer composes with the agent's: the construction-scoped
observer fires first, then the run's, per event — global metering and
per-request tracing coexist. Accepted by Run and its history,
streaming, and deferred-resume variants; a nil observer observes
nothing.
Run and conversation identity¶
Every event carries the emitting run's RunID and the
ConversationID it continues, constant across the run — the same pair
the run's Result and, on failure, RunError.Partial report. That is
what makes one construction-scoped observer enough for a shared agent:
interleaved runs' events sort by event.RunID without per-run
closures, and events from the runs of one conversation group by
event.ConversationID. The minting and inheritance rules — run IDs are
never inherited, conversation IDs travel through history and storage —
are documented in Conversations and
history
and decided in ADR 0027.
Events arrive in deterministic execution order, and that order is a compatibility promise:
model_start/model_endbracket every provider call attempt, carrying the turn index and 1-based attempt number;model_endcarries the attempt's usage and error. A retried attempt is individually observable.tool_start/tool_endbracket every tool execution, carrying the call ID, tool name, raw arguments, and on end the result text or error. A correction rejection arrives as atool_endwhose error is a*model.ModelRetry.output_rejectedmarks a decoder rejection starting a correction round; itsAttemptnumbers the round that follows, and turn indexes restart with it.
Parallel tool groups stay deterministic: starts are emitted in model emission order before the group runs, ends in the same order after it completes — matching the result-evidence order rule.
Example¶
Run examples/run-events (offline, against a scripted fake model):
go run ./examples/run-events
agent, err := golem.New[string, string](client,
golem.DecodeFunc[string](func(_ context.Context, response model.Response) (string, error) {
return response.Message.Content, nil
}),
golem.WithTools[string, string](getPlayerName),
golem.WithRunEvents[string, string](func(event golem.RunEvent) {
fmt.Printf("%-16s turn=%d attempt=%d\n", event.Kind, event.Turn, event.Attempt)
}),
)
API surface¶
golem.WithRunEvents[Deps, Output](onEvent func(RunEvent)) Option[Deps, Output]golem.WithRunObserver(onEvent func(RunEvent)) RunOptiongolem.RunEvent{Kind, Turn, Attempt, CallID, ToolName, Args, Result, Err, Usage, RunID, ConversationID}golem.EventKind—golem.EventModelStart,golem.EventModelEnd,golem.EventToolStart,golem.EventToolEnd,golem.EventOutputRejected,golem.EventDeferred,golem.EventCanceledgolem.WithRunID(id string) RunOption,golem.WithConversationID(id string) RunOption,golem.NewID()— see Conversations and history
Gotchas¶
- The observer runs inline with model and tool execution: it must not block. Cancel the run context — don't stall the callback — to stop the run.
- Events are advisory observation; usage bounds and the canonical record
come from the run
Result. A streaming run emits the same events alongside its fragments, with a model turn's events bracketing its fragments. - Which fields carry meaning depends on the kind; the rest are zero.
- Decisions live in
docs/adr/0014-run-event-stream.md.