1use crate::{EventTag, StablePluginEvent};
3
4pub struct EventView<'a> {
5 raw: &'a StablePluginEvent,
6}
7
8pub struct ToolCall {
9 pub tool_call_id: String,
10 pub tool_name: String,
11 pub params: String,
12}
13
14pub struct ToolResult {
15 pub tool_call_id: String,
16 pub tool_name: String,
17 pub result: String,
18 pub is_error: bool,
19}
20
21impl<'a> EventView<'a> {
22 pub unsafe fn from_raw(raw: &'a StablePluginEvent) -> Self {
29 Self { raw }
30 }
31
32 pub fn tag(&self) -> EventTag {
33 self.raw.tag
34 }
35
36 pub fn message_text(&self) -> Option<String> {
37 if !matches!(
38 self.tag(),
39 EventTag::MessageStart | EventTag::MessageUpdate | EventTag::MessageEnd
40 ) {
41 return None;
42 }
43 Some(unsafe { self.raw.payload.message.message.to_string_lossy() })
45 }
46
47 pub fn data_text(&self) -> Option<String> {
48 if !matches!(
49 self.tag(),
50 EventTag::SessionBeforeSwitch
51 | EventTag::SessionInfoChanged
52 | EventTag::SessionBeforeFork
53 | EventTag::SessionBeforeCompact
54 | EventTag::SessionCompact
55 | EventTag::SessionBeforeTree
56 | EventTag::SessionTree
57 | EventTag::Context
58 | EventTag::BeforeProviderRequest
59 | EventTag::BeforeProviderHeaders
60 | EventTag::AfterProviderResponse
61 | EventTag::BeforeAgentStart
62 | EventTag::ModelSelect
63 | EventTag::ThinkingLevelSelect
64 | EventTag::UserBash
65 | EventTag::Input
66 | EventTag::UiPromptStart
67 | EventTag::UiPromptEnd
68 | EventTag::EditorChange
69 ) {
70 return None;
71 }
72 Some(unsafe { self.raw.payload.data.data.to_string_lossy() })
74 }
75
76 pub fn tool_call(&self) -> Option<ToolCall> {
77 if !matches!(
78 self.tag(),
79 EventTag::ToolCall | EventTag::ToolExecutionStart | EventTag::ToolExecutionUpdate
80 ) {
81 return None;
82 }
83 let payload = unsafe { self.raw.payload.tool_call };
85 Some(unsafe {
87 ToolCall {
88 tool_call_id: payload.tool_call_id.to_string_lossy(),
89 tool_name: payload.tool_name.to_string_lossy(),
90 params: payload.params.to_string_lossy(),
91 }
92 })
93 }
94
95 pub fn tool_result(&self) -> Option<ToolResult> {
96 if !matches!(
97 self.tag(),
98 EventTag::ToolResult | EventTag::ToolExecutionEnd
99 ) {
100 return None;
101 }
102 let payload = unsafe { self.raw.payload.tool_result };
104 Some(unsafe {
106 ToolResult {
107 tool_call_id: payload.tool_call_id.to_string_lossy(),
108 tool_name: payload.tool_name.to_string_lossy(),
109 result: payload.result.to_string_lossy(),
110 is_error: payload.is_error != 0,
111 }
112 })
113 }
114}
115
116#[macro_export]
122macro_rules! event_handler {
123 ($(#[$attr:meta])* fn $name:ident($event:ident, $data:ident) $body:block) => {
124 $(#[$attr])*
125 extern "C" fn $name(raw: $crate::StablePluginEvent, $data: *mut std::ffi::c_void) -> i32 {
126 $crate::guard_or($crate::EVENT_HANDLER_ERROR, || {
127 let $event = unsafe { $crate::events::EventView::from_raw(&raw) };
130 $body
131 })
132 }
133 };
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::StbStringRef;
140
141 fn borrowed_string(text: &str) -> crate::StbString {
144 let borrowed = StbStringRef::from_str(text);
145 crate::StbString {
146 ptr: borrowed.ptr as *mut _,
147 len: borrowed.len,
148 }
149 }
150
151 #[test]
152 fn wrong_accessors_do_not_read_inactive_union_arms() {
153 let raw = StablePluginEvent::empty(EventTag::AgentStart);
154 let view = unsafe { EventView::from_raw(&raw) };
155 assert!(view.message_text().is_none());
156 assert!(view.data_text().is_none());
157 assert!(view.tool_call().is_none());
158 assert!(view.tool_result().is_none());
159 }
160
161 #[test]
162 fn copied_message_outlives_the_borrowed_event_buffer() {
163 let copy = {
164 let text = String::from("消息");
165 let raw = StablePluginEvent::message(EventTag::MessageEnd, borrowed_string(&text));
166 let view = unsafe { EventView::from_raw(&raw) };
167 assert!(view.tool_result().is_none());
168 view.message_text().unwrap()
169 };
170 assert_eq!(copy, "消息");
171 }
172
173 #[test]
174 fn tool_payloads_are_copied_with_the_error_flag() {
175 let raw = StablePluginEvent::tool_result(
176 EventTag::ToolExecutionEnd,
177 borrowed_string("call"),
178 borrowed_string("tool"),
179 borrowed_string("{}"),
180 true,
181 );
182 let view = unsafe { EventView::from_raw(&raw) };
183 let result = view.tool_result().unwrap();
184 assert_eq!(result.tool_call_id, "call");
185 assert_eq!(result.tool_name, "tool");
186 assert_eq!(result.result, "{}");
187 assert!(result.is_error);
188 assert!(view.tool_call().is_none());
189 }
190
191 event_handler! { fn panicking(_event, _data) { panic!("handler panic") } }
192
193 #[test]
194 fn data_and_call_accessors_select_only_their_own_payloads() {
195 let raw = StablePluginEvent::data(EventTag::BeforeProviderRequest, borrowed_string("{}"));
196 let view = unsafe { EventView::from_raw(&raw) };
197 assert_eq!(view.data_text().as_deref(), Some("{}"));
198 assert!(view.message_text().is_none());
199 let raw = StablePluginEvent::tool_call(
200 EventTag::ToolCall,
201 borrowed_string("call"),
202 borrowed_string("tool"),
203 borrowed_string("{}"),
204 );
205 let view = unsafe { EventView::from_raw(&raw) };
206 let call = view.tool_call().unwrap();
207 assert_eq!(call.tool_call_id, "call");
208 assert_eq!(call.tool_name, "tool");
209 assert_eq!(call.params, "{}");
210 assert!(view.data_text().is_none());
211 }
212
213 #[test]
214 fn handler_panic_returns_error_without_crossing_abi() {
215 assert_eq!(
216 panicking(
217 StablePluginEvent::empty(EventTag::AgentStart),
218 std::ptr::null_mut()
219 ),
220 crate::EVENT_HANDLER_ERROR
221 );
222 }
223}