1use std::sync::Arc;
4
5use awaken_contract::StateError;
6use awaken_contract::contract::executor::LlmExecutor;
7use awaken_contract::contract::storage::ThreadRunStore;
8use awaken_contract::contract::tool::Tool;
9use awaken_contract::registry_spec::AgentSpec;
10
11#[cfg(feature = "a2a")]
12use crate::backend::ExecutionBackendFactory;
13use crate::plugins::Plugin;
14#[cfg(feature = "a2a")]
15use crate::registry::BackendRegistry;
16#[cfg(feature = "a2a")]
17use crate::registry::composite::{CompositeAgentSpecRegistry, RemoteAgentSource};
18#[cfg(feature = "a2a")]
19use crate::registry::memory::MapBackendRegistry;
20use crate::registry::memory::{
21 MapAgentSpecRegistry, MapModelRegistry, MapPluginSource, MapProviderRegistry, MapToolRegistry,
22};
23use crate::registry::snapshot::RegistryHandle;
24use crate::registry::traits::{AgentSpecRegistry, ModelBinding, RegistrySet};
25use crate::runtime::AgentRuntime;
26
27#[derive(Debug, thiserror::Error)]
29pub enum BuildError {
30 #[error("state error: {0}")]
31 State(#[from] StateError),
32 #[error("agent registry conflict: {0}")]
33 AgentRegistryConflict(String),
34 #[error("tool registry conflict: {0}")]
35 ToolRegistryConflict(String),
36 #[error("model registry conflict: {0}")]
37 ModelRegistryConflict(String),
38 #[error("provider registry conflict: {0}")]
39 ProviderRegistryConflict(String),
40 #[error("plugin registry conflict: {0}")]
41 PluginRegistryConflict(String),
42 #[cfg(feature = "a2a")]
43 #[error("backend registry conflict: {0}")]
44 BackendRegistryConflict(String),
45 #[error("agent validation failed: {0}")]
46 ValidationFailed(String),
47 #[cfg(feature = "a2a")]
48 #[error("discovery failed: {0}")]
49 DiscoveryFailed(#[from] crate::registry::composite::DiscoveryError),
50}
51
52pub struct AgentRuntimeBuilder {
57 agents: MapAgentSpecRegistry,
58 tools: MapToolRegistry,
59 models: MapModelRegistry,
60 providers: MapProviderRegistry,
61 plugins: MapPluginSource,
62 #[cfg(feature = "a2a")]
63 backends: MapBackendRegistry,
64 thread_run_store: Option<Arc<dyn ThreadRunStore>>,
65 profile_store: Option<Arc<dyn awaken_contract::contract::profile_store::ProfileStore>>,
66 errors: Vec<BuildError>,
67 #[cfg(feature = "a2a")]
68 remote_sources: Vec<RemoteAgentSource>,
69}
70
71impl AgentRuntimeBuilder {
72 pub fn new() -> Self {
73 Self {
74 agents: MapAgentSpecRegistry::new(),
75 tools: MapToolRegistry::new(),
76 models: MapModelRegistry::new(),
77 providers: MapProviderRegistry::new(),
78 plugins: MapPluginSource::new(),
79 #[cfg(feature = "a2a")]
80 backends: MapBackendRegistry::with_default_remote_backends(),
81 thread_run_store: None,
82 profile_store: None,
83 errors: Vec::new(),
84 #[cfg(feature = "a2a")]
85 remote_sources: Vec::new(),
86 }
87 }
88
89 pub fn with_agent_spec(mut self, spec: AgentSpec) -> Self {
91 if let Err(e) = self.agents.register_spec(spec) {
92 self.errors.push(e);
93 }
94 self
95 }
96
97 pub fn with_agent_specs(mut self, specs: impl IntoIterator<Item = AgentSpec>) -> Self {
99 for spec in specs {
100 if let Err(e) = self.agents.register_spec(spec) {
101 self.errors.push(e);
102 }
103 }
104 self
105 }
106
107 pub fn with_tool(mut self, id: impl Into<String>, tool: Arc<dyn Tool>) -> Self {
109 if let Err(e) = self.tools.register_tool(id, tool) {
110 self.errors.push(e);
111 }
112 self
113 }
114
115 pub fn with_plugin(mut self, id: impl Into<String>, plugin: Arc<dyn Plugin>) -> Self {
117 if let Err(e) = self.plugins.register_plugin(id, plugin) {
118 self.errors.push(e);
119 }
120 self
121 }
122
123 pub fn with_model_binding(mut self, id: impl Into<String>, binding: ModelBinding) -> Self {
125 if let Err(e) = self.models.register_model(id, binding) {
126 self.errors.push(e);
127 }
128 self
129 }
130
131 pub fn with_provider(mut self, id: impl Into<String>, executor: Arc<dyn LlmExecutor>) -> Self {
133 if let Err(e) = self.providers.register_provider(id, executor) {
134 self.errors.push(e);
135 }
136 self
137 }
138
139 pub fn with_thread_run_store(mut self, store: Arc<dyn ThreadRunStore>) -> Self {
141 self.thread_run_store = Some(store);
142 self
143 }
144
145 pub fn with_profile_store(
147 mut self,
148 store: Arc<dyn awaken_contract::contract::profile_store::ProfileStore>,
149 ) -> Self {
150 self.profile_store = Some(store);
151 self
152 }
153
154 #[cfg(feature = "a2a")]
161 pub fn with_remote_agents(
162 mut self,
163 name: impl Into<String>,
164 base_url: impl Into<String>,
165 bearer_token: Option<String>,
166 ) -> Self {
167 self.remote_sources.push(RemoteAgentSource {
168 name: name.into(),
169 base_url: base_url.into(),
170 bearer_token,
171 });
172 self
173 }
174
175 #[cfg(feature = "a2a")]
177 pub fn with_agent_backend_factory(mut self, factory: Arc<dyn ExecutionBackendFactory>) -> Self {
178 if let Err(e) = self.backends.register_backend_factory(factory) {
179 self.errors.push(e);
180 }
181 self
182 }
183
184 pub fn build(self) -> Result<AgentRuntime, BuildError> {
191 let runtime = self.build_unchecked()?;
192 let resolver = runtime.resolver();
193 #[cfg(feature = "a2a")]
194 let registries = runtime.registry_set();
195 let mut errors = Vec::new();
196 for agent_id in resolver.agent_ids() {
197 #[cfg(feature = "a2a")]
198 {
199 if let Some(spec) = registries
200 .as_ref()
201 .and_then(|set| set.agents.get_agent(&agent_id))
202 && let Some(endpoint) = &spec.endpoint
203 {
204 let Some(factory) = registries
205 .as_ref()
206 .and_then(|set| set.backends.get_backend_factory(&endpoint.backend))
207 else {
208 errors.push(format!(
209 "{agent_id}: unsupported remote backend '{}'",
210 endpoint.backend
211 ));
212 continue;
213 };
214 if let Err(error) = factory.validate(endpoint) {
215 errors.push(format!("{agent_id}: {error}"));
216 }
217 continue;
218 }
219 }
220
221 if let Err(e) = resolver.resolve(&agent_id) {
222 errors.push(format!("{agent_id}: {e}"));
223 }
224 }
225 if !errors.is_empty() {
226 return Err(BuildError::ValidationFailed(errors.join("; ")));
227 }
228 Ok(runtime)
229 }
230
231 pub fn build_unchecked(mut self) -> Result<AgentRuntime, BuildError> {
237 if !self.errors.is_empty() {
238 return Err(self.errors.remove(0));
239 }
240
241 #[cfg(feature = "a2a")]
242 let (agents, composite_registry): (Arc<dyn AgentSpecRegistry>, _) =
243 if self.remote_sources.is_empty() {
244 (Arc::new(self.agents), None)
245 } else {
246 let mut composite = CompositeAgentSpecRegistry::new(Arc::new(self.agents));
247 for source in self.remote_sources {
248 composite.add_remote(source);
249 }
250 let arc = Arc::new(composite);
251 (Arc::clone(&arc) as Arc<dyn AgentSpecRegistry>, Some(arc))
252 };
253 #[cfg(not(feature = "a2a"))]
254 let agents: Arc<dyn AgentSpecRegistry> = Arc::new(self.agents);
255
256 let registry_set = RegistrySet {
257 agents,
258 tools: Arc::new(self.tools),
259 models: Arc::new(self.models),
260 providers: Arc::new(self.providers),
261 plugins: Arc::new(self.plugins),
262 #[cfg(feature = "a2a")]
263 backends: Arc::new(self.backends) as Arc<dyn BackendRegistry>,
264 };
265
266 let registry_handle = RegistryHandle::new(registry_set.clone());
267 let resolver: Arc<dyn crate::registry::ExecutionResolver> = Arc::new(
268 crate::registry::resolve::DynamicRegistryResolver::new(registry_handle.clone()),
269 );
270
271 let mut runtime = AgentRuntime::new_with_execution_resolver(resolver)
272 .with_registry_handle(registry_handle);
273
274 #[cfg(feature = "a2a")]
275 if let Some(composite) = composite_registry {
276 runtime = runtime.with_composite_registry(composite);
277 }
278
279 if let Some(store) = self.thread_run_store {
280 runtime = runtime.with_thread_run_store(store);
281 }
282
283 if let Some(store) = self.profile_store {
284 runtime = runtime.with_profile_store(store);
285 }
286
287 Ok(runtime)
288 }
289
290 #[cfg(feature = "a2a")]
292 pub async fn build_and_discover(self) -> Result<AgentRuntime, BuildError> {
293 let runtime = self.build_unchecked()?;
294 if let Some(composite) = runtime.composite_registry() {
295 composite.discover().await?;
296 }
297 Ok(runtime)
298 }
299}
300
301impl Default for AgentRuntimeBuilder {
302 fn default() -> Self {
303 Self::new()
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use async_trait::async_trait;
311 use awaken_contract::contract::executor::{InferenceExecutionError, InferenceRequest};
312 use awaken_contract::contract::inference::{StopReason, StreamResult, TokenUsage};
313 #[cfg(feature = "a2a")]
314 use awaken_contract::contract::lifecycle::TerminationReason;
315 use awaken_contract::contract::tool::{
316 ToolCallContext, ToolDescriptor, ToolError, ToolOutput, ToolResult,
317 };
318 #[cfg(feature = "a2a")]
319 use awaken_contract::registry_spec::RemoteEndpoint;
320 use serde_json::Value;
321 #[cfg(feature = "a2a")]
322 use std::sync::atomic::{AtomicUsize, Ordering};
323
324 use crate::registry::memory::{
325 MapAgentSpecRegistry, MapModelRegistry, MapPluginSource, MapProviderRegistry,
326 MapToolRegistry,
327 };
328
329 struct MockTool {
330 id: String,
331 }
332
333 #[async_trait]
334 impl Tool for MockTool {
335 fn descriptor(&self) -> ToolDescriptor {
336 ToolDescriptor::new(&self.id, &self.id, "mock tool")
337 }
338
339 async fn execute(
340 &self,
341 _args: Value,
342 _ctx: &ToolCallContext,
343 ) -> Result<ToolOutput, ToolError> {
344 Ok(ToolResult::success(&self.id, Value::Null).into())
345 }
346 }
347
348 struct MockExecutor;
349
350 #[async_trait]
351 impl LlmExecutor for MockExecutor {
352 async fn execute(
353 &self,
354 _request: InferenceRequest,
355 ) -> Result<StreamResult, InferenceExecutionError> {
356 Ok(StreamResult {
357 content: vec![],
358 tool_calls: vec![],
359 usage: Some(TokenUsage::default()),
360 stop_reason: Some(StopReason::EndTurn),
361 has_incomplete_tool_calls: false,
362 })
363 }
364
365 fn name(&self) -> &str {
366 "mock"
367 }
368 }
369
370 #[cfg(feature = "a2a")]
371 struct NoopRemoteBackend;
372
373 #[cfg(feature = "a2a")]
374 #[async_trait]
375 impl crate::backend::ExecutionBackend for NoopRemoteBackend {
376 async fn execute_root(
377 &self,
378 request: crate::backend::BackendRootRunRequest<'_>,
379 ) -> Result<crate::backend::BackendRunResult, crate::backend::ExecutionBackendError>
380 {
381 Ok(crate::backend::BackendRunResult {
382 agent_id: request.agent_id.to_string(),
383 status: crate::backend::BackendRunStatus::Completed,
384 termination: TerminationReason::NaturalEnd,
385 status_reason: None,
386 response: None,
387 output: crate::backend::BackendRunOutput::default(),
388 steps: 0,
389 run_id: None,
390 inbox: None,
391 state: None,
392 })
393 }
394 }
395
396 #[cfg(feature = "a2a")]
397 struct CountingValidationBackendFactory {
398 validate_count: Arc<AtomicUsize>,
399 build_count: Arc<AtomicUsize>,
400 }
401
402 #[cfg(feature = "a2a")]
403 impl crate::backend::ExecutionBackendFactory for CountingValidationBackendFactory {
404 fn backend(&self) -> &str {
405 "counting-remote"
406 }
407
408 fn validate(
409 &self,
410 endpoint: &RemoteEndpoint,
411 ) -> Result<(), crate::backend::ExecutionBackendFactoryError> {
412 self.validate_count.fetch_add(1, Ordering::SeqCst);
413 if endpoint.base_url.trim().is_empty() {
414 return Err(crate::backend::ExecutionBackendFactoryError::InvalidConfig(
415 "empty base_url".into(),
416 ));
417 }
418 Ok(())
419 }
420
421 fn build(
422 &self,
423 endpoint: &RemoteEndpoint,
424 ) -> Result<
425 Arc<dyn crate::backend::ExecutionBackend>,
426 crate::backend::ExecutionBackendFactoryError,
427 > {
428 self.build_count.fetch_add(1, Ordering::SeqCst);
429 if endpoint.backend != self.backend() {
430 return Err(crate::backend::ExecutionBackendFactoryError::InvalidConfig(
431 format!("unexpected backend '{}'", endpoint.backend),
432 ));
433 }
434 Ok(Arc::new(NoopRemoteBackend))
435 }
436 }
437
438 fn make_registry_set(agent_id: &str, model_id: &str, upstream_model: &str) -> RegistrySet {
439 let mut agents = MapAgentSpecRegistry::new();
440 agents
441 .register_spec(AgentSpec {
442 id: agent_id.into(),
443 model_id: model_id.into(),
444 system_prompt: format!("system-{agent_id}"),
445 ..Default::default()
446 })
447 .expect("register test agent");
448
449 let mut models = MapModelRegistry::new();
450 models
451 .register_model(
452 model_id,
453 ModelBinding {
454 provider_id: "mock".into(),
455 upstream_model: upstream_model.into(),
456 },
457 )
458 .expect("register test model");
459
460 let mut providers = MapProviderRegistry::new();
461 providers
462 .register_provider("mock", Arc::new(MockExecutor))
463 .expect("register test provider");
464
465 RegistrySet {
466 agents: Arc::new(agents),
467 tools: Arc::new(MapToolRegistry::new()),
468 models: Arc::new(models),
469 providers: Arc::new(providers),
470 plugins: Arc::new(MapPluginSource::new()),
471 backends: Arc::new(MapBackendRegistry::new()),
472 }
473 }
474
475 #[test]
476 fn builder_creates_runtime() {
477 let spec = AgentSpec {
478 id: "test-agent".into(),
479 model_id: "test-model".into(),
480 system_prompt: "You are helpful.".into(),
481 ..Default::default()
482 };
483
484 let runtime = AgentRuntimeBuilder::new()
485 .with_agent_spec(spec)
486 .with_tool("echo", Arc::new(MockTool { id: "echo".into() }))
487 .with_model_binding(
488 "test-model",
489 ModelBinding {
490 provider_id: "mock".into(),
491 upstream_model: "mock-model".into(),
492 },
493 )
494 .with_provider("mock", Arc::new(MockExecutor))
495 .build();
496
497 assert!(runtime.is_ok());
498 }
499
500 #[test]
501 fn builder_default_creates_empty() {
502 let builder = AgentRuntimeBuilder::default();
503 let runtime = builder.build();
505 assert!(runtime.is_ok());
506 }
507
508 #[test]
509 fn builder_with_multiple_agents() {
510 let spec1 = AgentSpec {
511 id: "agent-1".into(),
512 model_id: "m".into(),
513 system_prompt: "sys".into(),
514 ..Default::default()
515 };
516 let spec2 = AgentSpec {
517 id: "agent-2".into(),
518 model_id: "m".into(),
519 system_prompt: "sys".into(),
520 ..Default::default()
521 };
522
523 let runtime = AgentRuntimeBuilder::new()
524 .with_agent_specs(vec![spec1, spec2])
525 .with_model_binding(
526 "m",
527 ModelBinding {
528 provider_id: "p".into(),
529 upstream_model: "n".into(),
530 },
531 )
532 .with_provider("p", Arc::new(MockExecutor))
533 .build()
534 .unwrap();
535
536 assert!(runtime.resolver().resolve("agent-1").is_ok());
538 assert!(runtime.resolver().resolve("agent-2").is_ok());
539 }
540
541 #[test]
542 fn builder_resolver_returns_correct_config() {
543 let spec = AgentSpec {
544 id: "my-agent".into(),
545 model_id: "test-model".into(),
546 system_prompt: "Be helpful.".into(),
547 max_rounds: 10,
548 ..Default::default()
549 };
550
551 let runtime = AgentRuntimeBuilder::new()
552 .with_agent_spec(spec)
553 .with_tool(
554 "search",
555 Arc::new(MockTool {
556 id: "search".into(),
557 }),
558 )
559 .with_model_binding(
560 "test-model",
561 ModelBinding {
562 provider_id: "mock".into(),
563 upstream_model: "claude-test".into(),
564 },
565 )
566 .with_provider("mock", Arc::new(MockExecutor))
567 .build()
568 .unwrap();
569
570 let resolved = runtime.resolver().resolve("my-agent").unwrap();
571 assert_eq!(resolved.id(), "my-agent");
572 assert_eq!(resolved.upstream_model, "claude-test");
573 assert_eq!(resolved.system_prompt(), "Be helpful.");
574 assert_eq!(resolved.max_rounds(), 10);
575 assert!(resolved.tools.contains_key("search"));
576 }
577
578 #[test]
579 fn builder_missing_agent_errors() {
580 let runtime = AgentRuntimeBuilder::new()
581 .with_model_binding(
582 "m",
583 ModelBinding {
584 provider_id: "p".into(),
585 upstream_model: "n".into(),
586 },
587 )
588 .with_provider("p", Arc::new(MockExecutor))
589 .build()
590 .unwrap();
591
592 let err = runtime.resolver().resolve("nonexistent");
593 assert!(err.is_err());
594 }
595
596 #[test]
601 fn builder_with_plugin() {
602 use crate::plugins::{Plugin, PluginDescriptor, PluginRegistrar};
603
604 struct TestPlugin;
605 impl Plugin for TestPlugin {
606 fn descriptor(&self) -> PluginDescriptor {
607 PluginDescriptor {
608 name: "test-builder-plugin",
609 }
610 }
611 fn register(
612 &self,
613 _registrar: &mut PluginRegistrar,
614 ) -> Result<(), awaken_contract::StateError> {
615 Ok(())
616 }
617 }
618
619 let runtime = AgentRuntimeBuilder::new()
620 .with_plugin("test-builder-plugin", Arc::new(TestPlugin))
621 .build()
622 .unwrap();
623 let _ = runtime;
624 }
625
626 #[test]
627 fn builder_chained_tools_all_registered() {
628 let spec = AgentSpec {
629 id: "agent".into(),
630 model_id: "m".into(),
631 system_prompt: "sys".into(),
632 ..Default::default()
633 };
634
635 let runtime = AgentRuntimeBuilder::new()
636 .with_agent_spec(spec)
637 .with_tool("t1", Arc::new(MockTool { id: "t1".into() }))
638 .with_tool("t2", Arc::new(MockTool { id: "t2".into() }))
639 .with_tool("t3", Arc::new(MockTool { id: "t3".into() }))
640 .with_model_binding(
641 "m",
642 ModelBinding {
643 provider_id: "p".into(),
644 upstream_model: "n".into(),
645 },
646 )
647 .with_provider("p", Arc::new(MockExecutor))
648 .build()
649 .unwrap();
650
651 let resolved = runtime.resolver().resolve("agent").unwrap();
652 assert!(resolved.tools.contains_key("t1"));
653 assert!(resolved.tools.contains_key("t2"));
654 assert!(resolved.tools.contains_key("t3"));
655 }
656
657 #[test]
658 fn build_catches_missing_model() {
659 let spec = AgentSpec {
660 id: "bad-agent".into(),
661 model_id: "nonexistent-model".into(),
662 system_prompt: "sys".into(),
663 ..Default::default()
664 };
665
666 let result = AgentRuntimeBuilder::new().with_agent_spec(spec).build();
667
668 let err = match result {
669 Err(e) => e.to_string(),
670 Ok(_) => panic!("expected build to fail for missing model"),
671 };
672 assert!(
673 err.contains("bad-agent"),
674 "error should mention the agent ID: {err}"
675 );
676 }
677
678 #[test]
679 fn build_succeeds_with_valid_config() {
680 let spec = AgentSpec {
681 id: "good-agent".into(),
682 model_id: "m".into(),
683 system_prompt: "sys".into(),
684 ..Default::default()
685 };
686
687 let result = AgentRuntimeBuilder::new()
688 .with_agent_spec(spec)
689 .with_model_binding(
690 "m",
691 ModelBinding {
692 provider_id: "p".into(),
693 upstream_model: "n".into(),
694 },
695 )
696 .with_provider("p", Arc::new(MockExecutor))
697 .build();
698
699 assert!(result.is_ok());
700 }
701
702 #[test]
703 fn builder_runtime_starts_with_registry_version_one() {
704 let runtime = AgentRuntimeBuilder::new()
705 .with_agent_spec(AgentSpec {
706 id: "versioned-agent".into(),
707 model_id: "m".into(),
708 system_prompt: "sys".into(),
709 ..Default::default()
710 })
711 .with_model_binding(
712 "m",
713 ModelBinding {
714 provider_id: "mock".into(),
715 upstream_model: "model-v1".into(),
716 },
717 )
718 .with_provider("mock", Arc::new(MockExecutor))
719 .build()
720 .unwrap();
721
722 assert_eq!(runtime.registry_version(), Some(1));
723 assert!(runtime.registry_handle().is_some());
724 }
725
726 #[test]
727 fn replacing_registry_set_updates_dynamic_resolver() {
728 let runtime = AgentRuntimeBuilder::new()
729 .with_agent_spec(AgentSpec {
730 id: "agent-v1".into(),
731 model_id: "m".into(),
732 system_prompt: "sys".into(),
733 ..Default::default()
734 })
735 .with_model_binding(
736 "m",
737 ModelBinding {
738 provider_id: "mock".into(),
739 upstream_model: "model-v1".into(),
740 },
741 )
742 .with_provider("mock", Arc::new(MockExecutor))
743 .build()
744 .unwrap();
745
746 assert!(runtime.resolver().resolve("agent-v1").is_ok());
747 assert!(runtime.resolver().resolve("agent-v2").is_err());
748
749 let version = runtime
750 .replace_registry_set(make_registry_set("agent-v2", "m2", "model-v2"))
751 .expect("builder runtimes should expose a registry handle");
752
753 assert_eq!(version, 2);
754 assert_eq!(runtime.registry_version(), Some(2));
755 assert!(runtime.resolver().resolve("agent-v1").is_err());
756
757 let resolved = runtime.resolver().resolve("agent-v2").unwrap();
758 assert_eq!(resolved.id(), "agent-v2");
759 assert_eq!(resolved.upstream_model, "model-v2");
760 }
761
762 #[test]
763 fn builder_model_binding_provider_name() {
764 let spec = AgentSpec {
765 id: "agent".into(),
766 model_id: "gpt-4".into(),
767 system_prompt: "sys".into(),
768 ..Default::default()
769 };
770
771 let runtime = AgentRuntimeBuilder::new()
772 .with_agent_spec(spec)
773 .with_model_binding(
774 "gpt-4",
775 ModelBinding {
776 provider_id: "openai".into(),
777 upstream_model: "gpt-4-turbo".into(),
778 },
779 )
780 .with_provider("openai", Arc::new(MockExecutor))
781 .build()
782 .unwrap();
783
784 let resolved = runtime.resolver().resolve("agent").unwrap();
785 assert_eq!(resolved.upstream_model, "gpt-4-turbo");
787 }
788
789 #[test]
790 fn builder_with_profile_store() {
791 use awaken_contract::contract::profile_store::{
792 ProfileEntry, ProfileOwner as POwner, ProfileStore,
793 };
794 use awaken_contract::contract::storage::StorageError;
795
796 struct NoOpProfileStore;
797
798 #[async_trait]
799 impl ProfileStore for NoOpProfileStore {
800 async fn get(
801 &self,
802 _owner: &POwner,
803 _key: &str,
804 ) -> Result<Option<ProfileEntry>, StorageError> {
805 Ok(None)
806 }
807 async fn set(
808 &self,
809 _owner: &POwner,
810 _key: &str,
811 _value: Value,
812 ) -> Result<(), StorageError> {
813 Ok(())
814 }
815 async fn delete(&self, _owner: &POwner, _key: &str) -> Result<(), StorageError> {
816 Ok(())
817 }
818 async fn list(&self, _owner: &POwner) -> Result<Vec<ProfileEntry>, StorageError> {
819 Ok(vec![])
820 }
821 async fn clear_owner(&self, _owner: &POwner) -> Result<(), StorageError> {
822 Ok(())
823 }
824 }
825
826 let runtime = AgentRuntimeBuilder::new()
827 .with_profile_store(Arc::new(NoOpProfileStore))
828 .build()
829 .unwrap();
830 assert!(runtime.profile_store.is_some());
831 }
832
833 #[cfg(feature = "a2a")]
834 #[test]
835 fn build_allows_endpoint_agents_when_backend_factory_exists() {
836 let validate_count = Arc::new(AtomicUsize::new(0));
837 let build_count = Arc::new(AtomicUsize::new(0));
838 let runtime = AgentRuntimeBuilder::new()
839 .with_agent_spec(
840 AgentSpec::new("remote-agent")
841 .with_model_id("remote-model")
842 .with_system_prompt("remote")
843 .with_endpoint(RemoteEndpoint {
844 backend: "counting-remote".into(),
845 base_url: "https://remote.example.com".into(),
846 ..Default::default()
847 }),
848 )
849 .with_agent_backend_factory(Arc::new(CountingValidationBackendFactory {
850 validate_count: validate_count.clone(),
851 build_count: build_count.clone(),
852 }))
853 .build()
854 .expect("endpoint agents should validate through backend factory");
855
856 let spec = runtime
857 .registry_set()
858 .and_then(|set| set.agents.get_agent("remote-agent"))
859 .expect("remote agent should remain registered");
860 assert!(spec.endpoint.is_some());
861 assert_eq!(validate_count.load(Ordering::SeqCst), 1);
862 assert_eq!(build_count.load(Ordering::SeqCst), 0);
863 }
864
865 #[test]
866 fn duplicate_agent_spec_errors_at_build() {
867 let spec = AgentSpec {
868 id: "dup-agent".into(),
869 model_id: "m".into(),
870 system_prompt: "sys".into(),
871 ..Default::default()
872 };
873
874 let result = AgentRuntimeBuilder::new()
875 .with_agent_spec(spec.clone())
876 .with_agent_spec(spec)
877 .with_model_binding(
878 "m",
879 ModelBinding {
880 provider_id: "p".into(),
881 upstream_model: "n".into(),
882 },
883 )
884 .with_provider("p", Arc::new(MockExecutor))
885 .build();
886
887 match result {
888 Err(e) => {
889 let err = e.to_string();
890 assert!(
891 err.contains("dup-agent"),
892 "error should mention the duplicate agent ID: {err}"
893 );
894 }
895 Ok(_) => panic!("expected build to fail for duplicate agent"),
896 }
897 }
898
899 #[test]
900 fn duplicate_tool_errors_at_build() {
901 let result = AgentRuntimeBuilder::new()
902 .with_tool(
903 "dup-tool",
904 Arc::new(MockTool {
905 id: "dup-tool".into(),
906 }),
907 )
908 .with_tool(
909 "dup-tool",
910 Arc::new(MockTool {
911 id: "dup-tool".into(),
912 }),
913 )
914 .build();
915
916 match result {
917 Err(e) => {
918 let err = e.to_string();
919 assert!(
920 err.contains("dup-tool"),
921 "error should mention the duplicate tool ID: {err}"
922 );
923 }
924 Ok(_) => panic!("expected build to fail for duplicate tool"),
925 }
926 }
927
928 #[test]
929 fn duplicate_model_errors_at_build() {
930 let result = AgentRuntimeBuilder::new()
931 .with_model_binding(
932 "dup-model",
933 ModelBinding {
934 provider_id: "p".into(),
935 upstream_model: "n1".into(),
936 },
937 )
938 .with_model_binding(
939 "dup-model",
940 ModelBinding {
941 provider_id: "p".into(),
942 upstream_model: "n2".into(),
943 },
944 )
945 .build();
946
947 match result {
948 Err(e) => {
949 let err = e.to_string();
950 assert!(
951 err.contains("dup-model"),
952 "error should mention the duplicate model ID: {err}"
953 );
954 }
955 Ok(_) => panic!("expected build to fail for duplicate model"),
956 }
957 }
958
959 #[test]
960 fn duplicate_provider_errors_at_build() {
961 let result = AgentRuntimeBuilder::new()
962 .with_provider("dup-prov", Arc::new(MockExecutor))
963 .with_provider("dup-prov", Arc::new(MockExecutor))
964 .build();
965
966 match result {
967 Err(e) => {
968 let err = e.to_string();
969 assert!(
970 err.contains("dup-prov"),
971 "error should mention the duplicate provider ID: {err}"
972 );
973 }
974 Ok(_) => panic!("expected build to fail for duplicate provider"),
975 }
976 }
977
978 #[cfg(feature = "a2a")]
979 #[test]
980 fn duplicate_backend_factory_errors_at_build() {
981 let result = AgentRuntimeBuilder::new()
982 .with_agent_backend_factory(Arc::new(crate::extensions::a2a::A2aBackendFactory))
983 .build();
984
985 match result {
986 Err(BuildError::BackendRegistryConflict(err)) => {
987 assert!(
988 err.contains("a2a"),
989 "error should mention the duplicate backend kind: {err}"
990 );
991 }
992 Err(other) => panic!("expected backend registry conflict, got {other}"),
993 Ok(_) => panic!("expected build to fail for duplicate backend factory"),
994 }
995 }
996}