use std::sync::Arc;
use serde_json::Value as JsonValue;
use funera_core::chat::message::{MsgVariant, Role, TextMessage};
#[cfg(feature = "tool")]
use funera_core::chat::message::{ToolRequestMessage, ToolResponseMessage};
use funera_core::event_bus::env_state_bus::EnvStateEvent;
use funera_core::event_bus::react_bus::ReactEvent;
use funera_core::event_bus::token_bus::TokenEvent;
use funera_core::middleware::MiddlewareEvent;
#[cfg(feature = "tool")]
use funera_core::re_act::tool::ToolType;
#[derive(Debug, Clone)]
pub enum AgentEvent {
Text(String),
Reasoning(String),
ToolCallRequest {
index: usize,
call_id: Arc<str>,
name: String,
args: JsonValue,
},
ToolCallResult {
call_id: Arc<str>,
name: String,
result: Result<String, String>,
},
TurnStart,
TurnEnd {
finish_reason: Option<String>,
},
Error(String),
ToolApprovalRequired {
call_id: Arc<str>,
tool_name: String,
reason: String,
},
Done,
}
impl MiddlewareEvent for AgentEvent {
type Error = String;
fn assistant_text(content: String, _reasoning: Option<String>) -> Self {
AgentEvent::Text(content)
}
fn tool_call_request(call_id: Arc<str>, name: String, args: JsonValue) -> Self {
AgentEvent::ToolCallRequest {
index: 0,
call_id,
name,
args,
}
}
fn tool_response(call_id: Arc<str>, name: String, result: Result<String, String>) -> Self {
AgentEvent::ToolCallResult {
call_id,
name,
result,
}
}
fn turn_start() -> Self {
AgentEvent::TurnStart
}
fn turn_end(finish_reason: Option<String>) -> Self {
AgentEvent::TurnEnd { finish_reason }
}
fn done() -> Self {
AgentEvent::Done
}
fn into_session_message(self) -> Option<(Role, MsgVariant)> {
match self {
AgentEvent::Text(text) => Some((
Role::Assistant,
MsgVariant::Text(TextMessage {
text: text.into(),
reasoning_content: None,
}),
)),
#[cfg(feature = "tool")]
AgentEvent::ToolCallRequest {
call_id,
name,
args,
..
} => Some((
Role::Assistant,
MsgVariant::ToolRequest(ToolRequestMessage {
tool_call_id: call_id,
tool_type: ToolType::Function,
function_name: name.into(),
function_args: args,
reasoning_content: None,
}),
)),
#[cfg(feature = "tool")]
AgentEvent::ToolCallResult {
call_id,
name: _,
result,
} => Some((
Role::Tool,
MsgVariant::ToolResponse(ToolResponseMessage {
tool_call_id: call_id,
result: result
.unwrap_or_else(|e| format!("tool call failed: {e}"))
.into(),
}),
)),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub enum RawAgentEvent {
Token(TokenEvent),
React(ReactEvent),
EnvState(EnvStateEvent),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_event_text() {
let e = AgentEvent::Text("hello".into());
assert!(matches!(e, AgentEvent::Text(t) if t == "hello"));
}
#[test]
fn agent_event_tool_call_start() {
let e = AgentEvent::ToolCallRequest {
index: 0,
call_id: "call_abc".into(),
name: "test".into(),
args: serde_json::json!({"x": 1}),
};
assert!(matches!(e, AgentEvent::ToolCallRequest { name, .. } if name == "test"));
}
#[test]
fn agent_event_tool_call_result_ok() {
let e = AgentEvent::ToolCallResult {
call_id: "call_abc".into(),
name: "test".into(),
result: Ok("done".into()),
};
assert!(matches!(e, AgentEvent::ToolCallResult { .. }));
}
#[test]
fn agent_event_tool_call_result_err() {
let e = AgentEvent::ToolCallResult {
call_id: "call_abc".into(),
name: "test".into(),
result: Err("fail".into()),
};
assert!(matches!(
e,
AgentEvent::ToolCallResult { result: Err(_), .. }
));
}
#[cfg(feature = "tool")]
#[test]
fn tool_call_result_error_preserved_in_session_message() {
let event = AgentEvent::ToolCallResult {
call_id: "call_1".into(),
name: "shell".into(),
result: Err("tool call rejected by user".into()),
};
let (role, variant) = event.into_session_message().unwrap();
assert_eq!(role.to_string(), "tool");
match variant {
MsgVariant::ToolResponse(msg) => {
assert!(
msg.result.contains("tool call failed:"),
"result should contain error prefix; got: {}",
msg.result
);
assert!(
msg.result.contains("tool call rejected by user"),
"result should contain original error; got: {}",
msg.result
);
}
other => panic!("expected ToolResponse, got {other:?}"),
}
}
#[cfg(feature = "tool")]
#[test]
fn tool_call_result_success_not_modified() {
let event = AgentEvent::ToolCallResult {
call_id: "call_2".into(),
name: "read".into(),
result: Ok("file contents".into()),
};
let (_role, variant) = event.into_session_message().unwrap();
match variant {
MsgVariant::ToolResponse(msg) => {
assert_eq!(&*msg.result, "file contents");
}
other => panic!("expected ToolResponse, got {other:?}"),
}
}
#[test]
fn agent_event_clone() {
let e = AgentEvent::Text("hi".into());
let cloned = e.clone();
assert!(matches!(cloned, AgentEvent::Text(t) if t == "hi"));
}
#[test]
fn agent_event_turn_end() {
let e = AgentEvent::TurnEnd {
finish_reason: Some("Stop".into()),
};
assert!(matches!(e, AgentEvent::TurnEnd { finish_reason: Some(ref r) } if r == "Stop"));
}
#[test]
fn middleware_event_into_session() {
let event: AgentEvent = MiddlewareEvent::assistant_text("hi".into(), None);
let (role, variant) = event.into_session_message().unwrap();
assert_eq!(role.to_string(), "assistant");
assert!(matches!(variant, MsgVariant::Text(_)));
}
#[test]
fn raw_token_wraps_text() {
let raw = RawAgentEvent::Token(TokenEvent::Text("hello".into()));
assert!(matches!(raw, RawAgentEvent::Token(TokenEvent::Text(t)) if t == "hello"));
}
#[test]
fn raw_react_wraps_turn_start() {
let raw = RawAgentEvent::React(ReactEvent::TurnStart);
assert!(matches!(raw, RawAgentEvent::React(ReactEvent::TurnStart)));
}
#[test]
fn raw_env_state_wraps_session_start() {
let raw = RawAgentEvent::EnvState(EnvStateEvent::SessionStart);
assert!(matches!(
raw,
RawAgentEvent::EnvState(EnvStateEvent::SessionStart)
));
}
#[test]
fn raw_clone() {
let raw = RawAgentEvent::Token(TokenEvent::Text("x".into()));
let cloned = raw.clone();
assert!(matches!(cloned, RawAgentEvent::Token(TokenEvent::Text(t)) if t == "x"));
}
#[test]
fn raw_debug() {
let raw = RawAgentEvent::Token(TokenEvent::Text("x".into()));
let _ = format!("{raw:?}");
}
}