Skip to main content

rpi_plugin_sdk/
events.rs

1//! Borrowed ABI event views with tag-checked, owned payload copies.
2use 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    /// Create a view at an ABI callback boundary.
23    ///
24    /// # Safety
25    /// The host must initialize the payload variant specified by `tag` and
26    /// keep every payload buffer valid until the callback returns. The view
27    /// borrows the event; accessors copy strings and never free host memory.
28    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        // SAFETY: from_raw's invariant and the tag check select the live arm.
44        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        // SAFETY: from_raw's invariant and the tag check select the live arm.
73        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        // SAFETY: from_raw's invariant and the tag check select the live arm.
84        let payload = unsafe { self.raw.payload.tool_call };
85        // SAFETY: from_raw guarantees the selected payload buffers remain live.
86        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        // SAFETY: from_raw's invariant and the tag check select the live arm.
103        let payload = unsafe { self.raw.payload.tool_result };
104        // SAFETY: from_raw guarantees the selected payload buffers remain live.
105        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/// Define an ABI event handler whose body uses an `EventView`.
117///
118/// The generated trampoline contains the host's ABI validity assumption and
119/// catches panics before they can unwind across `extern "C"`. Payload strings
120/// copied by the view may safely be retained after the callback returns.
121#[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                // SAFETY: this is the host-owned ABI callback; its event and
128                // buffers remain valid for the duration of this invocation.
129                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    // Borrowing a Rust string gives this test a live buffer without allocating
142    // or leaking an ABI string that would need an unsafe deallocator.
143    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}