Skip to main content

goose_agent/
machine.rs

1use std::{
2    collections::HashSet,
3    sync::{Arc, Mutex},
4};
5
6use anyhow::{anyhow, Result};
7use async_trait::async_trait;
8use tokio_util::sync::CancellationToken;
9
10use crate::operation::{
11    messages_since_kickoff, ConversationEffect, Emitter, Inference, InferenceInput, MachineEffect,
12    Operation, OperationResult, RunStatus, StepResult,
13};
14use goose_provider_types::conversation::message::{Message, MessageContent};
15use goose_provider_types::conversation::Conversation;
16use goose_provider_types::maybe_send::{MaybeSend, MaybeSync};
17use goose_provider_types::thinking::ThinkingEffort;
18
19pub trait MachineSession: MaybeSend + MaybeSync {
20    fn id(&self) -> &str;
21    fn conversation(&self) -> Option<&Conversation>;
22    fn thinking_effort(&self) -> Option<ThinkingEffort> {
23        None
24    }
25}
26
27#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
28#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
29pub trait SessionLoader<S>: MaybeSend + MaybeSync {
30    async fn load(&self, session_id: &str) -> Result<S>;
31}
32
33#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
34#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
35pub trait EffectHandler<S, E>: MaybeSend + MaybeSync {
36    async fn apply_effects(&self, session: &S, effects: &mut [E], emit: &Emitter) -> Result<()>;
37}
38
39pub trait EffectUsage<E>: MaybeSend + MaybeSync {
40    fn usage(&self, _effect: &E) -> Option<goose_provider_types::conversation::token_usage::Usage> {
41        None
42    }
43}
44
45pub enum Step<'a, S, E = ConversationEffect> {
46    Operation(Arc<dyn Operation<S, E> + 'a>),
47    Inference(Arc<dyn Inference<S, E> + 'a>),
48}
49
50impl<S, E: MaybeSend> Step<'_, S, E> {
51    fn operation(&self) -> &dyn Operation<S, E> {
52        match self {
53            Step::Operation(operation) => operation.as_ref(),
54            Step::Inference(inference) => inference.as_ref(),
55        }
56    }
57}
58
59pub struct StateMachine<'a, S, E = ConversationEffect> {
60    steps: Vec<Step<'a, S, E>>,
61    cancel: CancellationToken,
62    interrupted_step: Mutex<Option<usize>>,
63}
64
65fn empty_result<E>(status: RunStatus) -> StepResult<E> {
66    StepResult {
67        effects: Vec::new(),
68        applied_step: None,
69        status,
70    }
71}
72
73fn interrupted_response(messages: &[Message]) -> Option<Message> {
74    let answered = messages
75        .iter()
76        .flat_map(Message::get_tool_response_ids)
77        .collect::<HashSet<_>>();
78    let mut request_ids = HashSet::new();
79    let mut response = Message::user();
80    for request in messages
81        .iter()
82        .flat_map(|message| &message.content)
83        .filter_map(MessageContent::as_tool_request)
84    {
85        if request_ids.insert(request.id.as_str()) && !answered.contains(request.id.as_str()) {
86            response.add_tool_response_with_metadata(
87                request.id.clone(),
88                Ok(rmcp::model::CallToolResult::error(vec![
89                    rmcp::model::ContentBlock::text("Tool call was interrupted before completing"),
90                ])),
91                request.metadata.as_ref(),
92            );
93        }
94    }
95    (!response.get_tool_response_ids().is_empty()).then_some(response)
96}
97
98fn add_tools_to_inference_input(
99    input: &mut InferenceInput,
100    tool_names: &mut HashSet<String>,
101    tools: Vec<rmcp::model::Tool>,
102) -> Result<()> {
103    for tool in tools {
104        if !tool_names.insert(tool.name.to_string()) {
105            anyhow::bail!("multiple operations registered tool '{}'", tool.name);
106        }
107        input.tools.push(tool);
108    }
109    Ok(())
110}
111
112impl<'a, S, E> StateMachine<'a, S, E>
113where
114    S: MachineSession,
115    E: MachineEffect + From<Message> + MaybeSend + 'static,
116{
117    pub fn new(steps: Vec<Step<'a, S, E>>, cancel: CancellationToken) -> Self {
118        Self {
119            steps,
120            cancel,
121            interrupted_step: Mutex::new(None),
122        }
123    }
124
125    pub async fn step(&self, session: &S, emit: &Emitter) -> Result<StepResult<E>> {
126        let conversation = session
127            .conversation()
128            .ok_or_else(|| anyhow!("state-machine session loaded without conversation"))?;
129
130        for (index, step) in self.steps.iter().enumerate() {
131            let name = step.operation().name();
132            if self.cancel.is_cancelled() {
133                return Ok(empty_result(RunStatus::Cancelled));
134            }
135            let execution = async {
136                match step {
137                    Step::Operation(operation) => operation.run(session, conversation, emit).await,
138                    Step::Inference(inference) => {
139                        let prepared_session = inference.prepare_session(session).await?;
140                        let session = prepared_session.as_ref().unwrap_or(session);
141                        let conversation = session.conversation().ok_or_else(|| {
142                            anyhow!("state-machine session loaded without conversation")
143                        })?;
144                        if !inference.applies(conversation) {
145                            return Ok(OperationResult::NotApplicable);
146                        }
147                        let mut input = InferenceInput::default();
148                        let mut tool_names = HashSet::new();
149                        for operation in self.steps.iter().map(|step| step.operation()) {
150                            let tools = operation.inference_tools(session).await?;
151                            add_tools_to_inference_input(&mut input, &mut tool_names, tools)?;
152                            input
153                                .prompt_parts
154                                .extend(operation.prompt_parts(session, conversation).await?);
155                            input
156                                .moim_parts
157                                .extend(operation.moim_parts(session, conversation).await?);
158                        }
159                        inference.infer(session, conversation, input, emit).await
160                    }
161                }
162            };
163            let result = tokio::select! {
164                biased;
165                result = execution => Some(result),
166                _ = self.cancel.cancelled() => None,
167            };
168            let result = match result {
169                Some(Ok(result)) => result,
170                Some(Err(error)) if !self.cancel.is_cancelled() => return Err(error),
171                _ => {
172                    *self.interrupted_step.lock().unwrap() = Some(index);
173                    return Ok(empty_result(RunStatus::Cancelled));
174                }
175            };
176
177            match result {
178                OperationResult::NotApplicable => {}
179                OperationResult::Applied(mut result) => {
180                    tracing::debug!(step = name, "applied step");
181                    result.applied_step = Some(name);
182                    for effect in &mut result.effects {
183                        effect.ensure_message_ids();
184                    }
185                    if self.cancel.is_cancelled() {
186                        result.status = RunStatus::Cancelled;
187                    }
188                    return Ok(result);
189                }
190            }
191        }
192
193        Ok(empty_result(RunStatus::Yielded))
194    }
195
196    pub async fn apply<R>(
197        &self,
198        runtime: &R,
199        session: &S,
200        effects: &mut [E],
201        emit: &Emitter,
202    ) -> Result<()>
203    where
204        R: EffectHandler<S, E>,
205    {
206        if effects.is_empty() {
207            return Ok(());
208        }
209        for effect in effects.iter_mut() {
210            effect.ensure_message_ids();
211        }
212        runtime.apply_effects(session, effects, emit).await
213    }
214
215    pub async fn run<R>(&self, runtime: &R, session_id: &str, emit: &Emitter) -> Result<S>
216    where
217        R: SessionLoader<S> + EffectHandler<S, E>,
218    {
219        loop {
220            let session = runtime.load(session_id).await?;
221            let mut result = self.step(&session, emit).await?;
222            self.apply(runtime, &session, &mut result.effects, emit)
223                .await?;
224            match result.status {
225                RunStatus::Continuing => {}
226                RunStatus::Yielded | RunStatus::Cancelled => break,
227            }
228        }
229        self.finalize(runtime, session_id, emit).await
230    }
231
232    pub async fn finalize<R>(&self, runtime: &R, session_id: &str, emit: &Emitter) -> Result<S>
233    where
234        R: SessionLoader<S> + EffectHandler<S, E>,
235    {
236        let mut session = runtime.load(session_id).await?;
237        if !self.cancel.is_cancelled() {
238            return Ok(session);
239        }
240
241        let interrupted_step = self.interrupted_step.lock().unwrap().take();
242        if let Some(index) = interrupted_step {
243            session = self
244                .finalize_step_cancellation(runtime, session_id, session, &self.steps[index], emit)
245                .await?;
246        }
247
248        let unanswered = session
249            .conversation()
250            .and_then(|conversation| messages_since_kickoff(conversation).ok())
251            .and_then(interrupted_response);
252        if let Some(response) = unanswered {
253            let effects = vec![E::from(emit.message(response))];
254            session = self
255                .save(runtime, session_id, session, effects, emit)
256                .await?;
257        }
258
259        for (index, step) in self.steps.iter().enumerate() {
260            if Some(index) != interrupted_step {
261                session = self
262                    .finalize_step_cancellation(runtime, session_id, session, step, emit)
263                    .await?;
264            }
265        }
266        Ok(session)
267    }
268
269    async fn finalize_step_cancellation<R>(
270        &self,
271        runtime: &R,
272        session_id: &str,
273        session: S,
274        step: &Step<'a, S, E>,
275        emit: &Emitter,
276    ) -> Result<S>
277    where
278        R: SessionLoader<S> + EffectHandler<S, E>,
279    {
280        let conversation = session
281            .conversation()
282            .ok_or_else(|| anyhow!("state-machine session loaded without conversation"))?;
283        let effects = step
284            .operation()
285            .finalize_cancellation(&session, conversation, emit)
286            .await;
287        self.save(runtime, session_id, session, effects, emit).await
288    }
289
290    async fn save<R>(
291        &self,
292        runtime: &R,
293        session_id: &str,
294        session: S,
295        mut effects: Vec<E>,
296        emit: &Emitter,
297    ) -> Result<S>
298    where
299        R: SessionLoader<S> + EffectHandler<S, E>,
300    {
301        if effects.is_empty() {
302            return Ok(session);
303        }
304        self.apply(runtime, &session, &mut effects, emit).await?;
305        runtime.load(session_id).await
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn rejects_duplicate_tools_across_operations() {
315        let schema = Arc::new(serde_json::Map::new());
316        let mut input = InferenceInput::default();
317        let mut names = HashSet::new();
318
319        add_tools_to_inference_input(
320            &mut input,
321            &mut names,
322            vec![rmcp::model::Tool::new("duplicate", "first", schema.clone())],
323        )
324        .unwrap();
325        let error = add_tools_to_inference_input(
326            &mut input,
327            &mut names,
328            vec![rmcp::model::Tool::new("duplicate", "second", schema)],
329        )
330        .unwrap_err();
331
332        assert_eq!(
333            error.to_string(),
334            "multiple operations registered tool 'duplicate'"
335        );
336    }
337}