Skip to main content

oxi/extensions/
types.rs

1//! Extension types: enums, structs, events, and emit results.
2//!
3//! This module contains all the data types used across the extension system.
4
5use oxi_ai::Message;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::HashMap;
9use std::fmt;
10use std::path::PathBuf;
11
12// Re-export from oxi-agent
13pub use oxi_agent::{AgentEvent, AgentTool, AgentToolResult};
14
15// ═══════════════════════════════════════════════════════════════════════════
16// Extension Permissions
17// ═══════════════════════════════════════════════════════════════════════════
18
19/// Permissions that an extension can request.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum ExtensionPermission {
23    /// Permission to read files.
24    FileRead,
25    /// Permission to write files.
26    FileWrite,
27    /// Permission to execute shell commands.
28    Bash,
29    /// Permission to make network requests.
30    Network,
31}
32
33impl fmt::Display for ExtensionPermission {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            ExtensionPermission::FileRead => write!(f, "file_read"),
37            ExtensionPermission::FileWrite => write!(f, "file_write"),
38            ExtensionPermission::Bash => write!(f, "bash"),
39            ExtensionPermission::Network => write!(f, "network"),
40        }
41    }
42}
43
44// ═══════════════════════════════════════════════════════════════════════════
45// Extension Manifest
46// ═══════════════════════════════════════════════════════════════════════════
47
48/// Metadata describing an extension's identity, permissions, and configuration.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ExtensionManifest {
51    /// Extension name.
52    pub name: String,
53    /// Semantic version string.
54    pub version: String,
55    /// Human-readable description.
56    #[serde(default)]
57    pub description: String,
58    /// Author or maintainers.
59    #[serde(default)]
60    pub author: String,
61    /// Requested permissions.
62    #[serde(default)]
63    pub permissions: Vec<ExtensionPermission>,
64    /// Optional JSON Schema for extension configuration.
65    #[serde(default)]
66    pub config_schema: Option<Value>,
67}
68
69impl ExtensionManifest {
70    /// Create a new manifest with the given name and version.
71    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
72        Self {
73            name: name.into(),
74            version: version.into(),
75            description: String::new(),
76            author: String::new(),
77            permissions: Vec::new(),
78            config_schema: None,
79        }
80    }
81    /// Set the description.
82    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
83        self.description = desc.into();
84        self
85    }
86    /// Set the author.
87    pub fn with_author(mut self, author: impl Into<String>) -> Self {
88        self.author = author.into();
89        self
90    }
91    /// Add a permission.
92    pub fn with_permission(mut self, perm: ExtensionPermission) -> Self {
93        if !self.permissions.contains(&perm) {
94            self.permissions.push(perm);
95        }
96        self
97    }
98    /// Set the configuration JSON Schema.
99    pub fn with_config_schema(mut self, schema: Value) -> Self {
100        self.config_schema = Some(schema);
101        self
102    }
103    /// Check whether the manifest includes a specific permission.
104    pub fn has_permission(&self, perm: ExtensionPermission) -> bool {
105        self.permissions.contains(&perm)
106    }
107}
108
109// ═══════════════════════════════════════════════════════════════════════════
110// Extension Error Handling
111// ═══════════════════════════════════════════════════════════════════════════
112
113/// Errors that can occur during extension operations.
114#[derive(Debug, thiserror::Error)]
115pub enum ExtensionError {
116    /// The requested extension was not found.
117    #[error("Extension '{name}' not found")]
118    NotFound {
119        /// Extension name.
120        name: String,
121    },
122    /// The extension failed to load.
123    #[error("Failed to load extension '{name}': {reason}")]
124    LoadFailed {
125        /// Extension name.
126        name: String,
127        /// Reason for the failure.
128        reason: String,
129    },
130    /// An extension hook invocation failed.
131    #[error("Extension '{name}' hook '{hook}' failed: {error}")]
132    HookFailed {
133        /// Extension name.
134        name: String,
135        /// Hook that failed.
136        hook: String,
137        /// Error message.
138        error: String,
139    },
140    /// The extension lacks the required permission.
141    #[error("Extension '{name}' requires permission '{permission}'")]
142    PermissionDenied {
143        /// Extension name.
144        name: String,
145        /// Required permission.
146        permission: ExtensionPermission,
147    },
148    /// The extension is currently disabled.
149    #[error("Extension '{name}' is disabled")]
150    Disabled {
151        /// Extension name.
152        name: String,
153    },
154    /// Hot-reload of the extension failed.
155    #[error("Hot-reload of extension '{name}' failed: {reason}")]
156    HotReloadFailed {
157        /// Extension name.
158        name: String,
159        /// Reason for the failure.
160        reason: String,
161    },
162    /// The extension configuration is invalid.
163    #[error("Invalid configuration for extension '{name}': {reason}")]
164    InvalidConfig {
165        /// Extension name.
166        name: String,
167        /// Reason for the failure.
168        reason: String,
169    },
170}
171
172/// A recorded extension error for auditing and diagnostics.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct ExtensionErrorRecord {
175    /// Name of the extension that produced the error.
176    pub extension_name: String,
177    /// Event or hook during which the error occurred.
178    pub event: String,
179    /// Error message.
180    pub error: String,
181    /// Optional stack trace.
182    #[serde(default)]
183    pub stack: Option<String>,
184    /// Unix-millis timestamp when the error was recorded.
185    pub timestamp: i64,
186}
187
188impl ExtensionErrorRecord {
189    /// Create a new error record.
190    pub fn new(
191        extension_name: impl Into<String>,
192        event: impl Into<String>,
193        error: impl Into<String>,
194    ) -> Self {
195        Self {
196            extension_name: extension_name.into(),
197            event: event.into(),
198            error: error.into(),
199            stack: None,
200            timestamp: chrono::Utc::now().timestamp_millis(),
201        }
202    }
203}
204
205// ═══════════════════════════════════════════════════════════════════════════
206// Event Enums
207// ═══════════════════════════════════════════════════════════════════════════
208
209/// Reason why a session is being switched.
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "snake_case")]
212pub enum SessionSwitchReason {
213    /// Starting a new session.
214    New,
215    /// Resuming an existing session.
216    Resume,
217}
218
219/// Reason why a session is shutting down.
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
221#[serde(rename_all = "snake_case")]
222pub enum SessionShutdownReason {
223    /// User quit the application.
224    Quit,
225    /// Session is being reloaded.
226    Reload,
227    /// Switching to a new session.
228    New,
229    /// Resuming a different session.
230    Resume,
231    /// Forking the session.
232    Fork,
233}
234
235/// How a model selection was triggered.
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
237#[serde(rename_all = "snake_case")]
238pub enum ModelSelectSource {
239    /// Model was explicitly set.
240    Set,
241    /// Model was changed via cycling.
242    Cycle,
243    /// Model was restored to a previous value.
244    Restore,
245}
246
247/// Source of user input in the extension system.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
249#[serde(rename_all = "snake_case")]
250pub enum InputSource {
251    /// Interactive terminal input.
252    Interactive,
253    /// RPC call input.
254    Rpc,
255    /// Extension-generated input.
256    Extension,
257}
258
259/// Result of an input event hook.
260#[derive(Debug, Clone)]
261pub enum InputEventResult {
262    /// Continue processing without modification.
263    Continue,
264    /// Transform the input text.
265    Transform {
266        /// Replacement text.
267        text: String,
268    },
269    /// Input was fully handled; stop propagation.
270    Handled,
271}
272
273// ═══════════════════════════════════════════════════════════════════════════
274// Event Structs
275// ═══════════════════════════════════════════════════════════════════════════
276
277/// Event fired before a session switch.
278#[derive(Debug, Clone)]
279pub struct SessionBeforeSwitchEvent {
280    /// Why the switch is happening.
281    pub reason: SessionSwitchReason,
282    /// Target session file path, if applicable.
283    pub target_session_file: Option<String>,
284}
285
286/// Event fired before a session fork.
287#[derive(Debug, Clone)]
288pub struct SessionBeforeForkEvent {
289    /// Entry ID at which to fork.
290    pub entry_id: String,
291    /// Fork position descriptor.
292    pub position: String,
293}
294
295/// Event fired before compaction runs.
296#[derive(Debug, Clone)]
297pub struct SessionBeforeCompactEvent {
298    /// Number of messages before compaction.
299    pub messages_count: usize,
300    /// Token count before compaction.
301    pub tokens_before: usize,
302    /// Target token count.
303    pub target_tokens: usize,
304    /// Optional custom compaction instructions.
305    pub custom_instructions: Option<String>,
306}
307
308/// Event fired after compaction completes.
309#[derive(Debug, Clone)]
310pub struct SessionCompactEvent {
311    /// Number of messages after compaction.
312    pub messages_count: usize,
313    /// Token count after compaction.
314    pub tokens_after: usize,
315    /// Whether the compaction was requested by an extension.
316    pub from_extension: bool,
317}
318
319/// Event fired when a session is shutting down.
320#[derive(Debug, Clone)]
321pub struct SessionShutdownEvent {
322    /// Why the session is shutting down.
323    pub reason: SessionShutdownReason,
324    /// Target session file path, if applicable.
325    pub target_session_file: Option<String>,
326}
327
328/// Event fired before navigating the session tree.
329#[derive(Debug, Clone)]
330pub struct SessionBeforeTreeEvent {
331    /// Target entry ID to navigate to.
332    pub target_id: String,
333    /// Previous leaf entry ID, if any.
334    pub old_leaf_id: Option<String>,
335}
336
337/// Event fired after a session tree navigation.
338#[derive(Debug, Clone)]
339pub struct SessionTreeEvent {
340    /// New leaf entry ID after navigation.
341    pub new_leaf_id: Option<String>,
342    /// Previous leaf entry ID.
343    pub old_leaf_id: Option<String>,
344    /// Whether the navigation was triggered by an extension.
345    pub from_extension: bool,
346}
347
348/// Event carrying the current context messages for modification.
349#[derive(Debug, Clone)]
350pub struct ContextEvent {
351    /// Messages in the current context.
352    pub messages: Vec<Message>,
353}
354
355/// Event fired before a provider request is sent.
356#[derive(Debug, Clone)]
357pub struct BeforeProviderRequestEvent {
358    /// JSON payload that will be sent to the provider.
359    pub payload: Value,
360}
361
362/// Event fired after a provider response is received.
363#[derive(Debug, Clone)]
364pub struct AfterProviderResponseEvent {
365    /// HTTP status code.
366    pub status: u16,
367    /// Response headers.
368    pub headers: HashMap<String, String>,
369}
370
371/// Event fired when a model is selected or changed.
372#[derive(Debug, Clone)]
373pub struct ModelSelectEvent {
374    /// Newly selected model identifier.
375    pub model: String,
376    /// Previous model identifier.
377    pub previous_model: Option<String>,
378    /// How the selection was triggered.
379    pub source: ModelSelectSource,
380}
381
382/// Event fired when a thinking level is selected or changed.
383#[derive(Debug, Clone)]
384pub struct ThinkingLevelSelectEvent {
385    /// New thinking level name.
386    pub level: String,
387    /// Previous thinking level name.
388    pub previous_level: String,
389}
390
391/// Event fired when a bash command is executed.
392#[derive(Debug, Clone)]
393pub struct BashEvent {
394    /// Command being executed.
395    pub command: String,
396    /// Whether this command should be excluded from the LLM context.
397    pub exclude_from_context: bool,
398    /// Working directory for the command.
399    pub cwd: PathBuf,
400}
401
402/// Event fired when user input is received.
403#[derive(Debug, Clone)]
404pub struct InputEvent {
405    /// The input text.
406    pub text: String,
407    /// Where the input originated.
408    pub source: InputSource,
409}
410
411// ═══════════════════════════════════════════════════════════════════════════
412// Extension State
413// ═══════════════════════════════════════════════════════════════════════════
414
415/// Lifecycle state of an extension.
416#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
417#[serde(rename_all = "snake_case")]
418pub enum ExtensionState {
419    /// Extension is loaded but not yet activated.
420    Pending,
421    /// Extension is active and running.
422    Active,
423    /// Extension has been disabled.
424    Disabled,
425    /// Extension failed to load or run.
426    Failed,
427    /// Extension has been unloaded.
428    Unloaded,
429}
430
431impl fmt::Display for ExtensionState {
432    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433        match self {
434            ExtensionState::Pending => write!(f, "pending"),
435            ExtensionState::Active => write!(f, "active"),
436            ExtensionState::Disabled => write!(f, "disabled"),
437            ExtensionState::Failed => write!(f, "failed"),
438            ExtensionState::Unloaded => write!(f, "unloaded"),
439        }
440    }
441}
442
443// ═══════════════════════════════════════════════════════════════════════════
444// Emit Result Types
445// ═══════════════════════════════════════════════════════════════════════════
446
447/// Result of emitting a tool-call event to extensions.
448#[derive(Debug, Default)]
449pub struct ToolCallEmitResult {
450    /// Whether the tool call was blocked by an extension.
451    pub blocked: bool,
452    /// Reason the call was blocked, if applicable.
453    pub block_reason: Option<String>,
454    /// Per-extension errors encountered.
455    pub errors: Vec<(String, String)>,
456}
457
458/// Result of emitting a tool-result event to extensions.
459#[derive(Debug, Default)]
460pub struct ToolResultEmitResult {
461    /// Optional replacement output from an extension.
462    pub output: Option<String>,
463    /// Optional override for the success flag.
464    pub success: Option<bool>,
465    /// Per-extension errors encountered.
466    pub errors: Vec<(String, String)>,
467}
468
469/// Result of emitting a context event to extensions.
470#[derive(Debug)]
471pub struct ContextEmitResult {
472    /// Whether the messages were modified by any extension.
473    pub modified: bool,
474    /// The (possibly modified) messages.
475    pub messages: Vec<Message>,
476    /// Per-extension errors encountered.
477    pub errors: Vec<(String, String)>,
478}
479
480/// Result of emitting a before-provider-request event to extensions.
481#[derive(Debug)]
482pub struct ProviderRequestEmitResult {
483    /// Whether the payload was modified by any extension.
484    pub modified: bool,
485    /// The (possibly modified) payload.
486    pub payload: Value,
487    /// Per-extension errors encountered.
488    pub errors: Vec<(String, String)>,
489}
490
491/// Result of emitting a session-before event to extensions.
492#[derive(Debug, Default)]
493pub struct SessionBeforeEmitResult {
494    /// Whether the operation was cancelled by an extension.
495    pub cancelled: bool,
496    /// Name of the extension that cancelled the operation.
497    pub cancelled_by: Option<String>,
498    /// Per-extension errors encountered.
499    pub errors: Vec<(String, String)>,
500}
501
502// ═══════════════════════════════════════════════════════════════════════════
503// Commands
504// ═══════════════════════════════════════════════════════════════════════════
505
506/// A slash-command registered by an extension.
507#[derive(Debug, Clone)]
508pub struct Command {
509    /// Command name (e.g. "my-cmd").
510    pub name: String,
511    /// Short description shown in help.
512    pub description: String,
513    /// Usage string (e.g. "/my-cmd <arg>").
514    pub usage: String,
515}
516impl Command {
517    /// Create a new command descriptor.
518    pub fn new(
519        name: impl Into<String>,
520        description: impl Into<String>,
521        usage: impl Into<String>,
522    ) -> Self {
523        Self {
524            name: name.into(),
525            description: description.into(),
526            usage: usage.into(),
527        }
528    }
529}
530
531// ═══════════════════════════════════════════════════════════════════════════
532// Error Listener
533// ═══════════════════════════════════════════════════════════════════════════
534
535/// Type alias for a closure that listens to extension errors.
536pub type ExtensionErrorListener = dyn Fn(&ExtensionErrorRecord) + Send + Sync;