salvor_tools/outcome.rs
1//! The success side of a tool call: a normal output, or a request to suspend
2//! the run.
3
4use serde_json::Value;
5
6/// A tool's request to park the run and wait for a human (or any out-of-band)
7/// input before continuing.
8///
9/// Suspension is a value a tool
10/// *returns*, not a runtime call available to orchestration. A tool that needs
11/// approval returns [`ToolOutcome::Suspend`] carrying one of these. The run
12/// parks durably; `salvor resume` later supplies an input that is validated
13/// against [`input_schema`](Self::input_schema) before the run continues.
14///
15/// The schema is a raw JSON Schema [`Value`] rather than a typed handle,
16/// because the tool decides at runtime what shape the resume input must take,
17/// and that shape can differ from one suspension to the next. A tool that
18/// wants a typed resume input can build the schema with
19/// `serde_json::to_value(schemars::schema_for!(T))`; the layer stores whatever
20/// `Value` it is given and does not interpret it.
21#[derive(Clone, Debug, PartialEq)]
22pub struct Suspension {
23 /// Why the run is parking, in human-readable form. This is what the
24 /// approval inbox shows the person who has to act.
25 pub reason: String,
26 /// The JSON Schema the resume input must satisfy. `salvor resume` validates
27 /// the supplied input against this before recording it and continuing.
28 pub input_schema: Value,
29}
30
31/// The `Ok` side of a tool call: either the tool's normal output, or a
32/// [`Suspension`].
33///
34/// Suspension is modeled here, on the success side, and not as a
35/// [`ToolError`](crate::ToolError). A parked run is a normal, expected outcome
36/// of a human-in-the-loop tool, not a failure, and the runtime loop treats the two
37/// branches differently: an [`Output`](Self::Output) feeds the next model
38/// turn, a [`Suspend`](Self::Suspend) records a `Suspended` event and parks the
39/// run. Encoding that split in the type keeps the loop from having to guess.
40///
41/// The typed layer produces `ToolOutcome<Self::Output>`; the type-erased layer
42/// produces `ToolOutcome<serde_json::Value>`. The [`Suspend`](Self::Suspend)
43/// branch is identical across both, so a suspension crosses the erasure
44/// boundary unchanged.
45#[derive(Clone, Debug, PartialEq)]
46pub enum ToolOutcome<T> {
47 /// The tool finished and produced this output.
48 Output(T),
49 /// The tool asks to park the run and wait for the described input.
50 Suspend(Suspension),
51}