Skip to main content

lash_core/
tool_result.rs

1/// What a pending tool call does when its `deadline` elapses.
2#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
3#[serde(rename_all = "snake_case")]
4pub enum TimeoutBehavior {
5    /// Resolve the call as a timeout failure the model can observe and react to.
6    ErrorAsResult,
7    /// Fail the whole turn instead of feeding a timeout result back to the model.
8    FailTurn,
9}
10
11/// What a pending tool call signals about its out-of-band work when cancelled.
12#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum CancelHint {
15    /// Leave the external work running; cancellation only drops the wait.
16    Ignore,
17    /// Request that the external work be cancelled along with the wait.
18    CancelExternalWork,
19}
20
21/// Configuration carried by a [`ToolResult::Pending`] result: how long the runtime
22/// waits for the deferred outcome, and what to do if it times out or is cancelled.
23///
24/// Defaults to no deadline, [`TimeoutBehavior::ErrorAsResult`], and
25/// [`CancelHint::CancelExternalWork`]. Build one with [`PendingCompletion::new`] and
26/// the `with_*` adjusters.
27#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
28pub struct PendingCompletion {
29    /// Maximum time to wait for the deferred outcome. `None` waits indefinitely (until
30    /// the turn or process is otherwise cancelled).
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub deadline: Option<std::time::Duration>,
33    /// What the runtime does when `deadline` elapses without a resolution.
34    pub on_timeout: TimeoutBehavior,
35    /// What the runtime signals about out-of-band work if the call is cancelled.
36    pub on_cancel: CancelHint,
37}
38
39impl Default for PendingCompletion {
40    fn default() -> Self {
41        Self {
42            deadline: None,
43            on_timeout: TimeoutBehavior::ErrorAsResult,
44            on_cancel: CancelHint::CancelExternalWork,
45        }
46    }
47}
48
49impl PendingCompletion {
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    pub fn with_deadline(mut self, deadline: std::time::Duration) -> Self {
55        self.deadline = Some(deadline);
56        self
57    }
58
59    pub fn fail_turn_on_timeout(mut self) -> Self {
60        self.on_timeout = TimeoutBehavior::FailTurn;
61        self
62    }
63}
64
65/// The outcome a [`ToolProvider::execute`](crate::ToolProvider::execute) returns
66/// for a single call.
67///
68/// The variant a tool returns chooses its completion mode:
69///
70/// - [`ToolResult::Done`] — **active await**. The result is available inline and the
71///   runtime finalizes the call immediately. Construct it with [`ToolResult::ok`],
72///   [`ToolResult::err`], [`ToolResult::failure`], and friends.
73/// - [`ToolResult::Pending`] — **deferred / callback completion**. The tool has
74///   launched out-of-band work (a webhook, a human approval, another service) and the
75///   real outcome is delivered later against a completion key.
76///
77/// # The completion-key contract
78///
79/// Before returning [`ToolResult::Pending`], a tool **must** first obtain a completion
80/// key by calling [`ToolContext::completion_key`](crate::ToolContext::completion_key)
81/// (reachable through `call.context`). That key names the durable wait the runtime parks
82/// the call on, and is what an external resolver uses to deliver the outcome. Returning
83/// `Pending` *without* having taken a completion key fails the call with the internal
84/// error `pending_tool_missing_completion_key`.
85///
86/// ```ignore
87/// async fn execute(&self, call: ToolCall<'_>) -> ToolResult {
88///     // Take the key first, then hand it to whatever completes the work out-of-band.
89///     let key = match call.context.completion_key().await {
90///         Ok(key) => key,
91///         Err(err) => return ToolResult::err_fmt(err),
92///     };
93///     enqueue_external_work(key);
94///     ToolResult::pending(PendingCompletion::new())
95/// }
96/// ```
97#[derive(Clone, Debug, PartialEq)]
98pub enum ToolResult {
99    /// Active await: the tool finished inline; this is its final output.
100    Done(Box<crate::ToolCallOutput>),
101    /// Deferred completion: the tool parked on a durable wait keyed by the
102    /// [`ToolContext::completion_key`](crate::ToolContext::completion_key) it took
103    /// before returning. The outcome arrives later through the resolve seam and is
104    /// shaped by the carried [`PendingCompletion`].
105    Pending(PendingCompletion),
106}
107
108impl ToolResult {
109    pub fn from_output(output: crate::ToolCallOutput) -> Self {
110        Self::Done(Box::new(output))
111    }
112
113    pub fn pending(pending: PendingCompletion) -> Self {
114        Self::Pending(pending)
115    }
116
117    pub fn ok(result: serde_json::Value) -> Self {
118        Self::from_output(crate::ToolCallOutput::success(result))
119    }
120
121    pub fn err(result: serde_json::Value) -> Self {
122        let message = result
123            .as_str()
124            .map(ToOwned::to_owned)
125            .unwrap_or_else(|| result.to_string());
126        Self::from_output(crate::ToolCallOutput::failure(crate::ToolFailure {
127            class: crate::ToolFailureClass::Execution,
128            code: "tool_error".to_string(),
129            message,
130            source: crate::ToolFailureSource::Tool,
131            retry: crate::ToolRetryDisposition::Never,
132            raw: Some(crate::ToolValue::from(result)),
133        }))
134    }
135
136    pub fn err_fmt(msg: impl std::fmt::Display) -> Self {
137        Self::err(serde_json::json!(msg.to_string()))
138    }
139
140    pub fn failure(failure: crate::ToolFailure) -> Self {
141        Self::from_output(crate::ToolCallOutput::failure(failure))
142    }
143
144    pub fn retryable_failure(
145        class: crate::ToolFailureClass,
146        code: impl Into<String>,
147        message: impl Into<String>,
148        after_ms: Option<u64>,
149    ) -> Self {
150        Self::failure(crate::ToolFailure::safe_retry(
151            class, code, message, after_ms,
152        ))
153    }
154
155    pub fn cancelled(message: impl Into<String>) -> Self {
156        Self::from_output(crate::ToolCallOutput::cancelled(
157            crate::ToolCancellation::runtime(message),
158        ))
159    }
160
161    pub fn cancelled_with_raw(message: impl Into<String>, raw: serde_json::Value) -> Self {
162        let mut cancellation = crate::ToolCancellation::runtime(message);
163        cancellation.raw = Some(crate::ToolValue::from(raw));
164        Self::from_output(crate::ToolCallOutput::cancelled(cancellation))
165    }
166
167    pub fn with_control(mut self, control: crate::ToolControl) -> Self {
168        if let Self::Done(output) = &mut self {
169            output.as_mut().control = Some(control);
170        }
171        self
172    }
173
174    pub fn is_success(&self) -> bool {
175        matches!(self, Self::Done(output) if output.is_success())
176    }
177
178    pub fn is_pending(&self) -> bool {
179        matches!(self, Self::Pending(_))
180    }
181
182    pub fn value_for_projection(&self) -> serde_json::Value {
183        match &self
184            .as_done_output()
185            .expect("pending tool result has no projection value")
186            .outcome
187        {
188            crate::ToolCallOutcome::Success(value) => value.to_json_value(),
189            crate::ToolCallOutcome::Failure(failure) => failure
190                .raw
191                .as_ref()
192                .map(crate::ToolValue::to_json_value)
193                .unwrap_or_else(|| failure.to_json_value()),
194            crate::ToolCallOutcome::Cancelled(cancellation) => cancellation
195                .raw
196                .as_ref()
197                .map(crate::ToolValue::to_json_value)
198                .unwrap_or_else(|| cancellation.to_json_value()),
199        }
200    }
201
202    pub fn as_done_output(&self) -> Option<&crate::ToolCallOutput> {
203        match self {
204            Self::Done(output) => Some(output.as_ref()),
205            Self::Pending(_) => None,
206        }
207    }
208
209    pub fn as_output(&self) -> &crate::ToolCallOutput {
210        self.as_done_output()
211            .expect("pending tool result cannot be viewed as completed output")
212    }
213
214    pub fn into_done_output(self) -> Result<crate::ToolCallOutput, PendingCompletion> {
215        match self {
216            Self::Done(output) => Ok(*output),
217            Self::Pending(pending) => Err(pending),
218        }
219    }
220}
221
222impl<T, E> From<Result<T, E>> for ToolResult
223where
224    T: serde::Serialize,
225    E: std::fmt::Display,
226{
227    fn from(result: Result<T, E>) -> Self {
228        match result {
229            Ok(value) => match serde_json::to_value(value) {
230                Ok(value) => Self::ok(value),
231                Err(err) => Self::err_fmt(format_args!("Failed to serialize tool result: {err}")),
232            },
233            Err(err) => Self::err_fmt(err),
234        }
235    }
236}
237
238pub(crate) fn tool_output_from_completion_resolution(
239    resolution: crate::Resolution,
240) -> crate::ToolCallOutput {
241    match resolution {
242        crate::Resolution::Ok(value) => crate::ToolCallOutput::success(value),
243        crate::Resolution::Err(err) => {
244            let mut failure =
245                crate::ToolFailure::tool(crate::ToolFailureClass::Execution, err.code, err.message);
246            failure.raw = err.raw.map(crate::ToolValue::from);
247            crate::ToolCallOutput::failure(failure)
248        }
249        crate::Resolution::Timeout => crate::ToolCallOutput::failure(crate::ToolFailure::runtime(
250            crate::ToolFailureClass::Timeout,
251            "tool_completion_timeout",
252            "pending tool completion timed out",
253        )),
254        crate::Resolution::Cancelled => crate::ToolCallOutput::cancelled(
255            crate::ToolCancellation::runtime("pending tool completion cancelled"),
256        ),
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use serde::ser::{Error as _, Serializer};
263
264    use super::*;
265
266    #[test]
267    fn tool_result_from_result_serializes_success_values() {
268        let result: ToolResult = Result::<_, std::io::Error>::Ok(vec!["alpha", "beta"]).into();
269        assert!(result.is_success());
270        assert_eq!(
271            result.value_for_projection(),
272            serde_json::json!(["alpha", "beta"])
273        );
274    }
275
276    #[test]
277    fn tool_result_from_result_formats_errors() {
278        let result: ToolResult =
279            Result::<serde_json::Value, _>::Err(std::io::Error::other("nope")).into();
280        assert!(!result.is_success());
281        assert_eq!(result.value_for_projection(), serde_json::json!("nope"));
282        assert_eq!(
283            result.as_output().value_for_projection()["message"],
284            serde_json::json!("nope")
285        );
286    }
287
288    #[test]
289    fn tool_result_from_result_reports_serialize_failures() {
290        struct BrokenValue;
291
292        impl serde::Serialize for BrokenValue {
293            fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
294            where
295                S: Serializer,
296            {
297                Err(S::Error::custom("boom"))
298            }
299        }
300
301        let result: ToolResult = Result::<BrokenValue, std::io::Error>::Ok(BrokenValue).into();
302        assert!(!result.is_success());
303        assert_eq!(
304            result.value_for_projection(),
305            serde_json::json!("Failed to serialize tool result: boom")
306        );
307    }
308
309    #[test]
310    fn pending_result_is_not_completed_output() {
311        let result = ToolResult::pending(PendingCompletion::new());
312        assert!(result.is_pending());
313        assert!(result.as_done_output().is_none());
314        assert!(result.into_done_output().is_err());
315    }
316}