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
114 .unwrap_or_else(|e| format!("tool call failed: {e}"))
115 .into(),
116 }),
117 )),
118 _ => None,
119 }
120 }
121}
122
123#[derive(Debug, Clone)]
127pub enum RawAgentEvent {
128 Token(TokenEvent),
129 React(ReactEvent),
130 EnvState(EnvStateEvent),
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn agent_event_text() {
139 let e = AgentEvent::Text("hello".into());
140 assert!(matches!(e, AgentEvent::Text(t) if t == "hello"));
141 }
142
143 #[test]
144 fn agent_event_tool_call_start() {
145 let e = AgentEvent::ToolCallRequest {
146 index: 0,
147 call_id: "call_abc".into(),
148 name: "test".into(),
149 args: serde_json::json!({"x": 1}),
150 };
151 assert!(matches!(e, AgentEvent::ToolCallRequest { name, .. } if name == "test"));
152 }
153
154 #[test]
155 fn agent_event_tool_call_result_ok() {
156 let e = AgentEvent::ToolCallResult {
157 call_id: "call_abc".into(),
158 name: "test".into(),
159 result: Ok("done".into()),
160 };
161 assert!(matches!(e, AgentEvent::ToolCallResult { .. }));
162 }
163
164 #[test]
165 fn agent_event_tool_call_result_err() {
166 let e = AgentEvent::ToolCallResult {
167 call_id: "call_abc".into(),
168 name: "test".into(),
169 result: Err("fail".into()),
170 };
171 assert!(matches!(
172 e,
173 AgentEvent::ToolCallResult { result: Err(_), .. }
174 ));
175 }
176
177 #[cfg(feature = "tool")]
178 #[test]
179 fn tool_call_result_error_preserved_in_session_message() {
180 let event = AgentEvent::ToolCallResult {
183 call_id: "call_1".into(),
184 name: "shell".into(),
185 result: Err("tool call rejected by user".into()),
186 };
187 let (role, variant) = event.into_session_message().unwrap();
188 assert_eq!(role.to_string(), "tool");
189 match variant {
190 MsgVariant::ToolResponse(msg) => {
191 assert!(
192 msg.result.contains("tool call failed:"),
193 "result should contain error prefix; got: {}",
194 msg.result
195 );
196 assert!(
197 msg.result.contains("tool call rejected by user"),
198 "result should contain original error; got: {}",
199 msg.result
200 );
201 }
202 other => panic!("expected ToolResponse, got {other:?}"),
203 }
204 }
205
206 #[cfg(feature = "tool")]
207 #[test]
208 fn tool_call_result_success_not_modified() {
209 let event = AgentEvent::ToolCallResult {
211 call_id: "call_2".into(),
212 name: "read".into(),
213 result: Ok("file contents".into()),
214 };
215 let (_role, variant) = event.into_session_message().unwrap();
216 match variant {
217 MsgVariant::ToolResponse(msg) => {
218 assert_eq!(&*msg.result, "file contents");
219 }
220 other => panic!("expected ToolResponse, got {other:?}"),
221 }
222 }
223
224 #[test]
225 fn agent_event_clone() {
226 let e = AgentEvent::Text("hi".into());
227 let cloned = e.clone();
228 assert!(matches!(cloned, AgentEvent::Text(t) if t == "hi"));
229 }
230
231 #[test]
232 fn agent_event_turn_end() {
233 let e = AgentEvent::TurnEnd {
234 finish_reason: Some("Stop".into()),
235 };
236 assert!(matches!(e, AgentEvent::TurnEnd { finish_reason: Some(ref r) } if r == "Stop"));
237 }
238
239 #[test]
240 fn middleware_event_into_session() {
241 let event: AgentEvent = MiddlewareEvent::assistant_text("hi".into(), None);
242 let (role, variant) = event.into_session_message().unwrap();
243 assert_eq!(role.to_string(), "assistant");
244 assert!(matches!(variant, MsgVariant::Text(_)));
245 }
246
247 #[test]
248 fn raw_token_wraps_text() {
249 let raw = RawAgentEvent::Token(TokenEvent::Text("hello".into()));
250 assert!(matches!(raw, RawAgentEvent::Token(TokenEvent::Text(t)) if t == "hello"));
251 }
252
253 #[test]
254 fn raw_react_wraps_turn_start() {
255 let raw = RawAgentEvent::React(ReactEvent::TurnStart);
256 assert!(matches!(raw, RawAgentEvent::React(ReactEvent::TurnStart)));
257 }
258
259 #[test]
260 fn raw_env_state_wraps_session_start() {
261 let raw = RawAgentEvent::EnvState(EnvStateEvent::SessionStart);
262 assert!(matches!(
263 raw,
264 RawAgentEvent::EnvState(EnvStateEvent::SessionStart)
265 ));
266 }
267
268 #[test]
269 fn raw_clone() {
270 let raw = RawAgentEvent::Token(TokenEvent::Text("x".into()));
271 let cloned = raw.clone();
272 assert!(matches!(cloned, RawAgentEvent::Token(TokenEvent::Text(t)) if t == "x"));
273 }
274
275 #[test]
276 fn raw_debug() {
277 let raw = RawAgentEvent::Token(TokenEvent::Text("x".into()));
278 let _ = format!("{raw:?}");
279 }
280}