pub enum Event {
Show 24 variants
RunStarted {
agent_def_hash: String,
input: Value,
labels: Option<BTreeMap<String, String>>,
},
ModelCallRequested {
seq: SequenceNumber,
request_hash: String,
request_body: Option<Value>,
},
ModelCallCompleted {
seq: SequenceNumber,
response: Value,
usage: TokenUsage,
},
ToolCallRequested {
seq: SequenceNumber,
tool: String,
input: Value,
effect: Effect,
idempotency_key: Option<String>,
},
ToolCallCompleted {
seq: SequenceNumber,
output: Value,
},
NowObserved {
now: OffsetDateTime,
},
RandomObserved {
value: u64,
},
Suspended {
reason: String,
input_schema: Value,
},
Resumed {
input: Value,
},
BudgetExceeded {
budget: Budget,
observed: f64,
},
RunCompleted {
output: Value,
},
RunFailed {
error: String,
},
RunAbandoned {
reason: Option<String>,
unresolved_write: Option<UnresolvedWrite>,
},
GraphRunStarted {
graph_hash: String,
input: Value,
labels: Option<BTreeMap<String, String>>,
forked_from: Option<ForkOrigin>,
},
NodeEntered {
node: String,
},
NodeExited {
node: String,
},
NodeSkipped {
node: String,
reason: String,
},
BranchTaken {
node: String,
case: String,
},
MapFannedOut {
node: String,
items: Value,
},
MapIterationStarted {
node: String,
index: u64,
child_run: String,
},
MapIterationJoined {
node: String,
index: u64,
},
FoldIterationStarted {
node: String,
index: u64,
},
FoldIterationJoined {
node: String,
index: u64,
},
FoldConverged {
node: String,
winner_index: u64,
reason: String,
},
}Expand description
Everything that can happen in a run.
Adjacently tagged: each event serializes as {"kind": "...", "payload": {...}}. The tag (kind) and the content (payload) live in separate
keys, which is the deliberate choice for a durable format. It never
collides with a payload field (a payload could legitimately contain a
field named kind), and it does not constrain payloads to be JSON objects
the way internal tagging would. The wire shape is a durability contract, so
it is spelled out rather than left to a default.
Variants§
RunStarted
A run began. Records the hash of the agent definition it ran under and the input it started with.
Fields
agent_def_hash: StringContent hash of the agent definition (model, prompt, tools, budget) this run executed.
labels: Option<BTreeMap<String, String>>Optional operator-supplied correlation tags for the run (for
example a build id or environment), set once at creation and
never rewritten. BTreeMap so the wire form serializes with keys
in sorted order regardless of insertion order, matching the
deterministic-serialization discipline the rest of this crate
holds to (see salvor_runtime::hash::canonical_json, which this
field is deliberately never fed into: labels are a tag, not part
of the run’s identity, and never enter agent_def_hash or
request_hash).
Absent by default. #[serde(default, skip_serializing_if = "Option::is_none")] follows the identical additive contract
request_body on Event::ModelCallRequested set: with no
labels supplied at creation, this field is omitted from the wire
form entirely, so an unlabeled run’s RunStarted serializes byte
for byte as it did before this field existed. Sanity bounds (at
most 16 labels, keys under 64 bytes, values under 256 bytes) are
enforced where a run is created, never here: a log already on
disk is trusted and replayed as recorded, whatever it holds.
ModelCallRequested
A model call was requested. Records the correlating sequence number and the hash of the request, so a later completion can be matched to it.
Fields
seq: SequenceNumberCorrelates this request with its Event::ModelCallCompleted.
request_hash: StringContent hash of the request sent to the model. This is the sole
replay-correlation key for the call; it is computed the same way
whether or not request_body is recorded, and the body never feeds
into it.
request_body: Option<Value>The full model request body, verbatim, recorded only when prompt
recording is opted into (per-agent record_prompts or the
SALVOR_RECORD_PROMPTS default). It exists so the inspector can
show the exact prompt sent.
Off by default, and for a reason: the body can hold user data and
secrets. When recording is off the field is None and, thanks to
skip_serializing_if, is omitted from the wire form, so the event
serializes byte for byte as it did before this field existed. The
body is purely informational: replay correlates on request_hash
alone and ignores whatever is (or is not) recorded here, so a log
captured with bodies replays identically to one captured without.
ModelCallCompleted
A model call completed. This is the captured nondeterministic boundary: once recorded, replay reads the response from here and never calls the model again.
Fields
seq: SequenceNumberCorrelates this completion with its Event::ModelCallRequested.
response: ValueThe model response, stored inline.
Inline is the deliberate choice: the full response lives in the log. Large multimodal outputs may later force a content-addressed blob store, at which point this field becomes the seam, holding a blob reference instead of the response itself.
usage: TokenUsageToken usage reported for the call.
ToolCallRequested
A tool call was requested. Records intent before execution, which is
what lets an unrecorded-but-attempted Effect::Write be detected on
resume.
Fields
seq: SequenceNumberCorrelates this request with its Event::ToolCallCompleted.
effect: EffectThe tool’s declared side-effect class, which governs retry and resume behavior for a call that did not complete.
idempotency_key: Option<String>The idempotency key for this attempt, when the tool has one.
A Effect::Read call needs none; an Effect::Idempotent retry
reuses this exact key so the provider collapses duplicates.
ToolCallCompleted
A tool call completed. Once recorded, replay reads the output from here and never calls the tool again, whatever its effect class.
Fields
seq: SequenceNumberCorrelates this completion with its Event::ToolCallRequested.
NowObserved
The value ctx.now() returned, captured once while the run executed
live. On replay the recorded value is returned again, bit for bit; the
clock is never consulted a second time. This is how orchestration code
gets to observe time without breaking the determinism constraint.
§Wire compatibility
This variant (together with Event::RandomObserved) was added after
the original ten. Adding variants is a read-compatible change, so
SCHEMA_VERSION stayed at 1; the constant’s docs carry the full
argument.
Fields
now: OffsetDateTimeThe observed time. RFC 3339 on the wire with nanosecond precision, so the value replays exactly as recorded.
RandomObserved
The value ctx.random() returned, captured once while the run
executed live. On replay the recorded value is returned again, bit for
bit; the random source is never consulted a second time.
Fields
value: u64Sixty-four raw bits from the runtime’s random source.
A u64 is the deliberate representation: JSON integers carry the
full 64-bit range exactly, so replay returns the identical bits.
Richer values (a float in a range, a choice from a list) must be
derived from these bits deterministically by the caller, never
drawn fresh.
Suspended
The run parked durably, awaiting input (for example, human approval). Records why and the schema the resume input must satisfy.
Fields
input_schema: ValueJSON Schema the Event::Resumed input is validated against.
Resumed
A suspended run resumed with the given input.
BudgetExceeded
A declared budget was exceeded. The run suspends rather than dies, so a human can raise the limit and resume.
Fields
observed: f64The observed value, interpreted in the units of budget.kind. An
f64 for the same reason Budget::limit is: exact for integral
token and step counts up to 2^53, and inherently fractional for
cost and wall time.
RunCompleted
The run finished successfully with this output.
RunFailed
The run terminated with an error.
RunAbandoned
The run was abandoned by an operator: deliberately retired without ever finishing or failing. A terminal event, appended by hand through the server’s abandon endpoint, never emitted by orchestration.
§Abandonment is not failure
This is a separate terminal from Event::RunFailed on purpose. A
failure says the run tried to continue and could not; an abandonment
says a human decided it should stop mattering (a husk that is dead
forever, or a run whose noise is no longer worth carrying in the
inbox). The two read differently everywhere downstream — the fold gives
abandonment its own status, and the surfaces treat it as a muted
resting state, never the failure ink. Keeping Event::RunFailed
untouched is the point: its recorded meaning must not shift.
§Wire compatibility
Added the same read-compatible way the deterministic-context and graph
events were: a new variant, so a log written before it contains none of
the kind and every event in it parses to the identical value under the
new build. SCHEMA_VERSION stays 1; the constant’s docs carry the
full argument. Both fields are additive-optional under the identical
#[serde(default, skip_serializing_if = "Option::is_none")] contract
the other optional payloads hold to, so a bare abandonment (no reason,
no dangling write) serializes with an empty payload object:
{"kind":"RunAbandoned","payload":{}}.
Fields
reason: Option<String>The operator’s optional note for why the run was abandoned. Absent by default: an abandonment with no reason omits the key.
unresolved_write: Option<UnresolvedWrite>Set only when the abandoned run was parked at a dangling write
(status NeedsReconciliation): the outstanding write intent’s
position and tool, recorded as evidence.
The abandonment never claims the write question was answered.
Abandoning a needs-reconciliation run is allowed precisely because
this field carries the honesty forward: the write may or may not
have taken effect, and the record says so by naming the intent that
was left unsettled rather than pretending a completion. Absent for
any run abandoned from a state with no dangling write, under the
same additive-optional contract as reason.
GraphRunStarted
A graph run began. The head of a run that executes a graph document rather than a single agent loop.
A separate variant from Event::RunStarted, not a field on it:
RunStarted requires exactly one agent_def_hash, but a graph run has
many agent hashes (one per agent node) and none at its head. It records
the hash of the frozen graph document and the run’s input; the node,
branch, and map markers below then narrate the walk. Downstream of this
crate a graph_hash is sha256:<64 lowercase hex> over the canonical
graph document, but this crate treats it as an opaque string and never
depends on salvor-graph to interpret it.
Fields
graph_hash: StringContent hash of the frozen graph document this run executes. Opaque
here; the runtime forms and checks it against salvor-graph.
labels: Option<BTreeMap<String, String>>Optional operator-supplied correlation tags, carried for parity
with Event::RunStarted::labels: grouping (a build id, an
environment) matters for graph runs exactly as it does for agent
runs. Same additive-optional contract as that field — absent by
default via #[serde(default, skip_serializing_if = "Option::is_none")], so an unlabeled graph run omits the key
entirely. A tag, never part of identity: never fed into any hash.
Sanity bounds are enforced where a run is created, never here; a log
on disk is replayed as recorded.
forked_from: Option<ForkOrigin>Set only when this run is a fork of an earlier run: the recorded
link back to its origin (see ForkOrigin). A fork is a new run
whose log opens with the origin’s prefix rewritten under the new
id, and this field at seq 0 is the durable fact that it descends
from that origin. Absent by default under the same additive-optional
contract, so an ordinary (non-forked) graph run omits the key.
NodeEntered
Execution entered a graph node. Marks the node as the run’s current
position; a later Event::NodeExited closes it.
NodeExited
Execution left a graph node, having produced its output.
NodeSkipped
A graph node was skipped: reached on the walk but deliberately not run (for example, a branch case that did not fire routes past its node). A skipped node WAS reached — it is recorded precisely so a projection can tell “skipped” apart from “never reached”, which is the absence of any event naming the node.
Fields
BranchTaken
A branch node routed: the named case fired. This is the sole recorded authority for which way a branch went. The executed path is read from these events, never inferred from which sibling nodes were skipped.
Fields
MapFannedOut
A map node fanned out: the resolved list of items to map over was determined and recorded. Recording the items here (rather than re-resolving on replay) is what makes the fan-out deterministic — the per-iteration child ids are derived from this recorded data.
Fields
MapIterationStarted
One iteration of a map fan-out started, as a child run. The child’s id is derived deterministically from recorded data (the parent run, the node, and the index), so replay reconstructs the identical id without drawing a fresh one.
Fields
MapIterationJoined
One iteration of a map fan-out joined back: its child run’s result was folded into the map node’s output. Joins are recorded in index order, never completion order, so the concurrency of the fan-out never influences the parent log’s byte sequence.
Fields
FoldIterationStarted
A fold node began one bounded iteration: a single revision pass of the
accumulate-and-refine loop the node models. A fold’s passes run
sequentially in the one log (never as child runs, unlike a map’s
iterations), so index is both the pass position and its recorded
order.
Fields
FoldIterationJoined
A fold iteration joined back: its pass result was folded into the fold
node’s accumulated value. Recorded in index order, exactly like
Event::MapIterationJoined; because a fold’s passes are sequential,
index order already is completion order.
Fields
FoldConverged
A fold node settled: its loop stopped and its join rule selected the
winning iteration. This is the sole recorded authority for WHICH pass
the fold’s output came from — the argmax of a best_by join is read
from winner_index, never inferred from the iteration order — exactly
as Event::BranchTaken is the sole authority for a branch’s route.
reason records WHY the loop ended (its stop predicate fired, the
iteration bound was reached, or a pass failed to improve), an opaque
audit string like Event::NodeSkipped’s.