1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//! The success side of a tool call: a normal output, or a request to park the
//! run, on a human gate or on a durable timer.
use SuspensionKind;
use Value;
use OffsetDateTime;
/// A tool's request to park the run and wait for a human (or any out-of-band)
/// input before continuing.
///
/// Suspension is a value a tool
/// *returns*, not a runtime call available to orchestration. A tool that needs
/// approval returns [`ToolOutcome::Suspend`] carrying one of these. The run
/// parks durably; `salvor resume` later supplies an input that is validated
/// against [`input_schema`](Self::input_schema) before the run continues.
///
/// The schema is a raw JSON Schema [`Value`] rather than a typed handle,
/// because the tool decides at runtime what shape the resume input must take,
/// and that shape can differ from one suspension to the next. A tool that
/// wants a typed resume input can build the schema with
/// `serde_json::to_value(schemars::schema_for!(T))`; the layer stores whatever
/// `Value` it is given and does not interpret it.
///
/// # Waiting on a person, or on a system
///
/// A tool that parks the run on a webhook, a callback, or another service
/// reporting back builds the suspension with [`on_signal`](Self::on_signal).
/// The park is mechanically identical either way, down to the recorded
/// events; what changes is the [`SuspensionKind`] the runtime records, which
/// is how a listing keeps a wait nobody can answer out of an approval inbox.
/// Build it with [`new`](Self::new) (or leave `kind` at `None`) for the
/// ordinary case, a person deciding.
/// A tool's request to park the run on a durable timer until an instant
/// arrives.
///
/// The timer counterpart of [`Suspension`], and a value a tool *returns* for
/// the same reason: a tool that has started work it cannot finish yet (a
/// rate-limited backend, a settlement window, a retry-after header) says when
/// to come back and returns [`ToolOutcome::Sleep`]. The run parks durably and
/// continues when something re-drives it at or after `wake_at`, with no input
/// and nothing for a human to supply.
///
/// # An instant, never a duration
///
/// The runtime records an instant (`SleepStarted { wake_at }`) and replay
/// matches it exactly, so the deadline has to be a value that reproduces. A
/// duration is not one: resolving it needs a clock, and a clock read on the
/// second drive gives a second, later deadline. So this carries the instant
/// the tool decided on.
///
/// That decision is a *live* read and is allowed to be one. A tool executes
/// only live; on every later drive its completion is replayed, never
/// re-executed. So a tool computing `now + 30 minutes` from the ambient clock
/// reads that clock exactly once in the run's life, and the instant it
/// produced is recorded in the completion (see the runtime's sleep sentinel)
/// and read back from the log forever after. That is the same property the
/// suspension sentinel gives a reason and a schema.
/// The `Ok` side of a tool call: the tool's normal output, or one of the two
/// ways it can ask to park the run ([`Suspension`], [`Sleep`]).
///
/// Both parks are modeled here, on the success side, and not as a
/// [`ToolError`](crate::ToolError). A parked run is a normal, expected outcome
/// of a human-in-the-loop or a wait-and-retry tool, not a failure, and the
/// runtime loop treats the branches differently: an [`Output`](Self::Output)
/// feeds the next model turn, a [`Suspend`](Self::Suspend) records a
/// `Suspended` event and waits for an input, a [`Sleep`](Self::Sleep) records
/// a `SleepStarted` and waits for an instant. Encoding that split in the type
/// keeps the loop from having to guess.
///
/// The typed layer produces `ToolOutcome<Self::Output>`; the type-erased layer
/// produces `ToolOutcome<serde_json::Value>`. The two park branches are
/// identical across both, so either crosses the erasure boundary unchanged.