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}