1pub mod context;
6#[allow(missing_docs)]
7pub mod ext_cli;
8pub mod loading;
9pub mod registry;
10pub mod types;
11#[allow(missing_docs)]
12pub mod wasm;
13pub mod wasm_hooks;
14pub mod wasm_tool;
15
16pub use crate::extensions::context::{ExtensionContext, ExtensionContextBuilder};
18pub use crate::extensions::loading::{
19 discover_extensions, discover_extensions_in_dir, load_extension, load_extensions,
20 validate_extension, ValidatedExtension, SHARED_LIB_EXTENSION,
21};
22pub use crate::extensions::registry::{ExtensionErrorHandle, ExtensionRegistry, ExtensionRunner};
23pub use crate::extensions::types::{
24 AfterProviderResponseEvent, BashEvent, BeforeProviderRequestEvent, Command, ContextEmitResult,
25 ContextEvent, ExtensionError, ExtensionErrorListener, ExtensionErrorRecord, ExtensionManifest,
26 ExtensionPermission, ExtensionState, InputEvent, InputEventResult, InputSource,
27 ModelSelectEvent, ModelSelectSource, ProviderRequestEmitResult, SessionBeforeCompactEvent,
28 SessionBeforeEmitResult, SessionBeforeForkEvent, SessionBeforeSwitchEvent,
29 SessionBeforeTreeEvent, SessionCompactEvent, SessionShutdownEvent, SessionShutdownReason,
30 SessionSwitchReason, SessionTreeEvent, ThinkingLevelSelectEvent, ToolCallEmitResult,
31 ToolResultEmitResult,
32};
33pub use crate::extensions::wasm::{
34 ExtensionInfo, WasmCommandDef, WasmExtensionManager, WasmToolDef,
35};
36pub use crate::extensions::wasm_tool::WasmTool;
37
38pub use oxi_agent::{AgentEvent, AgentTool, AgentToolResult};
40
41pub trait Extension: Send + Sync {
44 fn name(&self) -> &str;
46 fn description(&self) -> &str;
48 fn manifest(&self) -> ExtensionManifest {
50 ExtensionManifest::new(self.name(), "0.0.0").with_description(self.description())
51 }
52 fn register_tools(&self) -> Vec<std::sync::Arc<dyn oxi_agent::AgentTool>> {
54 vec![]
55 }
56 fn register_commands(&self) -> Vec<Command> {
58 vec![]
59 }
60 fn on_load(&self, _ctx: &ExtensionContext) {}
62 fn on_unload(&self) {}
64 fn on_message_sent(&self, _msg: &str) {}
66 fn on_message_received(&self, _msg: &str) {}
68 fn on_tool_call(&self, _tool: &str, _params: &serde_json::Value) {}
70 fn on_tool_result(&self, _tool: &str, _result: &oxi_agent::AgentToolResult) {}
72 fn on_session_start(&self, _session_id: &str) {}
74 fn on_session_end(&self, _session_id: &str) {}
76 fn on_settings_changed(&self, _settings: &oxi_store::settings::Settings) {}
78 fn on_event(&self, _event: &oxi_agent::AgentEvent) {}
80 fn on_before_tool_call(
82 &self,
83 _tool: &str,
84 _args: &serde_json::Value,
85 ) -> Result<(), anyhow::Error> {
86 Ok(())
87 }
88 fn on_after_tool_call(
90 &self,
91 _tool: &str,
92 _result: &oxi_agent::AgentToolResult,
93 ) -> Result<(), anyhow::Error> {
94 Ok(())
95 }
96 fn on_before_compaction(&self, _ctx: &crate::CompactionContext) -> Result<(), anyhow::Error> {
98 Ok(())
99 }
100 fn on_after_compaction(&self, _summary: &str) -> Result<(), anyhow::Error> {
102 Ok(())
103 }
104 fn on_error(&self, _error: &anyhow::Error) -> Result<(), anyhow::Error> {
106 Ok(())
107 }
108 fn session_before_switch(
110 &self,
111 _event: &crate::extensions::types::SessionBeforeSwitchEvent,
112 ) -> Result<(), anyhow::Error> {
113 Ok(())
114 }
115 fn session_before_fork(
117 &self,
118 _event: &crate::extensions::types::SessionBeforeForkEvent,
119 ) -> Result<(), anyhow::Error> {
120 Ok(())
121 }
122 fn session_before_compact(
124 &self,
125 _event: &crate::extensions::types::SessionBeforeCompactEvent,
126 ) -> Result<(), anyhow::Error> {
127 Ok(())
128 }
129 fn session_compact(
131 &self,
132 _event: &crate::extensions::types::SessionCompactEvent,
133 ) -> Result<(), anyhow::Error> {
134 Ok(())
135 }
136 fn session_shutdown(&self, _event: &crate::extensions::types::SessionShutdownEvent) {}
138 fn session_before_tree(
140 &self,
141 _event: &crate::extensions::types::SessionBeforeTreeEvent,
142 ) -> Result<(), anyhow::Error> {
143 Ok(())
144 }
145 fn session_tree(&self, _event: &crate::extensions::types::SessionTreeEvent) {}
147 fn context(
149 &self,
150 _event: &mut crate::extensions::types::ContextEvent,
151 ) -> Result<(), anyhow::Error> {
152 Ok(())
153 }
154 fn before_provider_request(
156 &self,
157 _event: &mut crate::extensions::types::BeforeProviderRequestEvent,
158 ) -> Result<(), anyhow::Error> {
159 Ok(())
160 }
161 fn after_provider_response(
163 &self,
164 _event: &crate::extensions::types::AfterProviderResponseEvent,
165 ) -> Result<(), anyhow::Error> {
166 Ok(())
167 }
168 fn model_select(&self, _event: &crate::extensions::types::ModelSelectEvent) {}
170 fn thinking_level_select(&self, _event: &crate::extensions::types::ThinkingLevelSelectEvent) {}
172 fn bash(&self, _event: &crate::extensions::types::BashEvent) {}
174 fn input(
176 &self,
177 _event: &crate::extensions::types::InputEvent,
178 ) -> crate::extensions::types::InputEventResult {
179 crate::extensions::types::InputEventResult::Continue
180 }
181}
182
183pub struct NoopExtension;
186impl Extension for NoopExtension {
187 fn name(&self) -> &str {
188 "noop"
189 }
190 fn description(&self) -> &str {
191 "Built-in no-op extension"
192 }
193}
194
195#[cfg(test)]
197pub struct RecordingExtension {
198 pub name: String,
199 pub calls: std::sync::Mutex<Vec<String>>,
200}
201#[cfg(test)]
202impl RecordingExtension {
203 pub fn new(name: impl Into<String>) -> Self {
204 Self {
205 name: name.into(),
206 calls: std::sync::Mutex::new(Vec::new()),
207 }
208 }
209 pub fn push(&self, call: &str) {
210 self.calls.lock().unwrap().push(call.to_string());
211 }
212 pub fn calls(&self) -> Vec<String> {
213 self.calls.lock().unwrap().clone()
214 }
215}
216#[cfg(test)]
217impl Extension for RecordingExtension {
218 fn name(&self) -> &str {
219 &self.name
220 }
221 fn description(&self) -> &str {
222 "recording test extension"
223 }
224 fn on_load(&self, _ctx: &ExtensionContext) {
225 self.push("on_load");
226 }
227 fn on_unload(&self) {
228 self.push("on_unload");
229 }
230 fn on_message_sent(&self, msg: &str) {
231 self.push(&format!("on_message_sent({})", msg));
232 }
233 fn on_message_received(&self, msg: &str) {
234 self.push(&format!("on_message_received({})", msg));
235 }
236 fn on_tool_call(&self, tool: &str, _params: &serde_json::Value) {
237 self.push(&format!("on_tool_call({})", tool));
238 }
239 fn on_tool_result(&self, tool: &str, _result: &oxi_agent::AgentToolResult) {
240 self.push(&format!("on_tool_result({})", tool));
241 }
242 fn on_session_start(&self, session_id: &str) {
243 self.push(&format!("on_session_start({})", session_id));
244 }
245 fn on_session_end(&self, session_id: &str) {
246 self.push(&format!("on_session_end({})", session_id));
247 }
248 fn on_settings_changed(&self, _settings: &oxi_store::settings::Settings) {
249 self.push("on_settings_changed");
250 }
251 fn on_event(&self, _event: &oxi_agent::AgentEvent) {
252 self.push("on_event");
253 }
254}
255
256#[cfg(test)]
258mod tests {
259 use super::*;
260 use oxi_store::settings::Settings;
261 use std::sync::Arc;
262
263 #[test]
264 fn test_manifest_builder() {
265 let manifest = ExtensionManifest::new("my-ext", "1.0.0")
266 .with_description("A test extension")
267 .with_author("test-author")
268 .with_permission(ExtensionPermission::FileRead)
269 .with_permission(ExtensionPermission::Bash)
270 .with_config_schema(serde_json::json!({"type": "object", "properties": {"api_key": {"type": "string"}}}));
271
272 assert_eq!(manifest.name, "my-ext");
273 assert_eq!(manifest.version, "1.0.0");
274 assert_eq!(manifest.description, "A test extension");
275 assert_eq!(manifest.author, "test-author");
276 assert!(manifest.has_permission(ExtensionPermission::FileRead));
277 assert!(manifest.has_permission(ExtensionPermission::Bash));
278 assert!(!manifest.has_permission(ExtensionPermission::Network));
279 }
280
281 #[test]
282 fn test_permission_display() {
283 assert_eq!(ExtensionPermission::FileRead.to_string(), "file_read");
284 assert_eq!(ExtensionPermission::Bash.to_string(), "bash");
285 }
286
287 #[test]
288 fn test_context_builder_minimal() {
289 let ctx = ExtensionContextBuilder::new(std::path::PathBuf::from("/tmp")).build();
290 assert_eq!(ctx.cwd, std::path::PathBuf::from("/tmp"));
291 assert!(ctx.session_id.is_none());
292 assert!(ctx.is_idle());
293 }
294
295 #[test]
296 fn test_context_builder_full() {
297 use parking_lot::RwLock;
298 let settings = Arc::new(RwLock::new(Settings::default()));
299 let ctx = ExtensionContextBuilder::new(std::path::PathBuf::from("/home"))
300 .settings(settings)
301 .config(serde_json::json!({"key": "value"}))
302 .session_id("sess-123")
303 .build();
304
305 assert_eq!(ctx.cwd, std::path::PathBuf::from("/home"));
306 assert_eq!(ctx.session_id, Some("sess-123".to_string()));
307 assert_eq!(ctx.config_get("key"), Some(serde_json::json!("value")));
308 }
309
310 #[test]
311 fn test_registry_register_and_collect() {
312 let mut reg = ExtensionRegistry::new();
313 reg.register(Arc::new(NoopExtension));
314 assert_eq!(reg.len(), 1);
315 assert!(!reg.is_empty());
316 }
317
318 #[test]
319 fn test_registry_enable_disable() {
320 let mut reg = ExtensionRegistry::new();
321 let ext = Arc::new(RecordingExtension::new("rec"));
322 reg.register(ext);
323 let ctx = ExtensionContextBuilder::new(std::path::PathBuf::from("/tmp")).build();
324 assert!(reg.is_enabled("rec"));
325 reg.disable("rec").unwrap();
326 assert!(!reg.is_enabled("rec"));
327 reg.enable("rec", &ctx).unwrap();
328 assert!(reg.is_enabled("rec"));
329 }
330
331 #[test]
332 fn test_emit_load() {
333 let mut reg = ExtensionRegistry::new();
334 let ext = Arc::new(RecordingExtension::new("rec"));
335 reg.register(ext.clone());
336 let ctx = ExtensionContextBuilder::new(std::path::PathBuf::from("/tmp")).build();
337 reg.emit_load(&ctx);
338 assert_eq!(ext.calls(), vec!["on_load"]);
339 }
340
341 #[test]
342 fn test_graceful_degradation_on_panic() {
343 struct PanickingExtension;
344 impl Extension for PanickingExtension {
345 fn name(&self) -> &str {
346 "panicker"
347 }
348 fn description(&self) -> &str {
349 "Panics"
350 }
351 fn on_load(&self, _ctx: &ExtensionContext) {
352 panic!("intentional panic in on_load");
353 }
354 fn on_message_sent(&self, _msg: &str) {
355 panic!("intentional panic in on_message_sent");
356 }
357 }
358
359 let mut reg = ExtensionRegistry::new();
360 reg.register(Arc::new(PanickingExtension));
361 let ctx = ExtensionContextBuilder::new(std::path::PathBuf::from("/tmp")).build();
362 reg.emit_load(&ctx);
363 reg.emit_message_sent("hello");
364 let errors = reg.errors();
365 assert_eq!(errors.len(), 2);
366 }
367
368 #[test]
369 fn test_extension_state_display() {
370 assert_eq!(ExtensionState::Pending.to_string(), "pending");
371 assert_eq!(ExtensionState::Active.to_string(), "active");
372 }
373
374 #[test]
375 fn test_tool_call_emit_result_default() {
376 let result = ToolCallEmitResult::default();
377 assert!(!result.blocked);
378 assert!(result.errors.is_empty());
379 }
380
381 #[test]
382 fn test_runner_new() {
383 let runner = ExtensionRunner::new(std::path::PathBuf::from("/tmp"));
384 assert!(runner.is_empty());
385 assert_eq!(runner.len(), 0);
386 }
387}