1use anyhow::{anyhow, Result};
2use async_trait::async_trait;
3use futures::Stream;
4use std::future::Future;
5use std::pin::Pin;
6use tokio::sync::mpsc;
7use tokio_util::sync::CancellationToken;
8
9use crate::events::AgentEvent;
10use goose_provider_types::conversation::message::{Message, MessageContent, MessageErrorKind};
11use goose_provider_types::conversation::{effective_role, Conversation, EffectiveRole};
12use goose_provider_types::maybe_send::{MaybeSend, MaybeSync};
13use rmcp::model::Tool;
14
15#[cfg(not(target_arch = "wasm32"))]
16pub type OperationFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
17
18#[cfg(target_arch = "wasm32")]
19pub type OperationFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
20
21#[cfg(not(target_arch = "wasm32"))]
22pub type OperationStream<'a, T> = Pin<Box<dyn Stream<Item = T> + Send + 'a>>;
23
24#[cfg(target_arch = "wasm32")]
25pub type OperationStream<'a, T> = Pin<Box<dyn Stream<Item = T> + 'a>>;
26
27pub const CLIENT_LOG: &str = "client_log";
28
29pub struct SlashCommand<'a> {
30 pub command: &'a str,
31 pub params_str: &'a str,
32}
33
34pub fn messages_since_kickoff(conversation: &Conversation) -> Result<&[Message]> {
35 let messages = conversation.messages();
36 let is_kickoff = |msg: &Message| msg.role == rmcp::model::Role::User && !msg.is_tool_response();
37 let start = messages
38 .iter()
39 .rposition(|msg| is_kickoff(msg) && msg.is_user_visible())
40 .or_else(|| {
41 messages
42 .iter()
43 .rposition(|msg| is_kickoff(msg) && msg.is_agent_visible())
44 })
45 .ok_or_else(|| anyhow!("state machine conversation has no kickoff message"))?;
46 Ok(&messages[start..])
47}
48
49pub fn trailing_error(conversation: &Conversation) -> Option<MessageErrorKind> {
50 conversation.last().and_then(Message::error_kind)
51}
52
53pub fn last_effective_role(messages: &[Message]) -> Result<EffectiveRole> {
54 messages
55 .last()
56 .map(effective_role)
57 .ok_or_else(|| anyhow!("cannot determine the role of an empty conversation"))
58}
59
60pub fn assistant_turn_count(messages: &[Message]) -> u32 {
61 let mut turns = 0;
62 let mut in_assistant_block = false;
63 for message in messages.iter().rev() {
64 if message.role == rmcp::model::Role::Assistant {
65 if !in_assistant_block {
66 turns += 1;
67 in_assistant_block = true;
68 }
69 } else {
70 in_assistant_block = false;
71 }
72 }
73 turns
74}
75
76pub fn ends_turn(messages: &[Message]) -> bool {
77 messages.last().is_some_and(|last| {
78 last.role == rmcp::model::Role::Assistant
79 && last.error_kind().is_none()
80 && !last.content.iter().any(|content| {
81 matches!(
82 content,
83 MessageContent::ToolRequest(_) | MessageContent::ActionRequired(_)
84 )
85 })
86 })
87}
88
89#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
90#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
91pub trait Operation<S, E: MaybeSend + 'static = ConversationEffect>: MaybeSend + MaybeSync {
92 fn name(&self) -> &'static str;
93
94 fn set_message_meta(&self, message: &mut Message, key: &str, value: serde_json::Value) {
99 message.metadata.set_operation_note(self.name(), key, value);
100 }
101
102 fn message_meta<'a>(&self, message: &'a Message, key: &str) -> Option<&'a serde_json::Value> {
103 message.metadata.operation_note(self.name(), key)
104 }
105
106 async fn finalize_cancellation(
107 &self,
108 _session: &S,
109 _conversation: &Conversation,
110 _emit: &Emitter,
111 ) -> Vec<E> {
112 Vec::new()
113 }
114
115 async fn run_command(
116 &self,
117 _command: &SlashCommand<'_>,
118 _session: &S,
119 _conversation: &Conversation,
120 _emit: &Emitter,
121 ) -> Result<OperationResult<E>> {
122 not_applicable()
123 }
124
125 async fn inference_tools(&self, _session: &S) -> Result<Vec<Tool>> {
126 Ok(Vec::new())
127 }
128
129 async fn prompt_parts(
130 &self,
131 _session: &S,
132 _conversation: &Conversation,
133 ) -> Result<Vec<(String, String)>> {
134 Ok(Vec::new())
135 }
136
137 async fn moim_parts(&self, _session: &S, _conversation: &Conversation) -> Result<Vec<String>> {
138 Ok(Vec::new())
139 }
140
141 async fn run(
142 &self,
143 _session: &S,
144 _conversation: &Conversation,
145 _emit: &Emitter,
146 ) -> Result<OperationResult<E>> {
147 not_applicable()
148 }
149}
150
151#[derive(Default)]
152pub struct InferenceInput {
153 pub tools: Vec<Tool>,
154 pub prompt_parts: Vec<(String, String)>,
155 pub moim_parts: Vec<String>,
156}
157
158#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
159#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
160pub trait Inference<S, E: MaybeSend + 'static = ConversationEffect>: Operation<S, E> {
161 fn applies(&self, conversation: &Conversation) -> bool;
164
165 async fn prepare_session(&self, _session: &S) -> Result<Option<S>> {
166 Ok(None)
167 }
168
169 async fn infer(
170 &self,
171 session: &S,
172 conversation: &Conversation,
173 input: InferenceInput,
174 emit: &Emitter,
175 ) -> Result<OperationResult<E>>;
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum RunStatus {
180 Continuing,
181 Yielded,
182 Cancelled,
183}
184
185pub struct StepResult<E = ConversationEffect> {
186 pub effects: Vec<E>,
187 pub applied_step: Option<&'static str>,
188 pub status: RunStatus,
189}
190
191pub enum OperationResult<E = ConversationEffect> {
192 NotApplicable,
193 Applied(StepResult<E>),
194}
195
196pub fn not_applicable<E>() -> Result<OperationResult<E>> {
197 Ok(OperationResult::NotApplicable)
198}
199
200pub fn applied<E>(effects: impl IntoIterator<Item = E>) -> Result<OperationResult<E>> {
201 Ok(OperationResult::Applied(StepResult {
202 effects: effects.into_iter().collect(),
203 applied_step: None,
204 status: RunStatus::Continuing,
205 }))
206}
207
208pub fn yielded<E>() -> Result<OperationResult<E>> {
209 Ok(OperationResult::Applied(StepResult {
210 effects: Vec::new(),
211 applied_step: None,
212 status: RunStatus::Yielded,
213 }))
214}
215
216pub fn yielded_with<E>(effects: impl IntoIterator<Item = E>) -> Result<OperationResult<E>> {
217 Ok(OperationResult::Applied(StepResult {
218 effects: effects.into_iter().collect(),
219 applied_step: None,
220 status: RunStatus::Yielded,
221 }))
222}
223
224pub trait MachineEffect {
225 fn ensure_message_ids(&mut self);
226}
227
228pub enum ConversationEffect {
229 AppendMessage(Message),
230 ReplaceConversation(Conversation),
231 PatchToolRequestMeta {
232 tool_call_id: String,
233 patch: serde_json::Value,
234 },
235 SetMessageOperationNote {
236 message_id: String,
237 operation: String,
238 key: String,
239 value: serde_json::Value,
240 },
241 SetMessageVisibility {
242 message_id: String,
243 user_visible: bool,
244 agent_visible: bool,
245 },
246}
247
248impl MachineEffect for ConversationEffect {
249 fn ensure_message_ids(&mut self) {
250 let messages = match self {
251 ConversationEffect::AppendMessage(message) => std::slice::from_mut(message),
252 ConversationEffect::ReplaceConversation(conversation) => {
253 conversation.messages_mut().as_mut_slice()
254 }
255 _ => return,
256 };
257 for message in messages {
258 if message.id.is_none() {
259 message.id = Some(format!("msg_{}", uuid::Uuid::new_v4()));
260 }
261 }
262 }
263}
264
265impl From<Message> for ConversationEffect {
266 fn from(message: Message) -> Self {
267 ConversationEffect::AppendMessage(message)
268 }
269}
270
271impl From<Conversation> for ConversationEffect {
272 fn from(conversation: Conversation) -> Self {
273 ConversationEffect::ReplaceConversation(conversation)
274 }
275}
276
277pub struct Emitter {
278 tx: mpsc::UnboundedSender<AgentEvent>,
279 cancel: CancellationToken,
280}
281
282impl Emitter {
283 pub fn new(tx: mpsc::UnboundedSender<AgentEvent>, cancel: CancellationToken) -> Self {
284 Self { tx, cancel }
285 }
286
287 pub fn emit(&self, event: AgentEvent) {
288 let _ = self.tx.send(event);
289 }
290
291 pub fn message(&self, message: Message) -> Message {
292 let message = message.with_generated_id_if_missing();
293 self.emit(AgentEvent::Message(message.clone()));
294 message
295 }
296
297 pub fn cancel_token(&self) -> &CancellationToken {
298 &self.cancel
299 }
300
301 pub async fn cancelled(&self) {
302 self.cancel.cancelled().await
303 }
304}