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, find_entry_by_model_id, get_all_models,
208 get_cheapest_models, get_model_entry, get_provider_models, get_providers, get_reasoning_models,
209 get_vision_models, model_count, 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
292pub use oxicode_agent::issues::liveness;
294#[oxicode_stable(since = "0.77.0")]
295pub use oxicode_agent::issues::{
296 Assignment, FileIssueStore, GithubRef, Issue, IssueError, IssueFilter, IssueMeta, IssuePatch,
297 IssueSummary, Priority, Status,
298};
299pub use oxicode_agent::issues::{content_hash, issues_dir};
300#[oxicode_stable(since = "0.77.0")]
301pub use oxicode_agent::tools::issue::{IssueTool, cas_retry, format_issue_full, format_issue_line};
302
303#[cfg(feature = "memory")]
314#[oxicode_unstable(feature = "memory")]
315pub use crate::port_memory_backend::PortMemoryBackend;
316#[oxicode_stable(since = "0.63.0")]
319pub use oxicode_agent::BashTool;
320#[cfg(feature = "subagent")]
322#[oxicode_unstable(feature = "subagent")]
323pub use oxicode_agent::SubagentRunner;
324#[cfg(feature = "memory")]
327#[oxicode_unstable(feature = "memory")]
328pub use oxicode_agent::tools::{MemoryBackend, MemoryItem};
329#[cfg(feature = "memory")]
331#[oxicode_unstable(feature = "memory")]
332pub use oxicode_agent::tools::{
333 MemoryEditTool, MemoryRecallTool, MemoryReflectTool, MemoryRetainTool, SubagentTool,
334};
335#[oxicode_stable(since = "0.63.0")]
337pub use oxicode_agent::tools::{ResolvedContent, UrlResolver};
338#[cfg(feature = "agent-hub")]
340#[oxicode_unstable(feature = "agent-hub")]
341pub use oxicode_agent::{AgentHubStatus, AgentInfo, AgentKind, AgentPoolProvider};
342#[cfg(feature = "lsp")]
344#[oxicode_unstable(feature = "lsp")]
345pub use oxicode_agent::{LspAction, LspProvider};
346
347#[oxicode_internal]
350pub use oxicode_hashline;
351
352#[oxicode_stable(since = "0.63.0")]
359pub use oxicode_ai::OpenAiProvider;
360#[oxicode_stable(since = "0.63.0")]
361pub use oxicode_ai::OpenAiResponsesProvider;
362
363#[oxicode_stable(since = "0.63.0")]
371pub use oxicode_agent::mcp::{
372 ConsentManager, ConsentState, DirectToolDef, DirectToolsConfig, LifecycleMode, McpCallResult,
373 McpConfig, McpConnectionStatus, McpContent, McpDashboardData, McpDirectTool, McpManager,
374 McpSamplingRequest, McpServerInfo, McpSettings, McpSettingsView, McpTool, McpToolDef,
375 McpToolInfo, MetadataCache, ServerEntry, ToolMetadata, ToolPrefix,
376};
377
378#[cfg(feature = "mcp-transport")]
382#[oxicode_unstable(feature = "mcp-transport")]
383pub use oxicode_agent::mcp::transport::{
384 McpTransport, http::StreamableHttpTransport, stdio::StdioTransport,
385};
386
387#[cfg(feature = "mcp-spawn-validator")]
391#[oxicode_unstable(feature = "mcp-spawn-validator")]
392pub use oxicode_agent::mcp::{NoopSpawnValidator, SpawnValidator};
393
394#[cfg(feature = "circuit-breaker")]
398#[oxicode_unstable(feature = "circuit-breaker")]
399pub use oxicode_ai::circuit_breaker::{
400 BreakerError, BreakerState, CircuitBreaker, DefaultCircuitBreaker, SharedBreaker,
401};
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406 use std::path::Path;
407
408 #[test]
409 fn catalog_reachable_via_sdk() {
410 let providers = load_builtin_providers();
414 assert!(
415 providers.len() >= 70,
416 "expected >= 70 providers, got {}",
417 providers.len()
418 );
419 let all = get_all_models().collect::<Vec<_>>();
421 assert!(
422 !all.is_empty(),
423 "models should be loaded via get_all_models()"
424 );
425 assert!(
426 all.len() > 5000,
427 "expected >5000 materialized models, got {}",
428 all.len()
429 );
430 }
431
432 #[test]
433 fn sentinel_count_via_sdk() {
434 let n = builtin_model_count_sentinel();
435 assert_eq!(
438 n, 0,
439 "expected 0 sentinel entries with materialize, got {n}"
440 );
441 }
442
443 fn test_model(id: &str, provider: &str) -> Model {
445 Model::new(
446 id,
447 id,
448 Api::AnthropicMessages,
449 provider,
450 "https://api.example.com",
451 )
452 }
453
454 #[test]
455 fn test_oxicode_builder_new() {
456 let oxicode = OxicodeBuilder::new().build();
457 assert!(
459 oxicode
460 .resolve_model("anthropic/claude-sonnet-4-20250514")
461 .is_err()
462 );
463 }
464
465 #[test]
466 fn test_oxicode_builder_with_builtins() {
467 let oxicode = OxicodeBuilder::new().with_builtins().build();
468 assert!(
470 oxicode
471 .resolve_model("anthropic/claude-sonnet-4-20250514")
472 .is_ok()
473 );
474 assert!(oxicode.resolve_model("openai/gpt-4o").is_ok());
475 }
476
477 #[test]
478 fn test_oxicode_builder_custom_model() {
479 let oxicode = OxicodeBuilder::new()
480 .model(test_model("test-model", "test-provider"))
481 .build();
482 assert!(oxicode.resolve_model("test-provider/test-model").is_ok());
483 }
484
485 #[test]
486 fn test_oxicode_provider_resolution() {
487 let oxicode = OxicodeBuilder::new().with_builtins().build();
488 assert!(oxicode.create_provider("anthropic").is_ok());
490 assert!(oxicode.create_provider("nonexistent").is_err());
492 }
493
494 #[test]
495 fn test_agent_builder_workspace() {
496 let oxicode = OxicodeBuilder::new().with_builtins().build();
497 let config = AgentConfig {
498 model_id: "anthropic/claude-sonnet-4-20250514".into(),
499 timeout_seconds: 30,
500 ..Default::default()
501 };
502 let result = oxicode
504 .agent(config)
505 .workspace("/tmp/test-workspace")
506 .build();
507 assert!(result.is_ok() || result.is_err());
508 }
509
510 #[test]
511 fn test_agent_builder_coding_tools() {
512 let oxicode = OxicodeBuilder::new().with_builtins().build();
513 let config = AgentConfig {
514 model_id: "anthropic/claude-sonnet-4-20250514".into(),
515 timeout_seconds: 30,
516 ..Default::default()
517 };
518 let result = oxicode
519 .agent(config)
520 .workspace("/tmp")
521 .coding_tools()
522 .build();
523 if let Ok(agent) = result {
524 let tool_names = agent.tools().names();
525 assert!(tool_names.contains(&"read".to_string()));
526 assert!(tool_names.contains(&"write".to_string()));
527 assert!(tool_names.contains(&"edit".to_string()));
528 assert!(tool_names.contains(&"ls".to_string()));
529 }
530 }
531
532 #[test]
533 fn test_agent_builder_readonly_tools() {
534 let oxicode = OxicodeBuilder::new().with_builtins().build();
535 let config = AgentConfig {
536 model_id: "anthropic/claude-sonnet-4-20250514".into(),
537 timeout_seconds: 30,
538 ..Default::default()
539 };
540 let result = oxicode
541 .agent(config)
542 .workspace("/tmp")
543 .readonly_tools()
544 .build();
545 if let Ok(agent) = result {
546 let tool_names = agent.tools().names();
547 assert!(tool_names.contains(&"read".to_string()));
548 assert!(tool_names.contains(&"ls".to_string()));
549 assert!(!tool_names.contains(&"write".to_string()));
551 }
552 }
553
554 #[test]
555 fn test_model_registry_isolation() {
556 let oxicode1 = OxicodeBuilder::new()
558 .model(test_model("unique-1", "test"))
559 .build();
560
561 let oxicode2 = OxicodeBuilder::new().with_builtins().build();
562
563 assert!(oxicode2.resolve_model("test/unique-1").is_err());
565 assert!(oxicode1.resolve_model("test/unique-1").is_ok());
567 }
568
569 #[test]
570 fn test_tool_factory_coding_tools() {
571 let tools = crate::tool_factory::coding_tools(Path::new("/tmp"));
572 let names = tools.names();
573 assert!(names.contains(&"read".to_string()));
574 assert!(names.contains(&"write".to_string()));
575 assert!(names.contains(&"edit".to_string()));
576 assert!(names.contains(&"ls".to_string()));
577 assert_eq!(names.len(), 4);
578 }
579
580 #[test]
581 fn test_tool_factory_readonly_tools() {
582 let tools = crate::tool_factory::readonly_tools(Path::new("/tmp"));
583 let names = tools.names();
584 assert!(names.contains(&"read".to_string()));
585 assert!(names.contains(&"ls".to_string()));
586 assert_eq!(names.len(), 2);
587 }
588
589 #[test]
590 fn test_tool_registry_extend_from() {
591 let base = crate::tool_factory::coding_tools(Path::new("/tmp"));
592 let extra = crate::tool_factory::readonly_tools(Path::new("/tmp"));
593
594 let combined = ToolRegistry::new();
596 combined.extend_from(&base);
597 combined.extend_from(&extra);
598
599 let names = combined.names();
600 assert!(names.contains(&"read".to_string()));
601 assert!(names.contains(&"write".to_string()));
602 assert!(names.contains(&"ls".to_string()));
603 }
605
606 #[test]
609 fn test_provider_resolver_trait_on_oxicode() {
610 let oxicode = OxicodeBuilder::new().with_builtins().build();
611 let resolver: &dyn ProviderResolver = &oxicode;
613 assert!(resolver.resolve_provider("anthropic").is_some());
614 assert!(resolver.resolve_provider("nonexistent").is_none());
615 assert!(
616 resolver
617 .resolve_model("anthropic/claude-sonnet-4-20250514")
618 .is_some()
619 );
620 assert!(resolver.resolve_model("nonexistent/model").is_none());
621 }
622
623 #[test]
624 fn test_agent_uses_resolver_for_switch_model() {
625 let oxicode = OxicodeBuilder::new()
627 .model(test_model("test-model", "test-provider"))
628 .build();
629
630 let config = AgentConfig {
632 model_id: "test-provider/test-model".into(),
633 timeout_seconds: 5,
634 ..Default::default()
635 };
636 let result = oxicode.agent(config).build();
637 assert!(result.is_err());
640 }
641
642 #[test]
643 fn test_oxicode_builder_without_builtins() {
644 let oxicode = OxicodeBuilder::new().build();
645 assert!(
647 oxicode
648 .resolve_model("anthropic/claude-sonnet-4-20250514")
649 .is_err()
650 );
651 assert!(oxicode.create_provider("anthropic").is_err());
652 assert!(!oxicode.has_builtins());
653 }
654
655 #[test]
656 fn test_oxicode_builder_with_builtins_creates_providers() {
657 let oxicode = OxicodeBuilder::new().with_builtins().build();
658 assert!(oxicode.has_builtins());
659 assert!(oxicode.create_provider("anthropic").is_ok());
661 assert!(oxicode.create_provider("openai").is_ok());
662 assert!(oxicode.create_provider("deepseek").is_ok());
663 assert!(oxicode.create_provider("unknown-provider").is_err());
665 }
666
667 #[test]
676 fn test_create_provider_consults_auth_port() {
677 use parking_lot::Mutex;
678 use std::pin::Pin;
679
680 struct RecordingAuth {
684 calls: Mutex<Vec<String>>,
685 }
686 impl AuthProvider for RecordingAuth {
687 fn get_api_key(
688 &self,
689 _provider: &str,
690 ) -> Pin<Box<dyn Future<Output = Result<Option<String>, SdkError>> + Send + '_>>
691 {
692 Box::pin(async { Ok(None) })
693 }
694 fn get_api_key_sync(&self, provider: &str) -> Result<Option<String>, SdkError> {
695 self.calls.lock().push(provider.to_string());
696 Ok(None)
697 }
698 fn set_api_key(
699 &self,
700 _: &str,
701 _: &str,
702 ) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>> {
703 Box::pin(async { Ok(()) })
704 }
705 fn delete_api_key(
706 &self,
707 _: &str,
708 ) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>> {
709 Box::pin(async { Ok(()) })
710 }
711 fn get_oauth(
712 &self,
713 _: &str,
714 ) -> Pin<
715 Box<
716 dyn Future<Output = Result<Option<crate::ports::OAuthToken>, SdkError>>
717 + Send
718 + '_,
719 >,
720 > {
721 Box::pin(async { Ok(None) })
722 }
723 fn set_oauth(
724 &self,
725 _: &str,
726 _: crate::ports::OAuthToken,
727 ) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>> {
728 Box::pin(async { Ok(()) })
729 }
730 fn list_providers(
731 &self,
732 ) -> Pin<Box<dyn Future<Output = Result<Vec<String>, SdkError>> + Send + '_>>
733 {
734 Box::pin(async { Ok(Vec::new()) })
735 }
736 }
737
738 let auth = std::sync::Arc::new(RecordingAuth {
739 calls: Mutex::new(Vec::new()),
740 });
741 let oxicode = OxicodeBuilder::new()
742 .with_builtins()
743 .with_auth(auth.clone())
744 .build();
745
746 let _ = oxicode.create_provider("anthropic");
748 let calls = auth.calls.lock();
749 assert!(
750 calls.iter().any(|c| c == "anthropic"),
751 "Oxicode::create_provider must call AuthProvider::get_api_key_sync \
752 when the static api_keys map has no entry. Got calls: {:?}",
753 *calls
754 );
755 }
756
757 #[test]
758 fn test_closure_tool_sync() {
759 let tool = crate::closure_tool::ClosureTool::new_sync(
760 "test_tool",
761 "A test tool",
762 serde_json::json!({
763 "type": "object",
764 "properties": {
765 "input": { "type": "string" }
766 }
767 }),
768 |params, _ctx| {
769 let input = params["input"].as_str().unwrap_or("default");
770 Ok(AgentToolResult::success(format!("processed: {}", input)))
771 },
772 );
773
774 assert_eq!(tool.name(), "test_tool");
775 assert_eq!(tool.description(), "A test tool");
776
777 let rt = tokio::runtime::Runtime::new().unwrap();
778 let result = rt
779 .block_on(tool.execute(
780 "call_1",
781 serde_json::json!({"input": "hello"}),
782 None,
783 &ToolContext::default(),
784 ))
785 .unwrap();
786 assert!(result.success);
787 assert!(result.output.contains("processed: hello"));
788 }
789
790 #[test]
791 fn test_custom_tool_in_agent_builder() {
792 let oxicode = OxicodeBuilder::new().with_builtins().build();
793 let config = AgentConfig {
794 model_id: "anthropic/claude-sonnet-4-20250514".into(),
795 timeout_seconds: 30,
796 ..Default::default()
797 };
798 let result = oxicode
799 .agent(config)
800 .workspace("/tmp")
801 .custom_tool(
802 "my_tool",
803 "My custom tool",
804 serde_json::json!({"type": "object", "properties": {"query": {"type": "string"}}}),
805 |params, _ctx| {
806 Ok(AgentToolResult::success(format!(
807 "result: {}",
808 params["query"]
809 )))
810 },
811 )
812 .build();
813
814 if let Ok(agent) = result {
815 let tool_names = agent.tools().names();
816 assert!(tool_names.contains(&"my_tool".to_string()));
817 }
818 }
819
820 #[test]
821 fn test_full_isolation_between_instances() {
822 let oxicode1 = OxicodeBuilder::new()
824 .model(test_model("unique-alpha", "p1"))
825 .build();
826
827 let oxicode2 = OxicodeBuilder::new().with_builtins().build();
829
830 assert!(oxicode2.resolve_model("p1/unique-alpha").is_err());
832 assert!(
833 oxicode1
834 .resolve_model("anthropic/claude-sonnet-4-20250514")
835 .is_err()
836 );
837
838 assert!(oxicode1.create_provider("anthropic").is_err());
840 assert!(oxicode2.create_provider("anthropic").is_ok());
842 }
843
844 #[test]
845 fn test_agent_builder_system_prompt() {
846 let oxicode = OxicodeBuilder::new().with_builtins().build();
847 let config = AgentConfig {
848 model_id: "anthropic/claude-sonnet-4-20250514".into(),
849 timeout_seconds: 5,
850 ..Default::default()
851 };
852 let agent = oxicode
853 .agent(config)
854 .workspace("/tmp")
855 .system_prompt("You are a test agent.")
856 .build()
857 .unwrap();
858 drop(agent);
860 }
861
862 #[test]
863 fn test_oxicode_builder_api_key() {
864 let oxicode = OxicodeBuilder::new()
866 .with_builtins()
867 .api_key("anthropic", "sk-ant-test-key")
868 .build();
869 assert!(oxicode.has_builtins());
872 }
873
874 #[test]
875 fn test_oxicode_builder_base_url() {
876 let oxicode = OxicodeBuilder::new()
877 .with_builtins()
878 .base_url("openai", "https://my-proxy.example.com/v1")
879 .build();
880 assert!(oxicode.has_builtins());
881 }
882
883 #[test]
884 fn test_oxicode_builder_credential() {
885 let oxicode = OxicodeBuilder::new()
886 .with_builtins()
887 .credential(
888 "openai",
889 "sk-test-key",
890 Some("https://proxy.example.com/v1"),
891 )
892 .credential("anthropic", "sk-ant-test", None)
893 .build();
894 assert!(oxicode.has_builtins());
895 }
896}