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}