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;