use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::Stream;
use std::future::Future;
use std::pin::Pin;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::events::AgentEvent;
use goose_provider_types::conversation::message::{Message, MessageContent, MessageErrorKind};
use goose_provider_types::conversation::{effective_role, Conversation, EffectiveRole};
use goose_provider_types::maybe_send::{MaybeSend, MaybeSync};
use rmcp::model::Tool;
#[cfg(not(target_arch = "wasm32"))]
pub type OperationFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[cfg(target_arch = "wasm32")]
pub type OperationFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
#[cfg(not(target_arch = "wasm32"))]
pub type OperationStream<'a, T> = Pin<Box<dyn Stream<Item = T> + Send + 'a>>;
#[cfg(target_arch = "wasm32")]
pub type OperationStream<'a, T> = Pin<Box<dyn Stream<Item = T> + 'a>>;
pub const CLIENT_LOG: &str = "client_log";
pub struct SlashCommand<'a> {
pub command: &'a str,
pub params_str: &'a str,
}
pub fn messages_since_kickoff(conversation: &Conversation) -> Result<&[Message]> {
let messages = conversation.messages();
let is_kickoff = |msg: &Message| msg.role == rmcp::model::Role::User && !msg.is_tool_response();
let start = messages
.iter()
.rposition(|msg| is_kickoff(msg) && msg.is_user_visible())
.or_else(|| {
messages
.iter()
.rposition(|msg| is_kickoff(msg) && msg.is_agent_visible())
})
.ok_or_else(|| anyhow!("state machine conversation has no kickoff message"))?;
Ok(&messages[start..])
}
pub fn trailing_error(conversation: &Conversation) -> Option<MessageErrorKind> {
conversation.last().and_then(Message::error_kind)
}
pub fn last_effective_role(messages: &[Message]) -> Result<EffectiveRole> {
messages
.last()
.map(effective_role)
.ok_or_else(|| anyhow!("cannot determine the role of an empty conversation"))
}
pub fn assistant_turn_count(messages: &[Message]) -> u32 {
let mut turns = 0;
let mut in_assistant_block = false;
for message in messages.iter().rev() {
if message.role == rmcp::model::Role::Assistant {
if !in_assistant_block {
turns += 1;
in_assistant_block = true;
}
} else {
in_assistant_block = false;
}
}
turns
}
pub fn ends_turn(messages: &[Message]) -> bool {
messages.last().is_some_and(|last| {
last.role == rmcp::model::Role::Assistant
&& last.error_kind().is_none()
&& !last.content.iter().any(|content| {
matches!(
content,
MessageContent::ToolRequest(_) | MessageContent::ActionRequired(_)
)
})
})
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
pub trait Operation<S, E: MaybeSend + 'static = ConversationEffect>: MaybeSend + MaybeSync {
fn name(&self) -> &'static str;
fn set_message_meta(&self, message: &mut Message, key: &str, value: serde_json::Value) {
message.metadata.set_operation_note(self.name(), key, value);
}
fn message_meta<'a>(&self, message: &'a Message, key: &str) -> Option<&'a serde_json::Value> {
message.metadata.operation_note(self.name(), key)
}
async fn finalize_cancellation(
&self,
_session: &S,
_conversation: &Conversation,
_emit: &Emitter,
) -> Vec<E> {
Vec::new()
}
async fn run_command(
&self,
_command: &SlashCommand<'_>,
_session: &S,
_conversation: &Conversation,
_emit: &Emitter,
) -> Result<OperationResult<E>> {
not_applicable()
}
async fn inference_tools(&self, _session: &S) -> Result<Vec<Tool>> {
Ok(Vec::new())
}
async fn prompt_parts(
&self,
_session: &S,
_conversation: &Conversation,
) -> Result<Vec<(String, String)>> {
Ok(Vec::new())
}
async fn moim_parts(&self, _session: &S, _conversation: &Conversation) -> Result<Vec<String>> {
Ok(Vec::new())
}
async fn run(
&self,
_session: &S,
_conversation: &Conversation,
_emit: &Emitter,
) -> Result<OperationResult<E>> {
not_applicable()
}
}
#[derive(Default)]
pub struct InferenceInput {
pub tools: Vec<Tool>,
pub prompt_parts: Vec<(String, String)>,
pub moim_parts: Vec<String>,
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
pub trait Inference<S, E: MaybeSend + 'static = ConversationEffect>: Operation<S, E> {
fn applies(&self, conversation: &Conversation) -> bool;
async fn prepare_session(&self, _session: &S) -> Result<Option<S>> {
Ok(None)
}
async fn infer(
&self,
session: &S,
conversation: &Conversation,
input: InferenceInput,
emit: &Emitter,
) -> Result<OperationResult<E>>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunStatus {
Continuing,
Yielded,
Cancelled,
}
pub struct StepResult<E = ConversationEffect> {
pub effects: Vec<E>,
pub applied_step: Option<&'static str>,
pub status: RunStatus,
}
pub enum OperationResult<E = ConversationEffect> {
NotApplicable,
Applied(StepResult<E>),
}
pub fn not_applicable<E>() -> Result<OperationResult<E>> {
Ok(OperationResult::NotApplicable)
}
pub fn applied<E>(effects: impl IntoIterator<Item = E>) -> Result<OperationResult<E>> {
Ok(OperationResult::Applied(StepResult {
effects: effects.into_iter().collect(),
applied_step: None,
status: RunStatus::Continuing,
}))
}
pub fn yielded<E>() -> Result<OperationResult<E>> {
Ok(OperationResult::Applied(StepResult {
effects: Vec::new(),
applied_step: None,
status: RunStatus::Yielded,
}))
}
pub fn yielded_with<E>(effects: impl IntoIterator<Item = E>) -> Result<OperationResult<E>> {
Ok(OperationResult::Applied(StepResult {
effects: effects.into_iter().collect(),
applied_step: None,
status: RunStatus::Yielded,
}))
}
pub trait MachineEffect {
fn ensure_message_ids(&mut self);
}
pub enum ConversationEffect {
AppendMessage(Message),
ReplaceConversation(Conversation),
PatchToolRequestMeta {
tool_call_id: String,
patch: serde_json::Value,
},
SetMessageOperationNote {
message_id: String,
operation: String,
key: String,
value: serde_json::Value,
},
SetMessageVisibility {
message_id: String,
user_visible: bool,
agent_visible: bool,
},
}
impl MachineEffect for ConversationEffect {
fn ensure_message_ids(&mut self) {
let messages = match self {
ConversationEffect::AppendMessage(message) => std::slice::from_mut(message),
ConversationEffect::ReplaceConversation(conversation) => {
conversation.messages_mut().as_mut_slice()
}
_ => return,
};
for message in messages {
if message.id.is_none() {
message.id = Some(format!("msg_{}", uuid::Uuid::new_v4()));
}
}
}
}
impl From<Message> for ConversationEffect {
fn from(message: Message) -> Self {
ConversationEffect::AppendMessage(message)
}
}
impl From<Conversation> for ConversationEffect {
fn from(conversation: Conversation) -> Self {
ConversationEffect::ReplaceConversation(conversation)
}
}
pub struct Emitter {
tx: mpsc::UnboundedSender<AgentEvent>,
cancel: CancellationToken,
}
impl Emitter {
pub fn new(tx: mpsc::UnboundedSender<AgentEvent>, cancel: CancellationToken) -> Self {
Self { tx, cancel }
}
pub fn emit(&self, event: AgentEvent) {
let _ = self.tx.send(event);
}
pub fn message(&self, message: Message) -> Message {
let message = message.with_generated_id_if_missing();
self.emit(AgentEvent::Message(message.clone()));
message
}
pub fn cancel_token(&self) -> &CancellationToken {
&self.cancel
}
pub async fn cancelled(&self) {
self.cancel.cancelled().await
}
}