salvor_replay/effect.rs
1//! Side-effect classification for tool calls.
2
3use serde::{Deserialize, Serialize};
4
5/// How a tool call may be retried and how it behaves on replay.
6///
7/// Effect classification does not change one thing: on replay, **any completed
8/// recorded call is read from the log and never re-executed, regardless of its
9/// effect class**. A completion is immutable history.
10///
11/// Effect classification governs calls that were **not** completed, the gap
12/// between an intent recorded in the log and a completion that never arrived:
13///
14/// - [`Effect::Read`] calls re-execute freely.
15/// - [`Effect::Idempotent`] calls retry with the same idempotency key.
16/// - [`Effect::Write`] calls are never automatically retried and surface for
17/// human reconciliation.
18///
19/// Serializes lowercase (`"read"`, `"idempotent"`, `"write"`), because the
20/// wire form is part of the durable event contract.
21///
22/// This type lives in `salvor-core` because events reference it;
23/// `salvor-tools` consumes it when it defines tool contracts.
24#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
25#[serde(rename_all = "lowercase")]
26pub enum Effect {
27 /// A call with no side effects. If it was not completed, it re-executes
28 /// freely, because running it again observes state without changing it.
29 Read,
30 /// A call safe to repeat under the same idempotency key. If it was not
31 /// completed, it retries with that same key, so the provider collapses
32 /// duplicate attempts into one effect.
33 Idempotent,
34 /// A call that changes external state and is not safe to repeat blindly.
35 /// If it was not completed, it is never automatically retried: a crash
36 /// between the recorded intent and a completion surfaces for human
37 /// reconciliation rather than a guess about whether the write landed.
38 Write,
39}