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