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 tool in the shared tool registry.
291 pub fn tool(self, tool: impl oxicode_agent::AgentTool + 'static) -> Self {
292 self.tools.register(tool);
293 self
294 }
295
296 /// Register a provider factory — a closure that lazily creates a provider.
297 ///
298 /// Unlike [`Self::provider()`], which takes an already-constructed instance,
299 /// this stores a factory closure. The factory is invoked the **first time**
300 /// `Oxicode::create_provider(name)` is called, and the resulting provider is
301 /// cached for subsequent calls.
302 ///
303 /// This is useful when provider construction requires credential resolution
304 /// or network configuration that should happen at first use, not at build time.
305 ///
306 /// # Example
307 ///
308 /// ```no_run
309 /// use std::sync::Arc;
310 /// use oxicode_sdk::{OxicodeBuilder, OpenAiProvider};
311 ///
312 /// let oxicode = OxicodeBuilder::new()
313 /// .with_builtins()
314 /// .provider_factory("custom", || {
315 /// Ok(Arc::new(OpenAiProvider::with_base_url_and_key(
316 /// "https://api.example.com",
317 /// Some("key".into()),
318 /// )))
319 /// })
320 /// .build();
321 /// ```
322 pub fn provider_factory(
323 self,
324 name: &str,
325 factory: impl Fn() -> anyhow::Result<Arc<dyn Provider>> + Send + Sync + 'static,
326 ) -> Self {
327 self.providers.register_factory(name, factory);
328 self
329 }
330
331 /// Register an API key for a specific provider.
332 ///
333 /// When `create_provider(name)` is called, the key is injected into
334 /// the provider's constructor automatically. Keys registered here
335 /// take precedence over environment variables.
336 ///
337 /// # Example
338 ///
339 /// ```rust
340 /// use oxicode_sdk::OxicodeBuilder;
341 ///
342 /// let oxicode = OxicodeBuilder::new()
343 /// .with_builtins()
344 /// .api_key("anthropic", "sk-ant-test-key")
345 /// .api_key("openai", "sk-test-key")
346 /// .build();
347 /// ```
348 pub fn api_key(mut self, provider_name: &str, key: impl Into<String>) -> Self {
349 self.api_keys.insert(provider_name.to_string(), key.into());
350 self
351 }
352
353 /// Register a base URL override for a specific provider.
354 ///
355 /// Useful for OpenAI-compatible providers (ZAI, Groq, etc.)
356 /// that use a different endpoint.
357 ///
358 /// # Example
359 ///
360 /// ```rust
361 /// use oxicode_sdk::OxicodeBuilder;
362 ///
363 /// let oxicode = OxicodeBuilder::new()
364 /// .with_builtins()
365 /// .base_url("openai", "https://my-proxy.example.com/v1")
366 /// .build();
367 /// ```
368 pub fn base_url(mut self, provider_name: &str, url: impl Into<String>) -> Self {
369 self.base_urls.insert(provider_name.to_string(), url.into());
370 self
371 }
372
373 /// Register a full credential set for a provider.
374 ///
375 /// Convenience method combining [`api_key()`](Self::api_key) and
376 /// [`base_url()`](Self::base_url).
377 ///
378 /// # Example
379 ///
380 /// ```rust
381 /// use oxicode_sdk::OxicodeBuilder;
382 ///
383 /// let oxicode = OxicodeBuilder::new()
384 /// .with_builtins()
385 /// .credential("openai", "sk-test", Some("https://proxy.example.com/v1"))
386 /// .build();
387 /// ```
388 pub fn credential(
389 self,
390 provider_name: &str,
391 api_key: impl Into<String>,
392 base_url: Option<&str>,
393 ) -> Self {
394 let mut builder = self.api_key(provider_name, api_key);
395 if let Some(url) = base_url {
396 builder = builder.base_url(provider_name, url);
397 }
398 builder
399 }
400
401 /// Register a custom model.
402 pub fn model(self, model: Model) -> Self {
403 self.models.register(model);
404 self
405 }
406
407 // ─── Port registration ────────────────────────────────────────────────
408 //
409 // Products (oxicode-cli, oxios-kernel, custom apps) register concrete
410 // implementations of the port traits defined in `crate::ports`.
411 // All ports are optional: unset ports use a noop default.
412
413 /// Register a complete [`PortRegistry`] at once.
414 ///
415 /// Use this when you have a fully-built registry (e.g. loaded from a
416 /// directory of file-based adapters). For piecemeal registration, use
417 /// the `with_port_*` methods below.
418 pub fn with_ports(mut self, ports: PortRegistry) -> Self {
419 self.ports = Some(ports);
420 self
421 }
422
423 /// Register the model catalog port.
424 ///
425 /// The catalog is the source of truth for provider/model metadata.
426 /// If not called, the SDK uses [`NoopModelCatalog`](crate::ports::catalog::NoopModelCatalog)
427 /// (empty results — all lookups return `None`/`vec![]`).
428 ///
429 /// # Example
430 ///
431 /// ```no_run
432 /// use oxicode_sdk::{OxicodeBuilder, NoopModelCatalog};
433 ///
434 /// // `NoopModelCatalog` is the empty default used when no catalog is
435 /// // registered — pass any `Arc<dyn ModelCatalog>` here instead.
436 /// let catalog = NoopModelCatalog::new();
437 /// let oxicode = OxicodeBuilder::new()
438 /// .with_catalog(catalog)
439 /// .build();
440 /// ```
441 pub fn with_catalog(mut self, catalog: Arc<dyn crate::ports::catalog::ModelCatalog>) -> Self {
442 let mut ports = self.ports.unwrap_or_default();
443 ports.catalog = catalog;
444 self.ports = Some(ports);
445 self
446 }
447
448 /// Register the state store.
449 pub fn with_state(mut self, store: Arc<dyn crate::ports::StateStore>) -> Self {
450 let mut ports = self.ports.unwrap_or_default();
451 ports.state = store;
452 self.ports = Some(ports);
453 self
454 }
455
456 /// Register the config store.
457 pub fn with_config(mut self, store: Arc<dyn crate::ports::ConfigStore>) -> Self {
458 let mut ports = self.ports.unwrap_or_default();
459 ports.config = store;
460 self.ports = Some(ports);
461 self
462 }
463
464 /// Register the auth provider.
465 pub fn with_auth(mut self, auth: Arc<dyn crate::ports::AuthProvider>) -> Self {
466 let mut ports = self.ports.unwrap_or_default();
467 ports.auth = auth;
468 self.ports = Some(ports);
469 self
470 }
471
472 /// Register the event bus.
473 pub fn with_event_bus(mut self, bus: Arc<dyn crate::ports::EventBus>) -> Self {
474 let mut ports = self.ports.unwrap_or_default();
475 ports.event_bus = bus;
476 self.ports = Some(ports);
477 self
478 }
479
480 /// Register the skill loader.
481 pub fn with_skills(mut self, loader: Arc<dyn crate::ports::SkillLoader>) -> Self {
482 let mut ports = self.ports.unwrap_or_default();
483 ports.skills = loader;
484 self.ports = Some(ports);
485 self
486 }
487
488 /// Register the persona provider.
489 pub fn with_personas(mut self, provider: Arc<dyn crate::ports::PersonaProvider>) -> Self {
490 let mut ports = self.ports.unwrap_or_default();
491 ports.personas = provider;
492 self.ports = Some(ports);
493 self
494 }
495
496 /// Register the access gate.
497 pub fn with_access(mut self, gate: Arc<dyn crate::ports::AccessGate>) -> Self {
498 let mut ports = self.ports.unwrap_or_default();
499 ports.access = gate;
500 self.ports = Some(ports);
501 self
502 }
503
504 /// Register the capability resolver.
505 pub fn with_capabilities(
506 mut self,
507 resolver: Arc<dyn crate::ports::CapabilityResolver>,
508 ) -> Self {
509 let mut ports = self.ports.unwrap_or_default();
510 ports.capabilities = resolver;
511 self.ports = Some(ports);
512 self
513 }
514
515 /// Register the memory store.
516 pub fn with_memory(mut self, store: Arc<dyn crate::ports::MemoryStore>) -> Self {
517 let mut ports = self.ports.unwrap_or_default();
518 ports.memory = store;
519 self.ports = Some(ports);
520 self
521 }
522
523 /// Register the cron scheduler.
524 pub fn with_cron(mut self, scheduler: Arc<dyn crate::ports::CronScheduler>) -> Self {
525 let mut ports = self.ports.unwrap_or_default();
526 ports.cron = scheduler;
527 self.ports = Some(ports);
528 self
529 }
530
531 /// Register the resource monitor.
532 pub fn with_resources(mut self, monitor: Arc<dyn crate::ports::ResourceMonitor>) -> Self {
533 let mut ports = self.ports.unwrap_or_default();
534 ports.resources = monitor;
535 self.ports = Some(ports);
536 self
537 }
538
539 /// Register the internal URL router.
540 pub fn with_url_router(mut self, router: Arc<dyn crate::ports::InternalUrlRouter>) -> Self {
541 let mut ports = self.ports.unwrap_or_default();
542 ports.url_router = router;
543 self.ports = Some(ports);
544 self
545 }
546
547 /// Register the rule registry (TTSR).
548 pub fn with_rules(mut self, rules: Arc<dyn crate::ports::RuleRegistry>) -> Self {
549 let mut ports = self.ports.unwrap_or_default();
550 ports.rules = rules;
551 self.ports = Some(ports);
552 self
553 }
554
555 /// Register the embedding provider.
556 pub fn with_embeddings(mut self, embeddings: Arc<dyn crate::ports::EmbeddingProvider>) -> Self {
557 let mut ports = self.ports.unwrap_or_default();
558 ports.embeddings = embeddings;
559 self.ports = Some(ports);
560 self
561 }
562
563 /// Register the hook runner port.
564 ///
565 /// When set, [`crate::AgentBuilder::with_port_hooks`] composes a
566 /// [`HookMiddleware`](crate::middleware::HookMiddleware) backed by
567 /// this runner into the agent's hook pipeline. When unset, the port
568 /// stays at [`NoopHookRunner`](crate::ports::NoopHookRunner) and the
569 /// middleware short-circuits to a no-op.
570 pub fn with_hooks(mut self, runner: Arc<dyn crate::ports::HookRunner>) -> Self {
571 let mut ports = self.ports.unwrap_or_default();
572 ports.hooks = runner;
573 self.ports = Some(ports);
574 self
575 }
576
577 /// Create a supervisor builder for managing agent lifecycles.
578 ///
579 /// # Example
580 ///
581 /// ```ignore
582 /// use oxicode_sdk::OxicodeBuilder;
583 ///
584 /// let (oxicode, supervisor) = OxicodeBuilder::new()
585 /// .with_builtins()
586 /// .supervisor()
587 /// .snapshot_dir("/data/snapshots")
588 /// .build()?;
589 /// ```
590 pub fn supervisor(self) -> SupervisorBuilder {
591 SupervisorBuilder {
592 oxicode_builder: self,
593 policy: SupervisorPolicy::default(),
594 snapshot_dir: None,
595 agent_decorator: None,
596 }
597 }
598 /// Build the Oxicode engine. This consumes the builder.
599 pub fn build(self) -> Oxicode {
600 // Spawn the MCP manager unless explicitly disabled.
601 let mcp_manager = if self.mcp_enabled {
602 if self.mcp_cache_path.is_some() || self.mcp_consent_path.is_some() {
603 let cfg = match self.mcp_config {
604 Some(cfg) => cfg,
605 None => oxicode_agent::mcp::config::load_mcp_config(),
606 };
607 Some(oxicode_agent::mcp::McpManager::spawn_with_paths(
608 cfg,
609 self.mcp_cache_path,
610 self.mcp_consent_path,
611 ))
612 } else {
613 Some(match self.mcp_config {
614 Some(cfg) => oxicode_agent::mcp::McpManager::spawn_with_config(cfg),
615 None => oxicode_agent::mcp::McpManager::spawn(),
616 })
617 }
618 } else {
619 None
620 };
621
622 Oxicode {
623 providers: Arc::new(self.providers),
624 models: Arc::new(self.models),
625 tools: Arc::new(self.tools),
626 include_builtins: self.include_builtins,
627 api_keys: Arc::new(self.api_keys),
628 base_urls: Arc::new(self.base_urls),
629 ports: self.ports.unwrap_or_default(),
630 mcp_manager,
631 routing: Arc::new(crate::routing::RoutingControl::new(
632 crate::routing::RoutingConfig::default(),
633 )),
634 }
635 }
636
637 // ── MCP configuration (Phase SDK) ───────────────────────────────
638
639 /// Inject a programmatic MCP configuration. This overrides the
640 /// on-disk `~/.config/oxicode/mcp.json` and `.mcp.json` discovery.
641 ///
642 /// # Example
643 ///
644 /// ```no_run
645 /// use oxicode_sdk::{OxicodeBuilder, McpConfig, ServerEntry, LifecycleMode};
646 ///
647 /// let mut mcp = McpConfig::default();
648 /// mcp.mcp_servers.insert(
649 /// "my-server".into(),
650 /// ServerEntry {
651 /// command: Some("npx".into()),
652 /// args: Some(vec!["-y".into(), "@my-org/mcp-server".into()]),
653 /// lifecycle: Some(LifecycleMode::Lazy),
654 /// ..Default::default()
655 /// },
656 /// );
657 ///
658 /// let oxicode = OxicodeBuilder::new()
659 /// .with_builtins()
660 /// .with_mcp_config(mcp)
661 /// .build();
662 /// ```
663 pub fn with_mcp_config(mut self, config: oxicode_agent::mcp::McpConfig) -> Self {
664 self.mcp_config = Some(config);
665 self.mcp_enabled = true;
666 self
667 }
668
669 /// Set custom disk paths for the MCP metadata cache and consent store.
670 ///
671 /// Only takes effect when MCP is enabled (see [`with_mcp`](Self::with_mcp)).
672 /// When unset, oxicode uses its default paths (`~/.config/oxicode/`). Intended
673 /// for SDK consumers that self-host MCP state under their own config
674 /// directory (e.g. oxios under `~/.oxios/`).
675 ///
676 /// Combine with [`with_mcp_config`](Self::with_mcp_config) to also inject
677 /// a programmatic config. If only paths are supplied (no config), oxicode
678 /// auto-discovers its config from the standard file locations and writes
679 /// cache/consent to the supplied paths.
680 pub fn with_mcp_paths(
681 mut self,
682 cache_path: std::path::PathBuf,
683 consent_path: std::path::PathBuf,
684 ) -> Self {
685 self.mcp_cache_path = Some(cache_path);
686 self.mcp_consent_path = Some(consent_path);
687 self
688 }
689
690 /// Enable or disable MCP. When disabled, no `McpManager` is spawned
691 /// and the `mcp` proxy tool / direct tools are not registered.
692 ///
693 /// Defaults to `true`.
694 pub fn with_mcp(mut self, enabled: bool) -> Self {
695 self.mcp_enabled = enabled;
696 self
697 }
698}
699
700impl Default for OxicodeBuilder {
701 fn default() -> Self {
702 Self::new()
703 }
704}
705
706// ── SupervisorBuilder ──────────────────────────────────────────────────────
707
708/// Builder for creating an `AgentSupervisor`.
709///
710/// Created via [`OxicodeBuilder::supervisor()`].
711pub struct SupervisorBuilder {
712 oxicode_builder: OxicodeBuilder,
713 policy: SupervisorPolicy,
714 snapshot_dir: Option<std::path::PathBuf>,
715 /// Cross-cutting decorator applied to every supervisor-spawned
716 /// agent. `None` (default) keeps the legacy fast path.
717 agent_decorator: Option<Arc<dyn crate::observability::AgentDecorator>>,
718}
719
720impl SupervisorBuilder {
721 /// Set the restart policy.
722 pub fn policy(mut self, policy: SupervisorPolicy) -> Self {
723 self.policy = policy;
724 self
725 }
726
727 /// Set the directory for persisting snapshots.
728 pub fn snapshot_dir(mut self, dir: impl Into<std::path::PathBuf>) -> Self {
729 self.snapshot_dir = Some(dir.into());
730 self
731 }
732
733 /// Attach an [`crate::observability::AgentDecorator`] that wraps every
734 /// supervisor-spawned agent.
735 ///
736 /// When set, [`SupervisorBuilder::build`] clones the built `Oxicode`
737 /// into the supervisor and configures it to route spawns through
738 /// `Oxicode::agent(config)` + `decorator.decorate(builder)` instead
739 /// of the bare `Agent::new(provider, config, tools)` fast path.
740 /// Use [`crate::observability::ObservabilityDecorator`] to bundle audit / authorizer /
741 /// tracer / cost-tracker — those hooks then actually run on
742 /// every spawned agent (no longer silent no-ops).
743 ///
744 /// Replaces the four deprecated no-op setters `with_audit`,
745 /// `with_authorizer`, `with_tracer`, `with_cost_tracker`, which
746 /// emitted `tracing::warn!` and dropped their arguments.
747 pub fn with_agent_decorator(
748 mut self,
749 decorator: Arc<dyn crate::observability::AgentDecorator>,
750 ) -> Self {
751 self.agent_decorator = Some(decorator);
752 self
753 }
754
755 /// Build the supervisor.
756 ///
757 /// Creates an `Oxicode` instance internally and constructs the
758 /// supervisor with a file-based snapshot store. When
759 /// [`with_agent_decorator`](Self::with_agent_decorator) was
760 /// called, the built `Oxicode` is cloned into the supervisor so
761 /// every spawn routes through the decorator.
762 pub fn build(self) -> anyhow::Result<(Oxicode, AgentSupervisor)> {
763 let oxicode = self.oxicode_builder.build();
764 let resolver: Arc<dyn oxicode_agent::ProviderResolver> = Arc::new(oxicode.clone());
765
766 let snapshot_store: Arc<dyn crate::lifecycle::SnapshotStore> = match &self.snapshot_dir {
767 Some(dir) => Arc::new(FileSnapshotStore::new(dir)?),
768 None => Arc::new(FileSnapshotStore::new(
769 std::env::temp_dir().join("oxicode-snapshots"),
770 )?),
771 };
772
773 let supervisor = AgentSupervisor::with_policy(resolver, snapshot_store, self.policy);
774 let supervisor = if let Some(decorator) = self.agent_decorator {
775 supervisor.with_agent_decorator(Arc::new(oxicode.clone()), decorator)
776 } else {
777 supervisor
778 };
779 Ok((oxicode, supervisor))
780 }
781}