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(ToolCall {
86 tool_call_id: payload.tool_call_id.to_string_lossy(),
87 tool_name: payload.tool_name.to_string_lossy(),
88 params: payload.params.to_string_lossy(),
89 })
90 }
91
92 pub fn tool_result(&self) -> Option<ToolResult> {
93 if !matches!(
94 self.tag(),
95 EventTag::ToolResult | EventTag::ToolExecutionEnd
96 ) {
97 return None;
98 }
99 let payload = unsafe { self.raw.payload.tool_result };
101 Some(ToolResult {
102 tool_call_id: payload.tool_call_id.to_string_lossy(),
103 tool_name: payload.tool_name.to_string_lossy(),
104 result: payload.result.to_string_lossy(),
105 is_error: payload.is_error != 0,
106 })
107 }
108}
109
110#[macro_export]
116macro_rules! event_handler {
117 ($(#[$attr:meta])* fn $name:ident($event:ident, $data:ident) $body:block) => {
118 $(#[$attr])*
119 extern "C" fn $name(raw: $crate::StablePluginEvent, $data: *mut std::ffi::c_void) -> i32 {
120 $crate::guard_or($crate::EVENT_HANDLER_ERROR, || {
121 let $event = unsafe { $crate::events::EventView::from_raw(&raw) };
124 $body
125 })
126 }
127 };
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::StbStringRef;
134
135 fn borrowed_string(text: &str) -> crate::StbString {
138 let borrowed = StbStringRef::from_str(text);
139 crate::StbString {
140 ptr: borrowed.ptr as *mut _,
141 len: borrowed.len,
142 }
143 }
144
145 #[test]
146 fn wrong_accessors_do_not_read_inactive_union_arms() {
147 let raw = StablePluginEvent::empty(EventTag::AgentStart);
148 let view = unsafe { EventView::from_raw(&raw) };
149 assert!(view.message_text().is_none());
150 assert!(view.data_text().is_none());
151 assert!(view.tool_call().is_none());
152 assert!(view.tool_result().is_none());
153 }
154
155 #[test]
156 fn copied_message_outlives_the_borrowed_event_buffer() {
157 let copy = {
158 let text = String::from("消息");
159 let raw = StablePluginEvent::message(EventTag::MessageEnd, borrowed_string(&text));
160 let view = unsafe { EventView::from_raw(&raw) };
161 assert!(view.tool_result().is_none());
162 view.message_text().unwrap()
163 };
164 assert_eq!(copy, "消息");
165 }
166
167 #[test]
168 fn tool_payloads_are_copied_with_the_error_flag() {
169 let raw = StablePluginEvent::tool_result(
170 EventTag::ToolExecutionEnd,
171 borrowed_string("call"),
172 borrowed_string("tool"),
173 borrowed_string("{}"),
174 true,
175 );
176 let view = unsafe { EventView::from_raw(&raw) };
177 let result = view.tool_result().unwrap();
178 assert_eq!(result.tool_call_id, "call");
179 assert_eq!(result.tool_name, "tool");
180 assert_eq!(result.result, "{}");
181 assert!(result.is_error);
182 assert!(view.tool_call().is_none());
183 }
184
185 event_handler! { fn panicking(_event, _data) { panic!("handler panic") } }
186
187 #[test]
188 fn data_and_call_accessors_select_only_their_own_payloads() {
189 let raw = StablePluginEvent::data(EventTag::BeforeProviderRequest, borrowed_string("{}"));
190 let view = unsafe { EventView::from_raw(&raw) };
191 assert_eq!(view.data_text().as_deref(), Some("{}"));
192 assert!(view.message_text().is_none());
193 let raw = StablePluginEvent::tool_call(
194 EventTag::ToolCall,
195 borrowed_string("call"),
196 borrowed_string("tool"),
197 borrowed_string("{}"),
198 );
199 let view = unsafe { EventView::from_raw(&raw) };
200 let call = view.tool_call().unwrap();
201 assert_eq!(call.tool_call_id, "call");
202 assert_eq!(call.tool_name, "tool");
203 assert_eq!(call.params, "{}");
204 assert!(view.data_text().is_none());
205 }
206
207 #[test]
208 fn handler_panic_returns_error_without_crossing_abi() {
209 assert_eq!(
210 panicking(
211 StablePluginEvent::empty(EventTag::AgentStart),
212 std::ptr::null_mut()
213 ),
214 crate::EVENT_HANDLER_ERROR
215 );
216 }
217}