1use std::sync::Arc;
2
3use serde_json::Value as JsonValue;
4
5use funera_core::chat::message::{MsgVariant, Role, TextMessage};
6#[cfg(feature = "tool")]
7use funera_core::chat::message::{ToolRequestMessage, ToolResponseMessage};
8use funera_core::event_bus::env_state_bus::EnvStateEvent;
9use funera_core::event_bus::react_bus::ReactEvent;
10use funera_core::event_bus::token_bus::TokenEvent;
11use funera_core::middleware::MiddlewareEvent;
12#[cfg(feature = "tool")]
13use funera_core::re_act::tool::ToolType;
14
15#[derive(Debug, Clone)]
16pub enum AgentEvent {
17 Text(String),
18 Reasoning(String),
19 ToolCallRequest {
20 index: usize,
21 call_id: Arc<str>,
22 name: String,
23 args: JsonValue,
24 },
25 ToolCallResult {
26 call_id: Arc<str>,
27 name: String,
28 result: Result<String, String>,
29 },
30 TurnStart,
31 TurnEnd {
32 finish_reason: Option<String>,
33 },
34 Error(String),
35 ToolApprovalRequired {
36 call_id: Arc<str>,
37 tool_name: String,
38 reason: String,
39 },
40 Done,
41}
42
43impl MiddlewareEvent for AgentEvent {
44 type Error = String;
45
46 fn assistant_text(content: String, _reasoning: Option<String>) -> Self {
47 AgentEvent::Text(content)
48 }
49
50 fn tool_call_request(call_id: Arc<str>, name: String, args: JsonValue) -> Self {
51 AgentEvent::ToolCallRequest {
52 index: 0,
53 call_id,
54 name,
55 args,
56 }
57 }
58
59 fn tool_response(call_id: Arc<str>, name: String, result: Result<String, String>) -> Self {
60 AgentEvent::ToolCallResult {
61 call_id,
62 name,
63 result,
64 }
65 }
66
67 fn turn_start() -> Self {
68 AgentEvent::TurnStart
69 }
70
71 fn turn_end(finish_reason: Option<String>) -> Self {
72 AgentEvent::TurnEnd { finish_reason }
73 }
74
75 fn done() -> Self {
76 AgentEvent::Done
77 }
78
79 fn into_session_message(self) -> Option<(Role, MsgVariant)> {
80 match self {
81 AgentEvent::Text(text) => Some((
82 Role::Assistant,
83 MsgVariant::Text(TextMessage {
84 text: text.into(),
85 reasoning_content: None,
86 }),
87 )),
88 #[cfg(feature = "tool")]
89 AgentEvent::ToolCallRequest {
90 call_id,
91 name,
92 args,
93 ..
94 } => Some((
95 Role::Assistant,
96 MsgVariant::ToolRequest(ToolRequestMessage {
97 tool_call_id: call_id,
98 tool_type: ToolType::Function,
99 function_name: name.into(),
100 function_args: args,
101 reasoning_content: None,
102 }),
103 )),
104 #[cfg(feature = "tool")]
105 AgentEvent::ToolCallResult {
106 call_id,
107 name: _,
108 result,
109 } => Some((
110 Role::Tool,
111 MsgVariant::ToolResponse(ToolResponseMessage {
112 tool_call_id: call_id,
113 result: result.unwrap_or_default().into(),
114 }),
115 )),
116 _ => None,
117 }
118 }
119}
120
121#[derive(Debug, Clone)]
125pub enum RawAgentEvent {
126 Token(TokenEvent),
127 React(ReactEvent),
128 EnvState(EnvStateEvent),
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn agent_event_text() {
137 let e = AgentEvent::Text("hello".into());
138 assert!(matches!(e, AgentEvent::Text(t) if t == "hello"));
139 }
140
141 #[test]
142 fn agent_event_tool_call_start() {
143 let e = AgentEvent::ToolCallRequest {
144 index: 0,
145 call_id: "call_abc".into(),
146 name: "test".into(),
147 args: serde_json::json!({"x": 1}),
148 };
149 assert!(matches!(e, AgentEvent::ToolCallRequest { name, .. } if name == "test"));
150 }
151
152 #[test]
153 fn agent_event_tool_call_result_ok() {
154 let e = AgentEvent::ToolCallResult {
155 call_id: "call_abc".into(),
156 name: "test".into(),
157 result: Ok("done".into()),
158 };
159 assert!(matches!(e, AgentEvent::ToolCallResult { .. }));
160 }
161
162 #[test]
163 fn agent_event_tool_call_result_err() {
164 let e = AgentEvent::ToolCallResult {
165 call_id: "call_abc".into(),
166 name: "test".into(),
167 result: Err("fail".into()),
168 };
169 assert!(matches!(
170 e,
171 AgentEvent::ToolCallResult { result: Err(_), .. }
172 ));
173 }
174
175 #[test]
176 fn agent_event_clone() {
177 let e = AgentEvent::Text("hi".into());
178 let cloned = e.clone();
179 assert!(matches!(cloned, AgentEvent::Text(t) if t == "hi"));
180 }
181
182 #[test]
183 fn agent_event_turn_end() {
184 let e = AgentEvent::TurnEnd {
185 finish_reason: Some("Stop".into()),
186 };
187 assert!(matches!(e, AgentEvent::TurnEnd { finish_reason: Some(ref r) } if r == "Stop"));
188 }
189
190 #[test]
191 fn middleware_event_into_session() {
192 let event: AgentEvent = MiddlewareEvent::assistant_text("hi".into(), None);
193 let (role, variant) = event.into_session_message().unwrap();
194 assert_eq!(role.to_string(), "assistant");
195 assert!(matches!(variant, MsgVariant::Text(_)));
196 }
197
198 #[test]
199 fn raw_token_wraps_text() {
200 let raw = RawAgentEvent::Token(TokenEvent::Text("hello".into()));
201 assert!(matches!(raw, RawAgentEvent::Token(TokenEvent::Text(t)) if t == "hello"));
202 }
203
204 #[test]
205 fn raw_react_wraps_turn_start() {
206 let raw = RawAgentEvent::React(ReactEvent::TurnStart);
207 assert!(matches!(raw, RawAgentEvent::React(ReactEvent::TurnStart)));
208 }
209
210 #[test]
211 fn raw_env_state_wraps_session_start() {
212 let raw = RawAgentEvent::EnvState(EnvStateEvent::SessionStart);
213 assert!(matches!(
214 raw,
215 RawAgentEvent::EnvState(EnvStateEvent::SessionStart)
216 ));
217 }
218
219 #[test]
220 fn raw_clone() {
221 let raw = RawAgentEvent::Token(TokenEvent::Text("x".into()));
222 let cloned = raw.clone();
223 assert!(matches!(cloned, RawAgentEvent::Token(TokenEvent::Text(t)) if t == "x"));
224 }
225
226 #[test]
227 fn raw_debug() {
228 let raw = RawAgentEvent::Token(TokenEvent::Text("x".into()));
229 let _ = format!("{raw:?}");
230 }
231}