Skip to main content

goose_agent/
tool.rs

1use std::{
2    collections::HashSet,
3    sync::{Arc, Mutex},
4};
5
6use anyhow::Result;
7use async_trait::async_trait;
8use goose_provider_types::conversation::{
9    message::{Message, MessageContent, ToolRequest},
10    Conversation,
11};
12use rmcp::{
13    handler::server::router::tool::{AsyncTool, SyncTool, ToolBase},
14    model::{CallToolRequestParams, CallToolResult, ErrorData, JsonObject, Tool},
15};
16use serde_json::{json, Value};
17
18use crate::operation::{
19    applied, messages_since_kickoff, not_applicable, Emitter, Operation, OperationFuture,
20    OperationResult,
21};
22use goose_provider_types::maybe_send::{MaybeSend, MaybeSync};
23
24fn empty_input_schema() -> Arc<JsonObject> {
25    Arc::new(
26        serde_json::from_value(json!({
27            "type": "object",
28            "properties": {},
29            "additionalProperties": false
30        }))
31        .expect("empty tool input schema is an object"),
32    )
33}
34
35fn definition<T: ToolBase>() -> Tool {
36    let mut tool = Tool::new_with_raw(
37        T::name(),
38        T::description(),
39        T::input_schema().unwrap_or_else(empty_input_schema),
40    );
41    if let Some(title) = T::title() {
42        tool = tool.with_title(title);
43    }
44    if let Some(output_schema) = T::output_schema() {
45        tool = tool.with_raw_output_schema(output_schema);
46    }
47    if let Some(annotations) = T::annotations() {
48        tool = tool.with_annotations(annotations);
49    }
50    if let Some(icons) = T::icons() {
51        tool = tool.with_icons(icons);
52    }
53    if let Some(meta) = T::meta() {
54        tool = tool.with_meta(meta);
55    }
56    tool
57}
58
59fn pending_requests(requests: Vec<ToolRequest>, tool_names: &HashSet<&str>) -> Vec<ToolRequest> {
60    requests
61        .into_iter()
62        .filter(|request| {
63            request
64                .tool_call
65                .as_ref()
66                .map_or(true, |call| tool_names.contains(call.name.as_ref()))
67        })
68        .collect()
69}
70
71fn parameters<T: ToolBase>(arguments: Option<JsonObject>) -> Result<T::Parameter, ErrorData> {
72    if T::input_schema().is_none() {
73        return Ok(T::Parameter::default());
74    }
75
76    serde_json::from_value(Value::Object(arguments.unwrap_or_default())).map_err(|error| {
77        ErrorData::invalid_params(format!("failed to deserialize parameters: {error}"), None)
78    })
79}
80
81fn result<T: ToolBase>(output: Result<T::Output, T::Error>) -> Result<CallToolResult, ErrorData> {
82    let output = output.map_err(Into::into)?;
83    let value = serde_json::to_value(output).map_err(|error| {
84        ErrorData::internal_error(format!("failed to serialize tool output: {error}"), None)
85    })?;
86    Ok(CallToolResult::structured(value))
87}
88
89#[cfg(not(target_arch = "wasm32"))]
90async fn invoke_sync<S, T>(
91    session: S,
92    parameters: T::Parameter,
93) -> Result<CallToolResult, ErrorData>
94where
95    S: Send + Sync + 'static,
96    T: SyncTool<S> + 'static,
97{
98    tokio::task::spawn_blocking(move || result::<T>(T::invoke(&session, parameters)))
99        .await
100        .map_err(|error| {
101            ErrorData::internal_error(format!("synchronous tool task failed: {error}"), None)
102        })?
103}
104
105// wasm32 has no threads to move blocking work onto, so the tool runs inline
106// and cancellation cannot interrupt it.
107#[cfg(target_arch = "wasm32")]
108async fn invoke_sync<S, T>(
109    session: S,
110    parameters: T::Parameter,
111) -> Result<CallToolResult, ErrorData>
112where
113    S: MaybeSend + MaybeSync + 'static,
114    T: SyncTool<S>,
115{
116    result::<T>(T::invoke(&session, parameters))
117}
118
119/// Supplies tools whose definitions and implementations may vary by session.
120///
121/// A provider's tool names and their handlers must remain stable from an inference
122/// advertisement until every tool call produced by that inference has been handled.
123#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
124#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
125pub trait ToolProvider<S>: MaybeSend + MaybeSync {
126    async fn tools(&self, session: &S) -> Result<Vec<Tool>>;
127
128    async fn call(
129        &self,
130        session: &S,
131        request_id: &str,
132        call: CallToolRequestParams,
133        emit: &Emitter,
134    ) -> Result<CallToolResult, ErrorData>;
135}
136
137#[cfg(not(target_arch = "wasm32"))]
138type ToolHandler<S> = dyn for<'a> Fn(&'a S, Option<JsonObject>) -> OperationFuture<'a, Result<CallToolResult, ErrorData>>
139    + Send
140    + Sync;
141
142#[cfg(target_arch = "wasm32")]
143type ToolHandler<S> = dyn for<'a> Fn(
144    &'a S,
145    Option<JsonObject>,
146) -> OperationFuture<'a, Result<CallToolResult, ErrorData>>;
147
148struct RegisteredTool<S> {
149    definition: Tool,
150    handler: Arc<ToolHandler<S>>,
151}
152
153struct RegisteredToolProvider<S> {
154    tools: Vec<RegisteredTool<S>>,
155}
156
157#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
158#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
159impl<S> ToolProvider<S> for RegisteredToolProvider<S>
160where
161    S: MaybeSend + MaybeSync + 'static,
162{
163    async fn tools(&self, _session: &S) -> Result<Vec<Tool>> {
164        Ok(self
165            .tools
166            .iter()
167            .map(|tool| tool.definition.clone())
168            .collect())
169    }
170
171    async fn call(
172        &self,
173        session: &S,
174        _request_id: &str,
175        call: CallToolRequestParams,
176        _emit: &Emitter,
177    ) -> Result<CallToolResult, ErrorData> {
178        let tool = self
179            .tools
180            .iter()
181            .find(|tool| tool.definition.name == call.name)
182            .ok_or_else(|| {
183                ErrorData::invalid_params(format!("unknown tool {}", call.name), None)
184            })?;
185        (tool.handler)(session, call.arguments).await
186    }
187}
188
189/// An agent operation that advertises and dispatches tools.
190///
191/// Tools can be registered from rmcp's typed tool traits, or supplied at runtime
192/// by a [`ToolProvider`] whose definitions may vary by session.
193pub struct ToolOperation<S> {
194    registered: RegisteredToolProvider<S>,
195    providers: Vec<Arc<dyn ToolProvider<S>>>,
196    response: Mutex<Option<Message>>,
197}
198
199impl<S> ToolOperation<S>
200where
201    S: MaybeSend + MaybeSync + 'static,
202{
203    pub fn new() -> Self {
204        Self {
205            registered: RegisteredToolProvider { tools: Vec::new() },
206            providers: Vec::new(),
207            response: Mutex::new(None),
208        }
209    }
210
211    fn register(&mut self, tool: RegisteredTool<S>) {
212        if let Some(existing) = self
213            .registered
214            .tools
215            .iter_mut()
216            .find(|existing| existing.definition.name == tool.definition.name)
217        {
218            *existing = tool;
219        } else {
220            self.registered.tools.push(tool);
221        }
222    }
223
224    pub fn with_provider(mut self, provider: Arc<dyn ToolProvider<S>>) -> Self {
225        self.providers.push(provider);
226        self
227    }
228
229    pub fn with_sync_tool<T>(mut self) -> Self
230    where
231        S: Clone,
232        T: SyncTool<S> + MaybeSend + MaybeSync + 'static,
233    {
234        self.register(RegisteredTool {
235            definition: definition::<T>(),
236            handler: Arc::new(|session, arguments| {
237                let session = session.clone();
238                Box::pin(async move {
239                    let parameters = parameters::<T>(arguments)?;
240                    invoke_sync::<S, T>(session, parameters).await
241                })
242            }),
243        });
244        self
245    }
246
247    pub fn with_async_tool<T>(mut self) -> Self
248    where
249        T: AsyncTool<S> + MaybeSend + MaybeSync + 'static,
250    {
251        self.register(RegisteredTool {
252            definition: definition::<T>(),
253            handler: Arc::new(|session, arguments| {
254                Box::pin(async move {
255                    let parameters = parameters::<T>(arguments)?;
256                    result::<T>(T::invoke(session, parameters).await)
257                })
258            }),
259        });
260        self
261    }
262
263    async fn available_tools(&self, session: &S) -> Result<Vec<(Tool, &dyn ToolProvider<S>)>> {
264        let mut available = self
265            .registered
266            .tools(session)
267            .await?
268            .into_iter()
269            .map(|tool| (tool, &self.registered as &dyn ToolProvider<S>))
270            .collect::<Vec<_>>();
271        for provider in &self.providers {
272            for tool in provider.tools(session).await? {
273                if available
274                    .iter()
275                    .any(|(available, _)| available.name == tool.name)
276                {
277                    anyhow::bail!("multiple tool providers registered '{}'", tool.name);
278                }
279                available.push((tool, provider.as_ref()));
280            }
281        }
282        Ok(available)
283    }
284}
285
286impl<S> Default for ToolOperation<S>
287where
288    S: MaybeSend + MaybeSync + 'static,
289{
290    fn default() -> Self {
291        Self::new()
292    }
293}
294
295#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
296#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
297impl<S, E> Operation<S, E> for ToolOperation<S>
298where
299    S: MaybeSend + MaybeSync + 'static,
300    E: From<Message> + MaybeSend + 'static,
301{
302    fn name(&self) -> &'static str {
303        "tools"
304    }
305
306    async fn finalize_cancellation(
307        &self,
308        _session: &S,
309        _conversation: &Conversation,
310        emit: &Emitter,
311    ) -> Vec<E> {
312        let response = self.response.lock().unwrap().take();
313        match response {
314            Some(response) => vec![E::from(emit.message(response))],
315            None => Vec::new(),
316        }
317    }
318
319    async fn inference_tools(&self, session: &S) -> Result<Vec<Tool>> {
320        Ok(self
321            .available_tools(session)
322            .await?
323            .into_iter()
324            .map(|(tool, _)| tool)
325            .collect())
326    }
327
328    async fn run(
329        &self,
330        session: &S,
331        conversation: &Conversation,
332        emit: &Emitter,
333    ) -> Result<OperationResult<E>> {
334        let turn = messages_since_kickoff(conversation)?;
335        let answered: HashSet<&str> = turn
336            .iter()
337            .flat_map(Message::get_tool_response_ids)
338            .collect();
339        let pending = turn
340            .iter()
341            .flat_map(|message| message.content.iter())
342            .filter_map(MessageContent::as_tool_request)
343            .filter(|request| {
344                !request.was_executed_externally() && !answered.contains(request.id.as_str())
345            })
346            .cloned()
347            .collect::<Vec<_>>();
348        if pending.is_empty() {
349            return not_applicable();
350        }
351
352        let available = self.available_tools(session).await?;
353        let available_names = available
354            .iter()
355            .map(|(tool, _)| tool.name.as_ref())
356            .collect();
357        let pending = pending_requests(pending, &available_names);
358        if pending.is_empty() {
359            return not_applicable();
360        }
361
362        for request in pending {
363            let provider = match request.tool_call.as_ref() {
364                Ok(call) => available
365                    .iter()
366                    .find(|(tool, _)| tool.name == call.name)
367                    .map(|(_, provider)| *provider)
368                    .expect("pending requests were filtered by available tools"),
369                Err(_) => &self.registered as &dyn ToolProvider<S>,
370            };
371            let tool_result = match request.tool_call.as_ref() {
372                Err(error) => Err(error.clone()),
373                Ok(call) => {
374                    provider
375                        .call(session, &request.id, call.clone(), emit)
376                        .await
377                }
378            };
379            let mut output = self.response.lock().unwrap();
380            let message =
381                output.get_or_insert_with(|| Message::user().with_generated_id_if_missing());
382            message.add_tool_response_with_metadata(
383                request.id,
384                tool_result,
385                request.metadata.as_ref(),
386            );
387        }
388        let message = self
389            .response
390            .lock()
391            .unwrap()
392            .take()
393            .expect("tool responses retained");
394        let message = emit.message(message);
395        applied([E::from(message)])
396    }
397}