use futures::future::BoxFuture;
use oino_ai::{AgentMessage, AssistantMessageEvent, JsonObject, ToolResultMessage};
use serde::{Deserialize, Serialize};
use crate::tool::AgentToolResult;
#[allow(
clippy::large_enum_variant,
reason = "variants mirror pi event shapes; events are moved, not stored in bulk"
)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AgentEvent {
AgentStart,
AgentEnd {
messages: Vec<AgentMessage>,
},
TurnStart,
#[serde(rename_all = "camelCase")]
TurnEnd {
message: AgentMessage,
#[serde(with = "tagged_tool_results")]
tool_results: Vec<ToolResultMessage>,
},
MessageStart {
message: AgentMessage,
},
#[serde(rename_all = "camelCase")]
MessageUpdate {
message: AgentMessage,
assistant_message_event: AssistantMessageEvent,
},
MessageEnd {
message: AgentMessage,
},
#[serde(rename_all = "camelCase")]
ToolExecutionStart {
tool_call_id: String,
tool_name: String,
args: JsonObject,
},
#[serde(rename_all = "camelCase")]
ToolExecutionUpdate {
tool_call_id: String,
tool_name: String,
args: JsonObject,
partial_result: AgentToolResult,
},
#[serde(rename_all = "camelCase")]
ToolExecutionEnd {
tool_call_id: String,
tool_name: String,
result: AgentToolResult,
is_error: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
duration_ms: Option<u64>,
},
}
impl AgentEvent {
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::AgentStart => "agent_start",
Self::AgentEnd { .. } => "agent_end",
Self::TurnStart => "turn_start",
Self::TurnEnd { .. } => "turn_end",
Self::MessageStart { .. } => "message_start",
Self::MessageUpdate { .. } => "message_update",
Self::MessageEnd { .. } => "message_end",
Self::ToolExecutionStart { .. } => "tool_execution_start",
Self::ToolExecutionUpdate { .. } => "tool_execution_update",
Self::ToolExecutionEnd { .. } => "tool_execution_end",
}
}
}
mod tagged_tool_results {
use oino_ai::{AgentMessage, ToolResultMessage};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub(super) fn serialize<S: Serializer>(
results: &[ToolResultMessage],
serializer: S,
) -> Result<S::Ok, S::Error> {
let tagged: Vec<AgentMessage> = results
.iter()
.cloned()
.map(AgentMessage::ToolResult)
.collect();
tagged.serialize(serializer)
}
pub(super) fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<ToolResultMessage>, D::Error> {
let values = Vec::<serde_json::Value>::deserialize(deserializer)?;
values
.into_iter()
.map(|mut value| {
if let Some(object) = value.as_object_mut() {
object.shift_remove("role");
}
serde_json::from_value(value).map_err(serde::de::Error::custom)
})
.collect()
}
}
pub trait EventSink: Send + Sync {
fn emit(&self, event: AgentEvent) -> BoxFuture<'_, ()>;
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn wire_shapes() {
assert_eq!(
serde_json::to_value(AgentEvent::AgentStart).unwrap(),
json!({"type":"agent_start"})
);
let end = AgentEvent::ToolExecutionEnd {
tool_call_id: "c1".into(),
tool_name: "read".into(),
result: AgentToolResult::error("x"),
is_error: true,
duration_ms: None,
};
assert_eq!(
serde_json::to_value(end).unwrap(),
json!({"type":"tool_execution_end","toolCallId":"c1","toolName":"read",
"result":{"content":[{"type":"text","text":"x"}],"details":{}},"isError":true})
);
}
}