Skip to main content

oxicode_sdk/
builder.rs

1//! OxicodeBuilder and Oxicode — SDK entry point
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use oxicode_agent::{ProviderResolver, ToolRegistry};
7use oxicode_ai::{Model, ModelRegistry, Provider, ProviderRegistry};
8
9use crate::agent_builder::AgentBuilder;
10use crate::error::{SdkError, SdkResult};
11use crate::lifecycle::{AgentSupervisor, FileSnapshotStore, SupervisorPolicy};
12use crate::ports::PortRegistry;
13
14/// Oxicode AI engine instance — holds isolated provider and model registries.
15///
16/// Created via [`OxicodeBuilder`]. Provides access to providers, models,
17/// provider creation, and agent building.
18///
19/// Implements [`ProviderResolver`] so it can be passed directly to
20/// [`oxicode_agent::Agent::new_with_resolver`] for fully isolated operation.
21#[derive(Clone)]
22pub struct Oxicode {
23    providers: Arc<ProviderRegistry>,
24    models: Arc<ModelRegistry>,
25    tools: Arc<ToolRegistry>,
26    /// Whether built-in providers are enabled (`OxicodeBuilder::with_builtins`).
27    include_builtins: bool,
28    /// Per-provider API key overrides (`OxicodeBuilder::api_key`).
29    api_keys: Arc<HashMap<String, String>>,
30    /// Per-provider base URL overrides (`OxicodeBuilder::base_url`).
31    base_urls: Arc<HashMap<String, String>>,
32    /// Port registry (None = use noop default).
33    ports: PortRegistry,
34    /// MCP manager (Phase 1+). `None` if MCP is disabled or has not been
35    /// spawned yet.
36    mcp_manager: Option<Arc<oxicode_agent::mcp::McpManager>>,
37    /// Live routing state. `Arc` so external holders (the supervisor,
38    /// agent builders, host apps) share the same instance and see
39    /// each other's mutations. Resolution-time exclusion of models
40    /// declared in `excluded_models` consults this field.
41    routing: Arc<crate::routing::RoutingControl>,
42}
43
44impl Oxicode {
45    /// Create an agent builder with the given config.
46    pub fn agent(&self, config: oxicode_agent::AgentConfig) -> AgentBuilder<'_> {
47        AgentBuilder::new(self, config)
48    }
49
50    /// Get the provider registry.
51    pub fn providers(&self) -> &ProviderRegistry {
52        &self.providers
53    }
54
55    /// Get the model registry.
56    pub fn models(&self) -> &ModelRegistry {
57        &self.models
58    }
59
60    /// Get the shared tool registry.
61    pub fn tools(&self) -> Arc<ToolRegistry> {
62        Arc::clone(&self.tools)
63    }
64
65    /// Get the port registry (state, config, auth, event bus, ...).
66    pub fn ports(&self) -> &PortRegistry {
67        &self.ports
68    }
69
70    /// Catalog port accessor. Use this for all catalog queries.
71    ///
72    /// Returns a reference to the `Arc<dyn ModelCatalog>`. The default
73    /// (when `OxicodeBuilder::with_catalog()` is not called) is a
74    /// [`NoopModelCatalog`](crate::ports::catalog::NoopModelCatalog) —
75    /// all lookups return empty/None.
76    ///
77    /// # Example
78    ///
79    /// ```no_run
80    /// # async fn doc(oxicode: oxicode_sdk::Oxicode) -> Result<(), oxicode_sdk::SdkError> {
81    /// let providers = oxicode.catalog().list_providers().await?;
82    /// let model = oxicode.catalog().get_model("anthropic", "claude-sonnet-4-20250514").await?;
83    /// # Ok(()) }
84    /// ```
85    pub fn catalog(&self) -> &Arc<dyn crate::ports::catalog::ModelCatalog> {
86        &self.ports.catalog
87    }
88
89    /// Get the MCP manager, if MCP is enabled.
90    ///
91    /// This is the entry point for SDK consumers who want to use MCP from
92    /// outside the agent loop — e.g. the TUI dashboard, RPC handlers, or
93    /// custom agent integrations.
94    ///
95    /// Returns `None` if MCP was disabled via [`OxicodeBuilder::with_mcp`] with
96    /// `false`.
97    pub fn mcp(&self) -> Option<Arc<oxicode_agent::mcp::McpManager>> {
98        self.mcp_manager.clone()
99    }
100
101    /// Resolve a model ID to a Model.
102    ///
103    /// Accepts `"provider/model"` or bare `"model"` (defaults to "anthropic").
104    ///
105    /// Resolution order:
106    /// 1. The catalog port (if wired) — reads the in-memory snapshot.
107    /// 2. The static model registry (`with_builtins`).
108    ///
109    /// The `routing.excluded_models` list is consulted **before** the
110    /// catalog/static lookups — `set_enabled(false)` / `exclude_model`
111    /// / `unexclude_model` on the shared `RoutingControl` instance
112    /// take effect on the next resolution.
113    pub fn resolve_model(&self, model_id: &str) -> SdkResult<Model> {
114        // Live routing exclusion: ONLY active when is_enabled().
115        // `set_enabled(false)` is an explicit opt-out — it means
116        // "skip routing rules, resolve normally," NOT "refuse to
117        // resolve." The default Oxicode (RoutingControl::default) has
118        // auto_routing=true, so this gate is a no-op unless the
119        // host explicitly disabled routing.
120        if self.routing.is_enabled() && self.routing.excluded_models().iter().any(|m| m == model_id)
121        {
122            return Err(SdkError::ModelExcluded {
123                model_id: model_id.to_string(),
124            });
125        }
126
127        let parts: Vec<&str> = model_id.splitn(2, '/').collect();
128        let (provider, model) = if parts.len() == 2 {
129            (parts[0], parts[1])
130        } else {
131            ("anthropic", parts[0])
132        };
133
134        // 1. Catalog port (sync read of the snapshot).
135        if let Some(ref entry) = self.ports.catalog.get_model_sync(provider, model) {
136            return Ok(crate::bridge::catalog_entry_to_model(provider, entry));
137        }
138
139        // 2. Static model registry fallback.
140        self.models
141            .lookup(provider, model)
142            .ok_or_else(|| SdkError::ModelNotFound {
143                model_id: model_id.to_string(),
144            })
145    }
146
147    /// 1. Custom providers registered via `OxicodeBuilder::provider()`
148    /// 2. Provider factories registered via `OxicodeBuilder::provider_factory()`
149    /// 3. Built-in providers with credential injection (if `with_builtins()` was called):
150    ///    a. Explicit per-provider key from `OxicodeBuilder::api_key(name, key)`
151    ///    b. The wired `AuthProvider` port (sync fast-path). This is the
152    ///    primary credential source for products like the CLI, which
153    ///    never call `OxicodeBuilder::api_key()` and instead register
154    ///    `FileAuthProvider` via `.with_auth(...)`. Consulted on every
155    ///    `create_provider` call, so auth-store updates (e.g. a key entered
156    ///    via the TUI overlay) are picked up without rebuilding the engine.
157    ///    c. Provider env var (the `create_builtin_provider_with_options`
158    ///    fallback inside `oxicode-ai`).
159    ///
160    /// This is the **single credential authority** for the agent loop: the
161    /// `AgentConfig.api_key` field and the `api_key` params on
162    /// `Agent::switch_model` / `Agent::refresh_api_key` are vestigial after
163    /// this wiring and are removed in a follow-up. See issues #39 and #40.
164    pub fn create_provider(&self, name: &str) -> SdkResult<Arc<dyn Provider>> {
165        // 1. Check custom providers registered via OxicodeBuilder::provider()
166        if let Some(p) = self.providers.get_custom(name) {
167            return Ok(p);
168        }
169        // 2. Built-in providers with credential injection.
170        if self.include_builtins {
171            let base_url = self.base_urls.get(name).map(|s| s.as_str());
172            // Credential resolution: explicit OxicodeBuilder::api_key() override first,
173            // then the AuthProvider port's sync fast-path, then env-var fallback
174            // (handled inside create_builtin_provider_with_options).
175            let explicit_key = self.api_keys.get(name).map(|s| s.as_str());
176            let auth_port_key = self
177                .ports
178                .auth
179                .get_api_key_sync(name)
180                .ok()
181                .flatten()
182                .filter(|s| !s.is_empty());
183            let api_key = explicit_key.or(auth_port_key.as_deref());
184            if let Some(p) =
185                oxicode_ai::create_builtin_provider_with_options(name, api_key, base_url)
186            {
187                return Ok(Arc::from(p));
188            }
189            // Fallback to default built-in creation (no credential override)
190            if let Some(p) = oxicode_ai::create_builtin_provider(name) {
191                return Ok(Arc::from(p));
192            }
193        }
194        Err(SdkError::ProviderNotFound {
195            provider: name.to_string(),
196        })
197    }
198
199    /// Get the provider registry (Arc clone).
200    pub fn providers_arc(&self) -> Arc<ProviderRegistry> {
201        Arc::clone(&self.providers)
202    }
203
204    /// Get the model registry (Arc clone).
205    pub fn models_arc(&self) -> Arc<ModelRegistry> {
206        Arc::clone(&self.models)
207    }
208
209    /// Check whether built-in providers are enabled.
210    pub fn has_builtins(&self) -> bool {
211        self.include_builtins
212    }
213
214    /// Borrow the shared [`crate::routing::RoutingControl`] instance. Use this to
215    /// call `set_enabled`, `exclude_model`, `set_fallback_models`, etc.
216    /// Mutations are observed by the next model/provider resolution.
217    pub fn routing(&self) -> &Arc<crate::routing::RoutingControl> {
218        &self.routing
219    }
220}
221
222/// Implement ProviderResolver so Oxicode can be used as Agent's resolver.
223impl ProviderResolver for Oxicode {
224    fn resolve_provider(&self, name: &str) -> Option<Arc<dyn Provider>> {
225        self.create_provider(name).ok()
226    }
227
228    fn resolve_model(&self, model_id: &str) -> Option<Model> {
229        self.resolve_model(model_id).ok()
230    }
231}
232
233/// Builder for creating an Oxicode instance.
234pub struct OxicodeBuilder {
235    providers: ProviderRegistry,
236    models: ModelRegistry,
237    tools: ToolRegistry,
238    include_builtins: bool,
239    api_keys: HashMap<String, String>,
240    base_urls: HashMap<String, String>,
241    /// Port registry (None = use noop default).
242    ports: Option<PortRegistry>,
243    /// Programmatic MCP config (overrides the on-disk config if set).
244    mcp_config: Option<oxicode_agent::mcp::McpConfig>,
245    /// Whether MCP is enabled. Defaults to true (when `with_builtins()` is
246    /// also called) or as set by `with_mcp(false)`.
247    mcp_enabled: bool,
248    /// Custom disk path for the MCP metadata cache. When unset, oxicode uses
249    /// its default (`~/.config/oxicode/mcp-cache.json`).
250    mcp_cache_path: Option<std::path::PathBuf>,
251    /// Custom disk path for the MCP consent store. When unset, oxicode uses
252    /// its default (`~/.config/oxicode/mcp-consent.json`).
253    mcp_consent_path: Option<std::path::PathBuf>,
254}
255
256impl OxicodeBuilder {
257    /// Create a new empty builder (no builtins, no providers, no models).
258    pub fn new() -> Self {
259        Self {
260            providers: ProviderRegistry::new(),
261            models: ModelRegistry::new(),
262            tools: ToolRegistry::new(),
263            include_builtins: false,
264            api_keys: HashMap::new(),
265            base_urls: HashMap::new(),
266            ports: None,
267            mcp_config: None,
268            mcp_enabled: true,
269            mcp_cache_path: None,
270            mcp_consent_path: None,
271        }
272    }
273
274    /// Register all built-in models and enable built-in provider creation.
275    ///
276    /// This loads 50+ model definitions from the oxicode-ai static database
277    /// and enables `create_builtin_provider()` fallback in [`Oxicode::create_provider`].
278    pub fn with_builtins(mut self) -> Self {
279        self.models = ModelRegistry::from_static();
280        self.include_builtins = true;
281        self
282    }
283
284    /// Register a custom provider.
285    pub fn provider(self, name: &str, p: impl Provider + 'static) -> Self {
286        self.providers.register(name, p);
287        self
288    }
289
290    /// Register a custom provider from a pre-boxed `Arc<dyn Provider>`.
291    /// Useful when the provider comes from a factory that returns a
292    /// trait object (e.g. the Oxi Foundation profile resolver).
293    pub fn provider_arc(self, name: &str, p: Arc<dyn Provider>) -> Self {
294        self.providers.register_arc(name, p);
295        self
296    }
297
298    /// Register a custom tool in the shared tool registry.
299    pub fn tool(self, tool: impl oxicode_agent::AgentTool + 'static) -> Self {
300        self.tools.register(tool);
301        self
302    }
303
304    /// Register a provider factory — a closure that lazily creates a provider.
305    ///
306    /// Unlike [`Self::provider()`], which takes an already-constructed instance,
307    /// this stores a factory closure. The factory is invoked the **first time**
308    /// `Oxicode::create_provider(name)` is called, and the resulting provider is
309    /// cached for subsequent calls.
310    ///
311    /// This is useful when provider construction requires credential resolution
312    /// or network configuration that should happen at first use, not at build time.
313    ///
314    /// # Example
315    ///
316    /// ```no_run
317    /// use std::sync::Arc;
318    /// use oxicode_sdk::{OxicodeBuilder, OpenAiProvider};
319    ///
320    /// let oxicode = OxicodeBuilder::new()
321    ///     .with_builtins()
322    ///     .provider_factory("custom", || {
323    ///         Ok(Arc::new(OpenAiProvider::with_base_url_and_key(
324    ///             "https://api.example.com",
325    ///             Some("key".into()),
326    ///         )))
327    ///     })
328    ///     .build();
329    /// ```
330    pub fn provider_factory(
331        self,
332        name: &str,
333        factory: impl Fn() -> anyhow::Result<Arc<dyn Provider>> + Send + Sync + 'static,
334    ) -> Self {
335        self.providers.register_factory(name, factory);
336        self
337    }
338
339    /// Register an API key for a specific provider.
340    ///
341    /// When `create_provider(name)` is called, the key is injected into
342    /// the provider's constructor automatically. Keys registered here
343    /// take precedence over environment variables.
344    ///
345    /// # Example
346    ///
347    /// ```rust
348    /// use oxicode_sdk::OxicodeBuilder;
349    ///
350    /// let oxicode = OxicodeBuilder::new()
351    ///     .with_builtins()
352    ///     .api_key("anthropic", "sk-ant-test-key")
353    ///     .api_key("openai", "sk-test-key")
354    ///     .build();
355    /// ```
356    pub fn api_key(mut self, provider_name: &str, key: impl Into<String>) -> Self {
357        self.api_keys.insert(provider_name.to_string(), key.into());
358        self
359    }
360
361    /// Register a base URL override for a specific provider.
362    ///
363    /// Useful for OpenAI-compatible providers (ZAI, Groq, etc.)
364    /// that use a different endpoint.
365    ///
366    /// # Example
367    ///
368    /// ```rust
369    /// use oxicode_sdk::OxicodeBuilder;
370    ///
371    /// let oxicode = OxicodeBuilder::new()
372    ///     .with_builtins()
373    ///     .base_url("openai", "https://my-proxy.example.com/v1")
374    ///     .build();
375    /// ```
376    pub fn base_url(mut self, provider_name: &str, url: impl Into<String>) -> Self {
377        self.base_urls.insert(provider_name.to_string(), url.into());
378        self
379    }
380
381    /// Register a full credential set for a provider.
382    ///
383    /// Convenience method combining [`api_key()`](Self::api_key) and
384    /// [`base_url()`](Self::base_url).
385    ///
386    /// # Example
387    ///
388    /// ```rust
389    /// use oxicode_sdk::OxicodeBuilder;
390    ///
391    /// let oxicode = OxicodeBuilder::new()
392    ///     .with_builtins()
393    ///     .credential("openai", "sk-test", Some("https://proxy.example.com/v1"))
394    ///     .build();
395    /// ```
396    pub fn credential(
397        self,
398        provider_name: &str,
399        api_key: impl Into<String>,
400        base_url: Option<&str>,
401    ) -> Self {
402        let mut builder = self.api_key(provider_name, api_key);
403        if let Some(url) = base_url {
404            builder = builder.base_url(provider_name, url);
405        }
406        builder
407    }
408
409    /// Register a custom model.
410    pub fn model(self, model: Model) -> Self {
411        self.models.register(model);
412        self
413    }
414
415    // ─── Port registration ────────────────────────────────────────────────
416    //
417    // Products (oxicode-cli, oxios-kernel, custom apps) register concrete
418    // implementations of the port traits defined in `crate::ports`.
419    // All ports are optional: unset ports use a noop default.
420
421    /// Register a complete [`PortRegistry`] at once.
422    ///
423    /// Use this when you have a fully-built registry (e.g. loaded from a
424    /// directory of file-based adapters). For piecemeal registration, use
425    /// the `with_port_*` methods below.
426    pub fn with_ports(mut self, ports: PortRegistry) -> Self {
427        self.ports = Some(ports);
428        self
429    }
430
431    /// Register the model catalog port.
432    ///
433    /// The catalog is the source of truth for provider/model metadata.
434    /// If not called, the SDK uses [`NoopModelCatalog`](crate::ports::catalog::NoopModelCatalog)
435    /// (empty results — all lookups return `None`/`vec![]`).
436    ///
437    /// # Example
438    ///
439    /// ```no_run
440    /// use oxicode_sdk::{OxicodeBuilder, NoopModelCatalog};
441    ///
442    /// // `NoopModelCatalog` is the empty default used when no catalog is
443    /// // registered — pass any `Arc<dyn ModelCatalog>` here instead.
444    /// let catalog = NoopModelCatalog::new();
445    /// let oxicode = OxicodeBuilder::new()
446    ///     .with_catalog(catalog)
447    ///     .build();
448    /// ```
449    pub fn with_catalog(mut self, catalog: Arc<dyn crate::ports::catalog::ModelCatalog>) -> Self {
450        let mut ports = self.ports.unwrap_or_default();
451        ports.catalog = catalog;
452        self.ports = Some(ports);
453        self
454    }
455
456    /// Register the state store.
457    pub fn with_state(mut self, store: Arc<dyn crate::ports::StateStore>) -> Self {
458        let mut ports = self.ports.unwrap_or_default();
459        ports.state = store;
460        self.ports = Some(ports);
461        self
462    }
463
464    /// Register the config store.
465    pub fn with_config(mut self, store: Arc<dyn crate::ports::ConfigStore>) -> Self {
466        let mut ports = self.ports.unwrap_or_default();
467        ports.config = store;
468        self.ports = Some(ports);
469        self
470    }
471
472    /// Register the auth provider.
473    pub fn with_auth(mut self, auth: Arc<dyn crate::ports::AuthProvider>) -> Self {
474        let mut ports = self.ports.unwrap_or_default();
475        ports.auth = auth;
476        self.ports = Some(ports);
477        self
478    }
479
480    /// Register the event bus.
481    pub fn with_event_bus(mut self, bus: Arc<dyn crate::ports::EventBus>) -> Self {
482        let mut ports = self.ports.unwrap_or_default();
483        ports.event_bus = bus;
484        self.ports = Some(ports);
485        self
486    }
487
488    /// Register the skill loader.
489    pub fn with_skills(mut self, loader: Arc<dyn crate::ports::SkillLoader>) -> Self {
490        let mut ports = self.ports.unwrap_or_default();
491        ports.skills = loader;
492        self.ports = Some(ports);
493        self
494    }
495
496    /// Register the persona provider.
497    pub fn with_personas(mut self, provider: Arc<dyn crate::ports::PersonaProvider>) -> Self {
498        let mut ports = self.ports.unwrap_or_default();
499        ports.personas = provider;
500        self.ports = Some(ports);
501        self
502    }
503
504    /// Register the access gate.
505    pub fn with_access(mut self, gate: Arc<dyn crate::ports::AccessGate>) -> Self {
506        let mut ports = self.ports.unwrap_or_default();
507        ports.access = gate;
508        self.ports = Some(ports);
509        self
510    }
511
512    /// Register the capability resolver.
513    pub fn with_capabilities(
514        mut self,
515        resolver: Arc<dyn crate::ports::CapabilityResolver>,
516    ) -> Self {
517        let mut ports = self.ports.unwrap_or_default();
518        ports.capabilities = resolver;
519        self.ports = Some(ports);
520        self
521    }
522
523    /// Register the memory store.
524    pub fn with_memory(mut self, store: Arc<dyn crate::ports::MemoryStore>) -> Self {
525        let mut ports = self.ports.unwrap_or_default();
526        ports.memory = store;
527        self.ports = Some(ports);
528        self
529    }
530
531    /// Register the cron scheduler.
532    pub fn with_cron(mut self, scheduler: Arc<dyn crate::ports::CronScheduler>) -> Self {
533        let mut ports = self.ports.unwrap_or_default();
534        ports.cron = scheduler;
535        self.ports = Some(ports);
536        self
537    }
538
539    /// Register the resource monitor.
540    pub fn with_resources(mut self, monitor: Arc<dyn crate::ports::ResourceMonitor>) -> Self {
541        let mut ports = self.ports.unwrap_or_default();
542        ports.resources = monitor;
543        self.ports = Some(ports);
544        self
545    }
546
547    /// Register the internal URL router.
548    pub fn with_url_router(mut self, router: Arc<dyn crate::ports::InternalUrlRouter>) -> Self {
549        let mut ports = self.ports.unwrap_or_default();
550        ports.url_router = router;
551        self.ports = Some(ports);
552        self
553    }
554
555    /// Register the rule registry (TTSR).
556    pub fn with_rules(mut self, rules: Arc<dyn crate::ports::RuleRegistry>) -> Self {
557        let mut ports = self.ports.unwrap_or_default();
558        ports.rules = rules;
559        self.ports = Some(ports);
560        self
561    }
562
563    /// Register the embedding provider.
564    pub fn with_embeddings(mut self, embeddings: Arc<dyn crate::ports::EmbeddingProvider>) -> Self {
565        let mut ports = self.ports.unwrap_or_default();
566        ports.embeddings = embeddings;
567        self.ports = Some(ports);
568        self
569    }
570
571    /// Register the hook runner port.
572    ///
573    /// When set, [`crate::AgentBuilder::with_port_hooks`] composes a
574    /// [`HookMiddleware`](crate::middleware::HookMiddleware) backed by
575    /// this runner into the agent's hook pipeline. When unset, the port
576    /// stays at [`NoopHookRunner`](crate::ports::NoopHookRunner) and the
577    /// middleware short-circuits to a no-op.
578    pub fn with_hooks(mut self, runner: Arc<dyn crate::ports::HookRunner>) -> Self {
579        let mut ports = self.ports.unwrap_or_default();
580        ports.hooks = runner;
581        self.ports = Some(ports);
582        self
583    }
584
585    /// Create a supervisor builder for managing agent lifecycles.
586    ///
587    /// # Example
588    ///
589    /// ```ignore
590    /// use oxicode_sdk::OxicodeBuilder;
591    ///
592    /// let (oxicode, supervisor) = OxicodeBuilder::new()
593    ///     .with_builtins()
594    ///     .supervisor()
595    ///     .snapshot_dir("/data/snapshots")
596    ///     .build()?;
597    /// ```
598    pub fn supervisor(self) -> SupervisorBuilder {
599        SupervisorBuilder {
600            oxicode_builder: self,
601            policy: SupervisorPolicy::default(),
602            snapshot_dir: None,
603            agent_decorator: None,
604        }
605    }
606    /// Build the Oxicode engine. This consumes the builder.
607    pub fn build(self) -> Oxicode {
608        // Spawn the MCP manager unless explicitly disabled.
609        let mcp_manager = if self.mcp_enabled {
610            if self.mcp_cache_path.is_some() || self.mcp_consent_path.is_some() {
611                let cfg = match self.mcp_config {
612                    Some(cfg) => cfg,
613                    None => oxicode_agent::mcp::config::load_mcp_config(),
614                };
615                Some(oxicode_agent::mcp::McpManager::spawn_with_paths(
616                    cfg,
617                    self.mcp_cache_path,
618                    self.mcp_consent_path,
619                ))
620            } else {
621                Some(match self.mcp_config {
622                    Some(cfg) => oxicode_agent::mcp::McpManager::spawn_with_config(cfg),
623                    None => oxicode_agent::mcp::McpManager::spawn(),
624                })
625            }
626        } else {
627            None
628        };
629
630        Oxicode {
631            providers: Arc::new(self.providers),
632            models: Arc::new(self.models),
633            tools: Arc::new(self.tools),
634            include_builtins: self.include_builtins,
635            api_keys: Arc::new(self.api_keys),
636            base_urls: Arc::new(self.base_urls),
637            ports: self.ports.unwrap_or_default(),
638            mcp_manager,
639            routing: Arc::new(crate::routing::RoutingControl::new(
640                crate::routing::RoutingConfig::default(),
641            )),
642        }
643    }
644
645    // ── MCP configuration (Phase SDK) ───────────────────────────────
646
647    /// Inject a programmatic MCP configuration. This overrides the
648    /// on-disk `~/.config/oxicode/mcp.json` and `.mcp.json` discovery.
649    ///
650    /// # Example
651    ///
652    /// ```no_run
653    /// use oxicode_sdk::{OxicodeBuilder, McpConfig, ServerEntry, LifecycleMode};
654    ///
655    /// let mut mcp = McpConfig::default();
656    /// mcp.mcp_servers.insert(
657    ///     "my-server".into(),
658    ///     ServerEntry {
659    ///         command: Some("npx".into()),
660    ///         args: Some(vec!["-y".into(), "@my-org/mcp-server".into()]),
661    ///         lifecycle: Some(LifecycleMode::Lazy),
662    ///         ..Default::default()
663    ///     },
664    /// );
665    ///
666    /// let oxicode = OxicodeBuilder::new()
667    ///     .with_builtins()
668    ///     .with_mcp_config(mcp)
669    ///     .build();
670    /// ```
671    pub fn with_mcp_config(mut self, config: oxicode_agent::mcp::McpConfig) -> Self {
672        self.mcp_config = Some(config);
673        self.mcp_enabled = true;
674        self
675    }
676
677    /// Set custom disk paths for the MCP metadata cache and consent store.
678    ///
679    /// Only takes effect when MCP is enabled (see [`with_mcp`](Self::with_mcp)).
680    /// When unset, oxicode uses its default paths (`~/.config/oxicode/`). Intended
681    /// for SDK consumers that self-host MCP state under their own config
682    /// directory (e.g. oxios under `~/.oxios/`).
683    ///
684    /// Combine with [`with_mcp_config`](Self::with_mcp_config) to also inject
685    /// a programmatic config. If only paths are supplied (no config), oxicode
686    /// auto-discovers its config from the standard file locations and writes
687    /// cache/consent to the supplied paths.
688    pub fn with_mcp_paths(
689        mut self,
690        cache_path: std::path::PathBuf,
691        consent_path: std::path::PathBuf,
692    ) -> Self {
693        self.mcp_cache_path = Some(cache_path);
694        self.mcp_consent_path = Some(consent_path);
695        self
696    }
697
698    /// Enable or disable MCP. When disabled, no `McpManager` is spawned
699    /// and the `mcp` proxy tool / direct tools are not registered.
700    ///
701    /// Defaults to `true`.
702    pub fn with_mcp(mut self, enabled: bool) -> Self {
703        self.mcp_enabled = enabled;
704        self
705    }
706}
707
708impl Default for OxicodeBuilder {
709    fn default() -> Self {
710        Self::new()
711    }
712}
713
714// ── SupervisorBuilder ──────────────────────────────────────────────────────
715
716/// Builder for creating an `AgentSupervisor`.
717///
718/// Created via [`OxicodeBuilder::supervisor()`].
719pub struct SupervisorBuilder {
720    oxicode_builder: OxicodeBuilder,
721    policy: SupervisorPolicy,
722    snapshot_dir: Option<std::path::PathBuf>,
723    /// Cross-cutting decorator applied to every supervisor-spawned
724    /// agent. `None` (default) keeps the legacy fast path.
725    agent_decorator: Option<Arc<dyn crate::observability::AgentDecorator>>,
726}
727
728impl SupervisorBuilder {
729    /// Set the restart policy.
730    pub fn policy(mut self, policy: SupervisorPolicy) -> Self {
731        self.policy = policy;
732        self
733    }
734
735    /// Set the directory for persisting snapshots.
736    pub fn snapshot_dir(mut self, dir: impl Into<std::path::PathBuf>) -> Self {
737        self.snapshot_dir = Some(dir.into());
738        self
739    }
740
741    /// Attach an [`crate::observability::AgentDecorator`] that wraps every
742    /// supervisor-spawned agent.
743    ///
744    /// When set, [`SupervisorBuilder::build`] clones the built `Oxicode`
745    /// into the supervisor and configures it to route spawns through
746    /// `Oxicode::agent(config)` + `decorator.decorate(builder)` instead
747    /// of the bare `Agent::new(provider, config, tools)` fast path.
748    /// Use [`crate::observability::ObservabilityDecorator`] to bundle audit / authorizer /
749    /// tracer / cost-tracker — those hooks then actually run on
750    /// every spawned agent (no longer silent no-ops).
751    ///
752    /// Replaces the four deprecated no-op setters `with_audit`,
753    /// `with_authorizer`, `with_tracer`, `with_cost_tracker`, which
754    /// emitted `tracing::warn!` and dropped their arguments.
755    pub fn with_agent_decorator(
756        mut self,
757        decorator: Arc<dyn crate::observability::AgentDecorator>,
758    ) -> Self {
759        self.agent_decorator = Some(decorator);
760        self
761    }
762
763    /// Build the supervisor.
764    ///
765    /// Creates an `Oxicode` instance internally and constructs the
766    /// supervisor with a file-based snapshot store. When
767    /// [`with_agent_decorator`](Self::with_agent_decorator) was
768    /// called, the built `Oxicode` is cloned into the supervisor so
769    /// every spawn routes through the decorator.
770    pub fn build(self) -> anyhow::Result<(Oxicode, AgentSupervisor)> {
771        let oxicode = self.oxicode_builder.build();
772        let resolver: Arc<dyn oxicode_agent::ProviderResolver> = Arc::new(oxicode.clone());
773
774        let snapshot_store: Arc<dyn crate::lifecycle::SnapshotStore> = match &self.snapshot_dir {
775            Some(dir) => Arc::new(FileSnapshotStore::new(dir)?),
776            None => Arc::new(FileSnapshotStore::new(
777                std::env::temp_dir().join("oxicode-snapshots"),
778            )?),
779        };
780
781        let supervisor = AgentSupervisor::with_policy(resolver, snapshot_store, self.policy);
782        let supervisor = if let Some(decorator) = self.agent_decorator {
783            supervisor.with_agent_decorator(Arc::new(oxicode.clone()), decorator)
784        } else {
785            supervisor
786        };
787        Ok((oxicode, supervisor))
788    }
789}