1use std::fmt;
4use std::sync::Arc;
5
6use ferrin_message::Message;
7use ferrin_spec::BoxStream;
8use ferrin_spec::JsonValue;
9use ferrin_spec::ToolCallId;
10use futures_util::StreamExt;
11use tokio_util::sync::CancellationToken;
12
13use crate::error::ToolError;
14
15pub type ToolOutputStream = BoxStream<'static, Result<ToolOutput, ToolError>>;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum ToolOutput {
26 Preliminary(JsonValue),
28 Final(JsonValue),
30}
31
32impl ToolOutput {
33 #[must_use]
35 pub fn is_final(&self) -> bool {
36 matches!(self, Self::Final(_))
37 }
38
39 #[must_use]
41 pub fn value(&self) -> &JsonValue {
42 match self {
43 Self::Preliminary(value) | Self::Final(value) => value,
44 }
45 }
46
47 #[must_use]
49 pub fn into_value(self) -> JsonValue {
50 match self {
51 Self::Preliminary(value) | Self::Final(value) => value,
52 }
53 }
54}
55
56pub trait ToolExecute: Send + Sync {
63 fn execute(&self, input: JsonValue, ctx: ToolContext) -> ToolOutputStream;
65}
66
67#[derive(Clone)]
69pub struct ToolContext {
70 pub tool_call_id: ToolCallId,
72 pub messages: Arc<[Message]>,
75 pub cancellation: CancellationToken,
77 pub tools_context: Option<JsonValue>,
79 #[cfg(feature = "sandbox")]
81 pub sandbox: Option<Arc<dyn crate::sandbox::Sandbox>>,
82}
83
84impl fmt::Debug for ToolContext {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 let mut debug = f.debug_struct("ToolContext");
87 debug
88 .field("tool_call_id", &self.tool_call_id)
89 .field("messages", &self.messages.len())
90 .field("cancelled", &self.cancellation.is_cancelled())
91 .field("tools_context", &self.tools_context);
92 #[cfg(feature = "sandbox")]
93 debug.field(
94 "sandbox",
95 &self.sandbox.as_ref().map(|sandbox| sandbox.description()),
96 );
97 debug.finish()
98 }
99}
100
101impl ToolContext {
102 #[must_use]
104 pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
105 Self {
106 tool_call_id: tool_call_id.into(),
107 messages: Arc::from(Vec::new()),
108 cancellation: CancellationToken::new(),
109 tools_context: None,
110 #[cfg(feature = "sandbox")]
111 sandbox: None,
112 }
113 }
114
115 #[must_use]
117 pub fn with_messages(mut self, messages: impl Into<Arc<[Message]>>) -> Self {
118 self.messages = messages.into();
119 self
120 }
121
122 #[must_use]
124 pub fn with_cancellation(mut self, cancellation: CancellationToken) -> Self {
125 self.cancellation = cancellation;
126 self
127 }
128
129 #[must_use]
131 pub fn with_tools_context(mut self, tools_context: Option<JsonValue>) -> Self {
132 self.tools_context = tools_context;
133 self
134 }
135
136 #[cfg(feature = "sandbox")]
138 #[must_use]
139 pub fn with_sandbox(mut self, sandbox: Arc<dyn crate::sandbox::Sandbox>) -> Self {
140 self.sandbox = Some(sandbox);
141 self
142 }
143}
144
145pub async fn execute_to_completion(
153 mut stream: ToolOutputStream,
154 mut on_preliminary: impl FnMut(JsonValue),
155) -> Result<JsonValue, ToolError> {
156 while let Some(item) = stream.next().await {
157 match item? {
158 ToolOutput::Preliminary(value) => on_preliminary(value),
159 ToolOutput::Final(value) => return Ok(value),
160 }
161 }
162 Err(ToolError::message("tool produced no final output"))
163}