1#![warn(missing_docs)]
14#![cfg_attr(
17 not(test),
18 deny(clippy::expect_used, clippy::panic, clippy::unwrap_used)
19)]
20#![cfg_attr(
21 test,
22 allow(
23 clippy::unwrap_used,
24 clippy::field_reassign_with_default,
25 clippy::expect_used,
26 clippy::panic,
27 )
28)]
29use oxicode_api_stability::{internal as oxicode_internal, stable as oxicode_stable};
33#[allow(unused_imports)]
36use oxicode_api_stability::unstable as oxicode_unstable;
37
38pub mod agent_builder;
39pub mod agent_definition;
40pub mod agent_group;
41pub mod bridge;
42pub mod builder;
43pub mod closure_tool;
44pub mod coordination;
45pub mod delegation;
46pub mod error;
47pub mod event_bus;
48pub mod kernel_bridge;
49pub mod lifecycle;
50pub mod message_bus;
51pub mod metrics;
52pub mod middleware;
53pub mod observability;
54pub mod ports;
55
56#[oxicode_internal]
58pub use ports::{fs, inmem};
59pub mod port_memory_backend;
60pub mod prelude;
62pub mod routing;
63pub mod security;
64pub mod snapcompact_compactor;
65pub mod tool_factory;
66pub mod url_resolver;
67pub mod workflow_dsl;
68pub mod workflow_engine;
69
70#[oxicode_stable(since = "0.63.0")]
72pub use agent_builder::AgentBuilder;
73#[oxicode_stable(since = "0.63.0")]
74pub use agent_group::{AgentGroup, AgentGroupOutput, GroupResult, GroupStrategy};
75#[oxicode_stable(since = "0.63.0")]
76pub use builder::{Oxicode, OxicodeBuilder};
77#[cfg(feature = "delegation")]
78#[oxicode_unstable(feature = "delegation")]
79pub use delegation::SdkSubagentRunner;
80#[cfg(feature = "url-resolver")]
81#[oxicode_unstable(feature = "url-resolver")]
82pub use url_resolver::SdkUrlResolver;
83#[cfg(feature = "workflow-dsl")]
84#[oxicode_unstable(feature = "workflow-dsl")]
85pub use workflow_engine::{StepOutput, WorkflowEngine, WorkflowResult};
86
87#[oxicode_stable(since = "0.63.0")]
93pub use closure_tool::ClosureTool;
94#[oxicode_internal]
95pub use kernel_bridge::{KernelToolContext, KernelToolProvider};
96#[oxicode_stable(since = "0.63.0")]
97pub use message_bus::{InterAgentMessage, LagAwareReceiver, MessageBus, PublishResult};
98#[oxicode_stable(since = "0.63.0")]
99pub use metrics::{AgentMetrics, MetricsSnapshot};
100#[oxicode_stable(since = "0.63.0")]
101pub use ports::AccessGate as AccessGatePort;
102#[oxicode_stable(since = "0.63.0")]
103pub use ports::EventBus as EventBusPort;
104#[oxicode_stable(since = "0.63.0")]
105pub use ports::MemoryEntry as MemoryEntryPort;
106#[oxicode_stable(since = "0.63.0")]
107pub use ports::{
108 AccessDecision, AuthMethod, AuthProvider, CapabilityResolver, ConfigStore, CronJob,
109 CronScheduler, EventPayload, EventTopic, InMemoryEventBus, MemoryStore, NoopAuthProvider,
110 NoopConfigStore, NoopCronScheduler, NoopEventBus, NoopMemoryStore, NoopPersonaProvider,
111 NoopResourceMonitor, NoopSkillLoader, NoopStateStore, OAuthToken, Persona, PersonaProvider,
112 PortId, PortRegistry, PortValue, ResourceMonitor, ResourceUsage, Skill, SkillLoader, SkillMeta,
113 StateStore, SubscriptionHandle, ToolCallRequest,
114};
115
116#[oxicode_stable(since = "0.66.0")]
118pub use ports::hooks::{HookContext, HookEvent, HookOutcome, HookRunner, HookSpec, NoopHookRunner};
119
120#[oxicode_stable(since = "0.63.0")]
122pub use ports::catalog::{
123 CatalogEvent, CatalogModelEntry, CatalogProtocol, CatalogProviderEntry, CatalogSource,
124 ModelCatalog, NoopModelCatalog, RefreshOutcome,
125};
126#[oxicode_internal]
128pub use ports::fs::catalog::{CatalogConfig, FileModelCatalog};
129
130#[oxicode_stable(since = "0.63.0")]
132pub use event_bus::EventBus;
133
134#[oxicode_stable(since = "0.63.0")]
136pub use error::{SdkError, SdkResult};
137#[oxicode_stable(since = "0.63.0")]
138pub use lifecycle::{
139 AgentHandle, AgentLifecycleEvent, AgentPool, AgentSnapshot, AgentStatus, AgentSupervisor,
140 FileSnapshotStore, HubKind, HubStatus, RestartBackoff, SnapshotStore, SupervisorPolicy,
141 ToolManifest,
142};
143#[oxicode_stable(since = "0.63.0")]
144pub use middleware::Middleware;
145#[oxicode_stable(since = "0.63.0")]
146pub use middleware::{
147 MiddlewareContext, MiddlewareData, MiddlewarePhase, MiddlewarePipeline, MiddlewareResult,
148 build_hooks,
149};
150#[oxicode_stable(since = "0.63.0")]
151pub use observability::{
152 AuditAction, AuditEntry, AuditError, AuditFilter, AuditLog, AuditPersistence, AuditTrail,
153 CostBreakdown, CostSnapshot, CostTracker, CostTrackerConfig, EventQuery, EventStore,
154 EventStoreConfig, GlobalCostSnapshot, HashDigest, Span, SpanContext, SpanGuard, SpanId,
155 SpanKind, SpanStatus, StoredEvent, TokenUsage, TraceId, Tracer, TrailEntry,
156};
157
158#[oxicode_stable(since = "0.63.0")]
159pub use security::{
161 AccessDenied, AccessGate, Action, AgentContext, AgentPermissions, AllowlistMode,
162 ApprovalStatus, AuditEvent, AuditSink, Authorizer, Capability, CapabilitySet,
163 CapabilitySubject, CheckRequest, DefaultPolicy, DenyLayer, ExecPolicy, PathMode,
164 PendingApproval, PermAuditEntry, PermissionUpdate, RbacAuditEntry, RbacManager, RbacPolicy,
165 Role, SecurityMiddleware, StringPattern, Subject, TracingAuditSink, TrailAuditSink,
166};
167
168#[oxicode_stable(since = "0.63.0")]
169pub use coordination::{
171 Consensus, CoordinatedGroup, CoordinatedGroupBuilder, MemoryEntry, MemoryEvent, MemoryKey,
172 SharedMemory, VoteResult, WorkEvent, WorkItem, WorkQueue, WorkQueueConfig, WorkQueueStats,
173 WorkResult, WorkStatus,
174};
175
176#[oxicode_stable(since = "0.63.0")]
177pub use routing::RoutingControl;
179
180#[oxicode_stable(since = "0.63.0")]
181pub use oxicode_ai::{
183 Api, CompactionStrategy, ContentBlock, Context, Cost, InputModality, Message, MessageContent,
184 Model, ModelRegistry, Provider, ProviderError, ProviderEvent, ProviderOptions,
185 ProviderRegistry, StreamOptions, UserMessage,
186};
187#[cfg(feature = "role-routing")]
189#[oxicode_unstable(feature = "role-routing")]
190pub use oxicode_ai::role_routing::RoleRoutingProvider;
191#[cfg(feature = "role-switching")]
192#[oxicode_unstable(feature = "role-switching")]
193pub use oxicode_ai::role_switcher::{
194 RoleSignals, decide_role, resolve_role_to_model, role_for_tool,
195};
196#[cfg(feature = "role-routing")]
197#[oxicode_unstable(feature = "role-routing")]
198pub use oxicode_ai::roles::{ModelRole, RoleRegistry, live_role_registry, set_live_role_registry};
199
200#[oxicode_stable(since = "0.63.0")]
201pub use oxicode_ai::env_api_keys::{find_env_keys, get_all_env_keys, get_env_api_key, has_env_key};
203
204#[oxicode_internal]
205pub use oxicode_ai::model_db::{
207 ModelEntry, builtin_model_count_sentinel, get_all_models, get_cheapest_models, get_model_entry,
208 get_provider_models, get_providers, get_reasoning_models, get_vision_models, model_count,
209 search_models,
210};
211
212#[oxicode_stable(since = "0.63.0")]
216pub use oxicode_ai::catalog::{
218 BuiltinModelEntry, BuiltinProviderEntry, OverrideFile, apply_model_overrides,
219 apply_provider_overrides, builtin_model_count, builtin_providers_count, discover_all,
220 discover_all_authenticated, discover_all_local, discover_models, find_override_files,
221 load_builtin_providers, load_overrides,
222};
223#[oxicode_stable(since = "0.63.0")]
224pub use oxicode_ai::oauth::{
225 AuthStore, OAuthError, TokenBundle, default_auth_path, load_auth_store, load_token,
226 remove_token, save_auth_store, save_token,
227};
228
229#[oxicode_stable(since = "0.63.0")]
231pub use oxicode_ai::register_builtins::{
233 BuiltinProvider, get_all_provider_aliases, get_all_provider_names, get_api_mappings,
234 get_builtin_provider, get_builtin_providers, get_provider_api, get_provider_base_url,
235 get_provider_env_key, get_provider_env_keys, is_builtin_provider, resolve_provider_name,
236};
237
238#[oxicode_stable(since = "0.63.0")]
240pub use oxicode_ai::{
241 create_builtin_provider, create_builtin_provider_with_options, custom_provider_names,
242 dynamic_models, fetch_models_async, fetch_models_blocking, get_model, get_models, get_provider,
243 get_provider_arc, lookup_model, register_model, register_provider, unregister_provider,
244};
245
246#[cfg(feature = "router")]
248#[oxicode_unstable(feature = "router")]
249pub use oxicode_ai::router;
250
251#[oxicode_stable(since = "0.63.0")]
253pub use oxicode_ai::{
254 ProgressCallback, Tool, ToolCall, ToolCallType, ToolResult, ToolValidationError, validate_args,
255};
256
257#[oxicode_stable(since = "0.63.0")]
260pub use oxicode_ai::types::ThinkingLevel;
261
262#[oxicode_stable(since = "0.63.0")]
264pub use oxicode_agent::{
265 Agent, AgentConfig, AgentError, AgentEvent, AgentHooks, AgentLoop, AgentLoopConfig, AgentState,
266 AgentTool, AgentToolResult, BrowseProgress, BrowseProgressCallback, CompactedContext,
267 CompactionEvent, CompactionHook, EditTool, FindTool, GetSearchResultsTool, GrepTool, LsTool,
268 Mode, OutputMode, ProviderResolver, ReadTool, SearchCache, SharedState, StreamDelta,
269 StructuredOutput, StructuredOutputError, ToolCallContext, ToolContext, ToolError,
270 ToolExecutionMode, ToolRegistry, VisitReason, WebSearchTool, WriteTool,
271};
272#[cfg(feature = "advisor")]
278#[oxicode_unstable(feature = "advisor")]
279pub use oxicode_agent::advisor::{
280 ADVISOR_GUIDANCE, ADVISOR_READONLY_TOOL_NAMES, ADVISOR_SYSTEM_PROMPT, AdviseTool, AdvisorAgent,
281 AdvisorDeliveryChannel, AdvisorEmissionGuard, AdvisorNote, AdvisorRuntime, AdvisorRuntimeHost,
282 AdvisorSeverity, AgentAdvisor, DeliveryOpts, EnqueueAdviceFn, format_advisory_batch,
283 is_immune_turn_active, is_interrupting_severity, normalize_advisor_note,
284 resolve_delivery_channel,
285};
286#[oxicode_stable(since = "0.63.0")]
288pub use oxicode_agent::tools::todo::{TodoItem, TodoOp, TodoPhase, TodoStatus, TodoUpdateResult};
289#[oxicode_stable(since = "0.63.0")]
290pub use oxicode_agent::{TodoStateProvider, TodoTool};
291
292#[cfg(feature = "memory")]
303#[oxicode_unstable(feature = "memory")]
304pub use crate::port_memory_backend::PortMemoryBackend;
305#[oxicode_stable(since = "0.63.0")]
308pub use oxicode_agent::BashTool;
309#[cfg(feature = "subagent")]
311#[oxicode_unstable(feature = "subagent")]
312pub use oxicode_agent::SubagentRunner;
313#[cfg(feature = "memory")]
316#[oxicode_unstable(feature = "memory")]
317pub use oxicode_agent::tools::{MemoryBackend, MemoryItem};
318#[cfg(feature = "memory")]
320#[oxicode_unstable(feature = "memory")]
321pub use oxicode_agent::tools::{
322 MemoryEditTool, MemoryRecallTool, MemoryReflectTool, MemoryRetainTool, SubagentTool,
323};
324#[oxicode_stable(since = "0.63.0")]
326pub use oxicode_agent::tools::{ResolvedContent, UrlResolver};
327#[cfg(feature = "agent-hub")]
329#[oxicode_unstable(feature = "agent-hub")]
330pub use oxicode_agent::{AgentHubStatus, AgentInfo, AgentKind, AgentPoolProvider};
331#[cfg(feature = "lsp")]
333#[oxicode_unstable(feature = "lsp")]
334pub use oxicode_agent::{LspAction, LspProvider};
335
336#[oxicode_internal]
339pub use oxicode_hashline;
340
341#[oxicode_stable(since = "0.63.0")]
348pub use oxicode_ai::OpenAiProvider;
349#[oxicode_stable(since = "0.63.0")]
350pub use oxicode_ai::OpenAiResponsesProvider;
351
352#[oxicode_stable(since = "0.63.0")]
360pub use oxicode_agent::mcp::{
361 ConsentManager, ConsentState, DirectToolDef, DirectToolsConfig, LifecycleMode, McpCallResult,
362 McpConfig, McpConnectionStatus, McpContent, McpDashboardData, McpDirectTool, McpManager,
363 McpSamplingRequest, McpServerInfo, McpSettings, McpSettingsView, McpTool, McpToolDef,
364 McpToolInfo, MetadataCache, ServerEntry, ToolMetadata, ToolPrefix,
365};
366
367#[cfg(feature = "mcp-transport")]
371#[oxicode_unstable(feature = "mcp-transport")]
372pub use oxicode_agent::mcp::transport::{
373 McpTransport, http::StreamableHttpTransport, stdio::StdioTransport,
374};
375
376#[cfg(feature = "mcp-spawn-validator")]
380#[oxicode_unstable(feature = "mcp-spawn-validator")]
381pub use oxicode_agent::mcp::{NoopSpawnValidator, SpawnValidator};
382
383#[cfg(feature = "circuit-breaker")]
387#[oxicode_unstable(feature = "circuit-breaker")]
388pub use oxicode_ai::circuit_breaker::{
389 BreakerError, BreakerState, CircuitBreaker, DefaultCircuitBreaker, SharedBreaker,
390};
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395 use std::path::Path;
396
397 #[test]
398 fn catalog_reachable_via_sdk() {
399 let providers = load_builtin_providers();
403 assert!(
404 providers.len() >= 70,
405 "expected >= 70 providers, got {}",
406 providers.len()
407 );
408 let all = get_all_models().collect::<Vec<_>>();
410 assert!(
411 !all.is_empty(),
412 "models should be loaded via get_all_models()"
413 );
414 assert!(
415 all.len() > 5000,
416 "expected >5000 materialized models, got {}",
417 all.len()
418 );
419 }
420
421 #[test]
422 fn sentinel_count_via_sdk() {
423 let n = builtin_model_count_sentinel();
424 assert_eq!(
427 n, 0,
428 "expected 0 sentinel entries with materialize, got {n}"
429 );
430 }
431
432 fn test_model(id: &str, provider: &str) -> Model {
434 Model::new(
435 id,
436 id,
437 Api::AnthropicMessages,
438 provider,
439 "https://api.example.com",
440 )
441 }
442
443 #[test]
444 fn test_oxicode_builder_new() {
445 let oxicode = OxicodeBuilder::new().build();
446 assert!(
448 oxicode
449 .resolve_model("anthropic/claude-sonnet-4-20250514")
450 .is_err()
451 );
452 }
453
454 #[test]
455 fn test_oxicode_builder_with_builtins() {
456 let oxicode = OxicodeBuilder::new().with_builtins().build();
457 assert!(
459 oxicode
460 .resolve_model("anthropic/claude-sonnet-4-20250514")
461 .is_ok()
462 );
463 assert!(oxicode.resolve_model("openai/gpt-4o").is_ok());
464 }
465
466 #[test]
467 fn test_oxicode_builder_custom_model() {
468 let oxicode = OxicodeBuilder::new()
469 .model(test_model("test-model", "test-provider"))
470 .build();
471 assert!(oxicode.resolve_model("test-provider/test-model").is_ok());
472 }
473
474 #[test]
475 fn test_oxicode_provider_resolution() {
476 let oxicode = OxicodeBuilder::new().with_builtins().build();
477 assert!(oxicode.create_provider("anthropic").is_ok());
479 assert!(oxicode.create_provider("nonexistent").is_err());
481 }
482
483 #[test]
484 fn test_agent_builder_workspace() {
485 let oxicode = OxicodeBuilder::new().with_builtins().build();
486 let config = AgentConfig {
487 model_id: "anthropic/claude-sonnet-4-20250514".into(),
488 timeout_seconds: 30,
489 ..Default::default()
490 };
491 let result = oxicode
493 .agent(config)
494 .workspace("/tmp/test-workspace")
495 .build();
496 assert!(result.is_ok() || result.is_err());
497 }
498
499 #[test]
500 fn test_agent_builder_coding_tools() {
501 let oxicode = OxicodeBuilder::new().with_builtins().build();
502 let config = AgentConfig {
503 model_id: "anthropic/claude-sonnet-4-20250514".into(),
504 timeout_seconds: 30,
505 ..Default::default()
506 };
507 let result = oxicode
508 .agent(config)
509 .workspace("/tmp")
510 .coding_tools()
511 .build();
512 if let Ok(agent) = result {
513 let tool_names = agent.tools().names();
514 assert!(tool_names.contains(&"read".to_string()));
515 assert!(tool_names.contains(&"write".to_string()));
516 assert!(tool_names.contains(&"edit".to_string()));
517 assert!(tool_names.contains(&"ls".to_string()));
518 }
519 }
520
521 #[test]
522 fn test_agent_builder_readonly_tools() {
523 let oxicode = OxicodeBuilder::new().with_builtins().build();
524 let config = AgentConfig {
525 model_id: "anthropic/claude-sonnet-4-20250514".into(),
526 timeout_seconds: 30,
527 ..Default::default()
528 };
529 let result = oxicode
530 .agent(config)
531 .workspace("/tmp")
532 .readonly_tools()
533 .build();
534 if let Ok(agent) = result {
535 let tool_names = agent.tools().names();
536 assert!(tool_names.contains(&"read".to_string()));
537 assert!(tool_names.contains(&"ls".to_string()));
538 assert!(!tool_names.contains(&"write".to_string()));
540 }
541 }
542
543 #[test]
544 fn test_model_registry_isolation() {
545 let oxicode1 = OxicodeBuilder::new()
547 .model(test_model("unique-1", "test"))
548 .build();
549
550 let oxicode2 = OxicodeBuilder::new().with_builtins().build();
551
552 assert!(oxicode2.resolve_model("test/unique-1").is_err());
554 assert!(oxicode1.resolve_model("test/unique-1").is_ok());
556 }
557
558 #[test]
559 fn test_tool_factory_coding_tools() {
560 let tools = crate::tool_factory::coding_tools(Path::new("/tmp"));
561 let names = tools.names();
562 assert!(names.contains(&"read".to_string()));
563 assert!(names.contains(&"write".to_string()));
564 assert!(names.contains(&"edit".to_string()));
565 assert!(names.contains(&"ls".to_string()));
566 assert_eq!(names.len(), 4);
567 }
568
569 #[test]
570 fn test_tool_factory_readonly_tools() {
571 let tools = crate::tool_factory::readonly_tools(Path::new("/tmp"));
572 let names = tools.names();
573 assert!(names.contains(&"read".to_string()));
574 assert!(names.contains(&"ls".to_string()));
575 assert_eq!(names.len(), 2);
576 }
577
578 #[test]
579 fn test_tool_registry_extend_from() {
580 let base = crate::tool_factory::coding_tools(Path::new("/tmp"));
581 let extra = crate::tool_factory::readonly_tools(Path::new("/tmp"));
582
583 let combined = ToolRegistry::new();
585 combined.extend_from(&base);
586 combined.extend_from(&extra);
587
588 let names = combined.names();
589 assert!(names.contains(&"read".to_string()));
590 assert!(names.contains(&"write".to_string()));
591 assert!(names.contains(&"ls".to_string()));
592 }
594
595 #[test]
598 fn test_provider_resolver_trait_on_oxicode() {
599 let oxicode = OxicodeBuilder::new().with_builtins().build();
600 let resolver: &dyn ProviderResolver = &oxicode;
602 assert!(resolver.resolve_provider("anthropic").is_some());
603 assert!(resolver.resolve_provider("nonexistent").is_none());
604 assert!(
605 resolver
606 .resolve_model("anthropic/claude-sonnet-4-20250514")
607 .is_some()
608 );
609 assert!(resolver.resolve_model("nonexistent/model").is_none());
610 }
611
612 #[test]
613 fn test_agent_uses_resolver_for_switch_model() {
614 let oxicode = OxicodeBuilder::new()
616 .model(test_model("test-model", "test-provider"))
617 .build();
618
619 let config = AgentConfig {
621 model_id: "test-provider/test-model".into(),
622 timeout_seconds: 5,
623 ..Default::default()
624 };
625 let result = oxicode.agent(config).build();
626 assert!(result.is_err());
629 }
630
631 #[test]
632 fn test_oxicode_builder_without_builtins() {
633 let oxicode = OxicodeBuilder::new().build();
634 assert!(
636 oxicode
637 .resolve_model("anthropic/claude-sonnet-4-20250514")
638 .is_err()
639 );
640 assert!(oxicode.create_provider("anthropic").is_err());
641 assert!(!oxicode.has_builtins());
642 }
643
644 #[test]
645 fn test_oxicode_builder_with_builtins_creates_providers() {
646 let oxicode = OxicodeBuilder::new().with_builtins().build();
647 assert!(oxicode.has_builtins());
648 assert!(oxicode.create_provider("anthropic").is_ok());
650 assert!(oxicode.create_provider("openai").is_ok());
651 assert!(oxicode.create_provider("deepseek").is_ok());
652 assert!(oxicode.create_provider("unknown-provider").is_err());
654 }
655
656 #[test]
665 fn test_create_provider_consults_auth_port() {
666 use parking_lot::Mutex;
667 use std::pin::Pin;
668
669 struct RecordingAuth {
673 calls: Mutex<Vec<String>>,
674 }
675 impl AuthProvider for RecordingAuth {
676 fn get_api_key(
677 &self,
678 _provider: &str,
679 ) -> Pin<Box<dyn Future<Output = Result<Option<String>, SdkError>> + Send + '_>>
680 {
681 Box::pin(async { Ok(None) })
682 }
683 fn get_api_key_sync(&self, provider: &str) -> Result<Option<String>, SdkError> {
684 self.calls.lock().push(provider.to_string());
685 Ok(None)
686 }
687 fn set_api_key(
688 &self,
689 _: &str,
690 _: &str,
691 ) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>> {
692 Box::pin(async { Ok(()) })
693 }
694 fn delete_api_key(
695 &self,
696 _: &str,
697 ) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>> {
698 Box::pin(async { Ok(()) })
699 }
700 fn get_oauth(
701 &self,
702 _: &str,
703 ) -> Pin<
704 Box<
705 dyn Future<Output = Result<Option<crate::ports::OAuthToken>, SdkError>>
706 + Send
707 + '_,
708 >,
709 > {
710 Box::pin(async { Ok(None) })
711 }
712 fn set_oauth(
713 &self,
714 _: &str,
715 _: crate::ports::OAuthToken,
716 ) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>> {
717 Box::pin(async { Ok(()) })
718 }
719 fn list_providers(
720 &self,
721 ) -> Pin<Box<dyn Future<Output = Result<Vec<String>, SdkError>> + Send + '_>>
722 {
723 Box::pin(async { Ok(Vec::new()) })
724 }
725 }
726
727 let auth = std::sync::Arc::new(RecordingAuth {
728 calls: Mutex::new(Vec::new()),
729 });
730 let oxicode = OxicodeBuilder::new()
731 .with_builtins()
732 .with_auth(auth.clone())
733 .build();
734
735 let _ = oxicode.create_provider("anthropic");
737 let calls = auth.calls.lock();
738 assert!(
739 calls.iter().any(|c| c == "anthropic"),
740 "Oxicode::create_provider must call AuthProvider::get_api_key_sync \
741 when the static api_keys map has no entry. Got calls: {:?}",
742 *calls
743 );
744 }
745
746 #[test]
747 fn test_closure_tool_sync() {
748 let tool = crate::closure_tool::ClosureTool::new_sync(
749 "test_tool",
750 "A test tool",
751 serde_json::json!({
752 "type": "object",
753 "properties": {
754 "input": { "type": "string" }
755 }
756 }),
757 |params, _ctx| {
758 let input = params["input"].as_str().unwrap_or("default");
759 Ok(AgentToolResult::success(format!("processed: {}", input)))
760 },
761 );
762
763 assert_eq!(tool.name(), "test_tool");
764 assert_eq!(tool.description(), "A test tool");
765
766 let rt = tokio::runtime::Runtime::new().unwrap();
767 let result = rt
768 .block_on(tool.execute(
769 "call_1",
770 serde_json::json!({"input": "hello"}),
771 None,
772 &ToolContext::default(),
773 ))
774 .unwrap();
775 assert!(result.success);
776 assert!(result.output.contains("processed: hello"));
777 }
778
779 #[test]
780 fn test_custom_tool_in_agent_builder() {
781 let oxicode = OxicodeBuilder::new().with_builtins().build();
782 let config = AgentConfig {
783 model_id: "anthropic/claude-sonnet-4-20250514".into(),
784 timeout_seconds: 30,
785 ..Default::default()
786 };
787 let result = oxicode
788 .agent(config)
789 .workspace("/tmp")
790 .custom_tool(
791 "my_tool",
792 "My custom tool",
793 serde_json::json!({"type": "object", "properties": {"query": {"type": "string"}}}),
794 |params, _ctx| {
795 Ok(AgentToolResult::success(format!(
796 "result: {}",
797 params["query"]
798 )))
799 },
800 )
801 .build();
802
803 if let Ok(agent) = result {
804 let tool_names = agent.tools().names();
805 assert!(tool_names.contains(&"my_tool".to_string()));
806 }
807 }
808
809 #[test]
810 fn test_full_isolation_between_instances() {
811 let oxicode1 = OxicodeBuilder::new()
813 .model(test_model("unique-alpha", "p1"))
814 .build();
815
816 let oxicode2 = OxicodeBuilder::new().with_builtins().build();
818
819 assert!(oxicode2.resolve_model("p1/unique-alpha").is_err());
821 assert!(
822 oxicode1
823 .resolve_model("anthropic/claude-sonnet-4-20250514")
824 .is_err()
825 );
826
827 assert!(oxicode1.create_provider("anthropic").is_err());
829 assert!(oxicode2.create_provider("anthropic").is_ok());
831 }
832
833 #[test]
834 fn test_agent_builder_system_prompt() {
835 let oxicode = OxicodeBuilder::new().with_builtins().build();
836 let config = AgentConfig {
837 model_id: "anthropic/claude-sonnet-4-20250514".into(),
838 timeout_seconds: 5,
839 ..Default::default()
840 };
841 let agent = oxicode
842 .agent(config)
843 .workspace("/tmp")
844 .system_prompt("You are a test agent.")
845 .build()
846 .unwrap();
847 drop(agent);
849 }
850
851 #[test]
852 fn test_oxicode_builder_api_key() {
853 let oxicode = OxicodeBuilder::new()
855 .with_builtins()
856 .api_key("anthropic", "sk-ant-test-key")
857 .build();
858 assert!(oxicode.has_builtins());
861 }
862
863 #[test]
864 fn test_oxicode_builder_base_url() {
865 let oxicode = OxicodeBuilder::new()
866 .with_builtins()
867 .base_url("openai", "https://my-proxy.example.com/v1")
868 .build();
869 assert!(oxicode.has_builtins());
870 }
871
872 #[test]
873 fn test_oxicode_builder_credential() {
874 let oxicode = OxicodeBuilder::new()
875 .with_builtins()
876 .credential(
877 "openai",
878 "sk-test-key",
879 Some("https://proxy.example.com/v1"),
880 )
881 .credential("anthropic", "sk-ant-test", None)
882 .build();
883 assert!(oxicode.has_builtins());
884 }
885}