Skip to main content

exomonad_core/
lib.rs

1//! ExoMonad Core: effect system, WASM hosting, MCP server, built-in handlers, shared types.
2//!
3//! # Architecture
4//!
5//! ```text
6//! WASM Guest (Haskell) - pure logic
7//!     │
8//!     │ yield_effect(EffectEnvelope)
9//!     ▼
10//! PluginManager (single host function: yield_effect)
11//!     │
12//!     │ EffectRegistry::dispatch by namespace
13//!     ▼
14//! EffectHandler implementations (git, github, agent, fs, ...)
15//! ```
16//!
17//! # Features
18//!
19//! - **`runtime`** (default): Full runtime with WASM hosting, effect handlers, MCP server,
20//!   and all service integrations. This is what the `exomonad` binary uses.
21//! - Without `runtime`: Only lightweight UI protocol types (`ui_protocol` module).
22//!   Used by `exomonad-plugin` (Zellij WASM target) which can't link heavy native deps.
23//!
24//! # Usage
25//!
26//! ```rust,ignore
27//! use exomonad_core::{RuntimeBuilder, EffectHandler, EffectResult};
28//! use async_trait::async_trait;
29//!
30//! struct MyHandler;
31//!
32//! #[async_trait]
33//! impl EffectHandler for MyHandler {
34//!     fn namespace(&self) -> &str { "my_domain" }
35//!     async fn handle(&self, effect_type: &str, payload: &[u8]) -> EffectResult<Vec<u8>> {
36//!         todo!()
37//!     }
38//! }
39//!
40//! let runtime = RuntimeBuilder::new()
41//!     .with_effect_handler(MyHandler)
42//!     .with_wasm_bytes(wasm_bytes)
43//!     .build()
44//!     .await?;
45//! ```
46
47// === Always available (lightweight types for plugin consumers) ===
48pub mod ui_protocol;
49
50// === Framework (requires runtime feature) ===
51#[cfg(feature = "runtime")]
52pub mod common;
53#[cfg(feature = "runtime")]
54pub mod effects;
55#[cfg(feature = "runtime")]
56pub mod mcp;
57#[cfg(feature = "runtime")]
58pub mod plugin_manager;
59
60// === Shared types and utilities (requires runtime feature) ===
61#[cfg(feature = "runtime")]
62pub mod domain;
63#[cfg(feature = "runtime")]
64pub mod error;
65#[cfg(feature = "runtime")]
66pub mod ffi;
67#[cfg(feature = "runtime")]
68pub mod hooks;
69#[cfg(feature = "runtime")]
70pub mod logging;
71#[cfg(feature = "runtime")]
72pub mod protocol;
73#[cfg(feature = "runtime")]
74pub mod util;
75
76// === Handlers and services (requires runtime feature) ===
77#[cfg(feature = "runtime")]
78pub mod handlers;
79#[cfg(feature = "runtime")]
80pub mod layout;
81#[cfg(feature = "runtime")]
82pub mod services;
83
84// --- Framework re-exports ---
85#[cfg(feature = "runtime")]
86pub use common::{ErrorCode, ErrorContext, HostError, HostResult};
87#[cfg(feature = "runtime")]
88pub use effects::{EffectError, EffectHandler, EffectRegistry, EffectResult};
89#[cfg(feature = "runtime")]
90pub use plugin_manager::PluginManager;
91
92// --- Shared type re-exports ---
93#[cfg(feature = "runtime")]
94pub use domain::{
95    AbsolutePath, DomainError, GithubOwner, GithubRepo, IssueNumber, PathError, Role, SessionId,
96    ToolName, ToolPermission,
97};
98#[cfg(feature = "runtime")]
99pub use error::{ExoMonadError, Result};
100#[cfg(feature = "runtime")]
101pub use ffi::{
102    ErrorCode as FFIErrorCode, ErrorContext as FFIErrorContext, FFIBoundary, FFIError, FFIResult,
103};
104#[cfg(feature = "runtime")]
105pub use hooks::HookConfig;
106#[cfg(feature = "runtime")]
107pub use logging::{init_logging, init_logging_with_default};
108#[cfg(feature = "runtime")]
109pub use protocol::{
110    ClaudePreToolUseOutput, ClaudeStopHookOutput, GeminiStopHookOutput, HookEventType, HookInput,
111    HookSpecificOutput, InternalStopHookOutput, PermissionDecision, Runtime as ProtocolRuntime,
112    StopDecision,
113};
114#[cfg(feature = "runtime")]
115pub use util::{build_prompt, find_exomonad_binary, shell_quote};
116
117// --- Handler re-exports ---
118#[cfg(feature = "runtime")]
119pub use handlers::{
120    AgentHandler, CopilotHandler, FilePRHandler, FsHandler, GitHandler, GitHubHandler, LogHandler,
121    PopupHandler,
122};
123#[cfg(feature = "runtime")]
124pub use services::{Services, ValidatedServices};
125
126/// Prelude module for convenient imports.
127#[cfg(feature = "runtime")]
128pub mod prelude {
129    pub use crate::handlers::*;
130    pub use crate::services::{Services, ValidatedServices};
131}
132
133#[cfg(feature = "runtime")]
134use std::path::PathBuf;
135#[cfg(feature = "runtime")]
136use std::sync::Arc;
137
138/// Builder for constructing a runtime with custom effect handlers.
139///
140/// # Example
141///
142/// ```rust,ignore
143/// use exomonad_core::{RuntimeBuilder, EffectHandler};
144///
145/// let runtime = RuntimeBuilder::new()
146///     .with_effect_handler(MyCustomHandler::new())
147///     .with_wasm_bytes(wasm_bytes)
148///     .build()
149///     .await?;
150/// ```
151#[cfg(feature = "runtime")]
152pub struct RuntimeBuilder {
153    registry: EffectRegistry,
154    wasm_bytes: Option<Vec<u8>>,
155}
156
157#[cfg(feature = "runtime")]
158impl RuntimeBuilder {
159    /// Create a new runtime builder with an empty effect registry.
160    pub fn new() -> Self {
161        Self {
162            registry: EffectRegistry::new(),
163            wasm_bytes: None,
164        }
165    }
166
167    /// Register a custom effect handler.
168    ///
169    /// The handler will be dispatched for effects matching its namespace prefix.
170    pub fn with_effect_handler(mut self, handler: impl EffectHandler + 'static) -> Self {
171        self.registry.register_owned(handler);
172        self
173    }
174
175    /// Register an Arc-wrapped effect handler.
176    pub fn with_effect_handler_arc(mut self, handler: Arc<dyn EffectHandler>) -> Self {
177        self.registry.register(handler);
178        self
179    }
180
181    /// Set the WASM plugin bytes (embedded at compile time).
182    pub fn with_wasm_bytes(mut self, bytes: Vec<u8>) -> Self {
183        self.wasm_bytes = Some(bytes);
184        self
185    }
186
187    /// Get a reference to the effect registry.
188    pub fn registry(&self) -> &EffectRegistry {
189        &self.registry
190    }
191
192    /// Consume the builder and return the effect registry.
193    pub fn into_registry(self) -> EffectRegistry {
194        self.registry
195    }
196
197    /// Build the runtime with all configured handlers.
198    ///
199    /// # Errors
200    ///
201    /// Returns an error if:
202    /// - WASM bytes are not set
203    /// - WASM plugin loading fails
204    pub async fn build(self) -> anyhow::Result<Runtime> {
205        let wasm_bytes = self
206            .wasm_bytes
207            .ok_or_else(|| anyhow::anyhow!("WASM bytes not set"))?;
208
209        let registry = Arc::new(self.registry);
210        let plugin_manager = PluginManager::new(&wasm_bytes, registry.clone()).await?;
211
212        Ok(Runtime {
213            plugin_manager,
214            registry,
215        })
216    }
217}
218
219#[cfg(feature = "runtime")]
220impl Default for RuntimeBuilder {
221    fn default() -> Self {
222        Self::new()
223    }
224}
225
226/// Configured runtime with WASM plugin and effect handlers.
227#[cfg(feature = "runtime")]
228pub struct Runtime {
229    /// WASM plugin manager for calling guest functions.
230    pub plugin_manager: PluginManager,
231
232    /// Effect registry for custom effect dispatch.
233    pub registry: Arc<EffectRegistry>,
234}
235
236#[cfg(feature = "runtime")]
237impl Runtime {
238    /// Get a reference to the plugin manager.
239    pub fn plugin_manager(&self) -> &PluginManager {
240        &self.plugin_manager
241    }
242
243    /// Get a reference to the effect registry.
244    pub fn registry(&self) -> &EffectRegistry {
245        &self.registry
246    }
247
248    /// Dispatch an effect to the appropriate handler.
249    pub async fn dispatch_effect(
250        &self,
251        effect_type: &str,
252        payload: &[u8],
253    ) -> EffectResult<Vec<u8>> {
254        self.registry.dispatch(effect_type, payload).await
255    }
256
257    /// Convert into MCP state for running the stdio server.
258    pub fn into_mcp_state(self, project_dir: PathBuf) -> mcp::McpState {
259        mcp::McpState {
260            project_dir,
261            plugin: Arc::new(self.plugin_manager),
262        }
263    }
264}
265
266/// Register all built-in handlers with a RuntimeBuilder.
267#[cfg(feature = "runtime")]
268pub fn register_builtin_handlers(
269    builder: RuntimeBuilder,
270    services: &Arc<ValidatedServices>,
271) -> RuntimeBuilder {
272    let mut builder = builder;
273
274    builder = builder.with_effect_handler(handlers::GitHandler::new(services.git().clone()));
275
276    if let Some(github) = services.github() {
277        builder = builder.with_effect_handler(handlers::GitHubHandler::new(github.clone()));
278    }
279
280    builder = builder.with_effect_handler(handlers::LogHandler::new());
281
282    builder = builder.with_effect_handler(handlers::AgentHandler::new(
283        services.agent_control().clone(),
284    ));
285
286    builder = builder.with_effect_handler(handlers::FsHandler::new(services.filesystem().clone()));
287
288    if let Some(session) = services.zellij_session() {
289        builder = builder.with_effect_handler(handlers::PopupHandler::new(session.to_string()));
290    }
291
292    builder = builder.with_effect_handler(handlers::FilePRHandler::new());
293
294    builder = builder.with_effect_handler(handlers::CopilotHandler::new());
295
296    builder
297}