1use std::sync::Arc;
4
5use awaken_runtime_contract::StateError;
6use awaken_runtime_contract::contract::commit_coordinator::CommitCoordinator;
7use awaken_runtime_contract::contract::executor::LlmExecutor;
8use awaken_runtime_contract::contract::tool::Tool;
9use awaken_runtime_contract::registry_spec::{AgentSpec, ModelPoolSpec, ModelSpec};
10#[cfg(feature = "a2a")]
11use awaken_runtime_contract::registry_spec::{RemoteAuth, RemoteEndpoint};
12
13#[cfg(feature = "a2a")]
14use crate::backend::ExecutionBackendFactory;
15use crate::plugins::Plugin;
16#[cfg(feature = "a2a")]
17use crate::registry::BackendRegistry;
18#[cfg(feature = "a2a")]
19use crate::registry::composite::{CompositeAgentSpecRegistry, RemoteAgentSource};
20#[cfg(feature = "a2a")]
21use crate::registry::memory::MapBackendRegistry;
22use crate::registry::memory::{
23 MapAgentSpecRegistry, MapModelRegistry, MapPluginSource, MapProviderRegistry, MapToolRegistry,
24};
25use crate::registry::snapshot::RegistryHandle;
26use crate::registry::traits::{AgentSpecRegistry, RegistrySet};
27use crate::runtime::AgentRuntime;
28
29#[derive(Debug, thiserror::Error)]
31pub enum BuildError {
32 #[error("state error: {0}")]
33 State(#[from] StateError),
34 #[error("agent registry conflict: {0}")]
35 AgentRegistryConflict(String),
36 #[error("tool registry conflict: {0}")]
37 ToolRegistryConflict(String),
38 #[error("model registry conflict: {0}")]
39 ModelRegistryConflict(String),
40 #[error("provider registry conflict: {0}")]
41 ProviderRegistryConflict(String),
42 #[error("plugin registry conflict: {0}")]
43 PluginRegistryConflict(String),
44 #[cfg(feature = "a2a")]
45 #[error("backend registry conflict: {0}")]
46 BackendRegistryConflict(String),
47 #[error("agent validation failed: {0}")]
48 ValidationFailed(String),
49 #[error(
53 "ADR-0036 D8: `with_thread_run_store` requires a paired `with_commit_coordinator` \
54 supplied by the store/server integration"
55 )]
56 CommitCoordinatorRequired,
57 #[error("config validation failed: {0}")]
58 ConfigValidation(#[from] awaken_runtime_contract::ConfigValidationError),
59 #[cfg(feature = "a2a")]
60 #[error("discovery failed: {0}")]
61 DiscoveryFailed(#[from] crate::registry::composite::DiscoveryError),
62}
63
64pub struct AgentRuntimeBuilder {
69 agents: MapAgentSpecRegistry,
70 tools: MapToolRegistry,
71 models: MapModelRegistry,
72 providers: MapProviderRegistry,
73 plugins: MapPluginSource,
74 #[cfg(feature = "a2a")]
75 backends: MapBackendRegistry,
76 commit_coordinator: Option<Arc<dyn CommitCoordinator>>,
77 profile_store: Option<Arc<dyn awaken_runtime_contract::contract::profile_store::ProfileStore>>,
78 errors: Vec<BuildError>,
79 #[cfg(feature = "a2a")]
80 remote_sources: Vec<RemoteAgentSource>,
81}
82
83impl AgentRuntimeBuilder {
84 pub fn new() -> Self {
85 Self {
86 agents: MapAgentSpecRegistry::new(),
87 tools: MapToolRegistry::new(),
88 models: MapModelRegistry::new(),
89 providers: MapProviderRegistry::new(),
90 plugins: MapPluginSource::new(),
91 #[cfg(feature = "a2a")]
92 backends: MapBackendRegistry::with_default_remote_backends(),
93 commit_coordinator: None,
94 profile_store: None,
95 errors: Vec::new(),
96 #[cfg(feature = "a2a")]
97 remote_sources: Vec::new(),
98 }
99 }
100
101 pub fn with_agent_spec(mut self, spec: AgentSpec) -> Self {
103 if let Err(e) = self.agents.register_spec(spec) {
104 self.errors.push(e);
105 }
106 self
107 }
108
109 pub fn with_agent_specs(mut self, specs: impl IntoIterator<Item = AgentSpec>) -> Self {
111 for spec in specs {
112 if let Err(e) = self.agents.register_spec(spec) {
113 self.errors.push(e);
114 }
115 }
116 self
117 }
118
119 pub fn with_tool(mut self, id: impl Into<String>, tool: Arc<dyn Tool>) -> Self {
121 if let Err(e) = self.tools.register_tool(id, tool) {
122 self.errors.push(e);
123 }
124 self
125 }
126
127 pub fn with_plugin(mut self, id: impl Into<String>, plugin: Arc<dyn Plugin>) -> Self {
129 if let Err(e) = self.plugins.register_plugin(id, plugin) {
130 self.errors.push(e);
131 }
132 self
133 }
134
135 pub fn with_model(mut self, spec: ModelSpec) -> Self {
137 if self.models.contains_key(&spec.id) {
140 self.errors.push(BuildError::ConfigValidation(
141 awaken_runtime_contract::ConfigValidationError::DuplicateModelId {
142 id: spec.id.clone(),
143 },
144 ));
145 return self;
146 }
147 if let Err(e) = self.models.register_model(spec) {
148 self.errors.push(e);
149 }
150 self
151 }
152
153 pub fn with_model_pool(mut self, spec: ModelPoolSpec) -> Self {
157 if let Err(e) = self.models.register_model_pool(spec) {
158 self.errors.push(e);
159 }
160 self
161 }
162
163 pub fn with_provider(mut self, id: impl Into<String>, executor: Arc<dyn LlmExecutor>) -> Self {
165 if let Err(e) = self.providers.register_provider(id, executor) {
166 self.errors.push(e);
167 }
168 self
169 }
170
171 pub fn with_mock_provider_profile(
173 mut self,
174 profile: crate::engine::MockProviderProfile,
175 ) -> Self {
176 let provider_id = profile.provider_id.clone();
177 if let Err(e) = self
178 .providers
179 .register_provider(provider_id, profile.executor())
180 {
181 self.errors.push(e);
182 }
183 if let Err(e) = self.models.register_model(profile.model_spec()) {
184 self.errors.push(e);
185 }
186 self
187 }
188
189 #[cfg(feature = "test-utils")]
196 pub fn with_in_memory_thread_run_store(
197 mut self,
198 store: Arc<awaken_stores::InMemoryStore>,
199 ) -> Self {
200 let coordinator = awaken_stores::MemoryCommitCoordinator::wrap(store);
201 self.commit_coordinator = Some(coordinator as Arc<dyn CommitCoordinator>);
202 self
203 }
204
205 pub fn with_commit_coordinator(mut self, coordinator: Arc<dyn CommitCoordinator>) -> Self {
211 self.commit_coordinator = Some(coordinator);
212 self
213 }
214
215 pub fn with_profile_store(
217 mut self,
218 store: Arc<dyn awaken_runtime_contract::contract::profile_store::ProfileStore>,
219 ) -> Self {
220 self.profile_store = Some(store);
221 self
222 }
223
224 #[cfg(feature = "a2a")]
231 pub fn with_remote_agents(
232 mut self,
233 name: impl Into<String>,
234 base_url: impl Into<String>,
235 bearer_token: Option<String>,
236 ) -> Self {
237 self.remote_sources.push(RemoteAgentSource::from_endpoint(
238 name,
239 RemoteEndpoint {
240 base_url: base_url.into(),
241 auth: bearer_token.map(RemoteAuth::bearer),
242 ..Default::default()
243 },
244 ));
245 self
246 }
247
248 #[cfg(feature = "a2a")]
250 pub fn with_agent_backend_factory(mut self, factory: Arc<dyn ExecutionBackendFactory>) -> Self {
251 if let Err(e) = self.backends.register_backend_factory(factory) {
252 self.errors.push(e);
253 }
254 self
255 }
256
257 pub fn build(self) -> Result<AgentRuntime, BuildError> {
264 let runtime = self.build_unchecked()?;
265 let resolver = runtime.resolver();
266 #[cfg(feature = "a2a")]
267 let registries = runtime.registry_set();
268 let mut errors = Vec::new();
269 for agent_id in resolver.agent_ids() {
270 #[cfg(feature = "a2a")]
271 {
272 if let Some(spec) = registries
273 .as_ref()
274 .and_then(|set| set.agents.get_agent(&agent_id))
275 && spec.uses_remote_backend()
276 {
277 let endpoint = match spec.remote_endpoint() {
278 Ok(Some(endpoint)) => endpoint,
279 Ok(None) => {
280 errors.push(format!(
281 "{agent_id}: invalid remote backend '{}' config",
282 spec.backend.kind
283 ));
284 continue;
285 }
286 Err(error) => {
287 errors.push(format!(
288 "{agent_id}: invalid remote backend '{}' config: {error}",
289 spec.backend.kind
290 ));
291 continue;
292 }
293 };
294 let Some(factory) = registries
295 .as_ref()
296 .and_then(|set| set.backends.get_backend_factory(&endpoint.backend))
297 else {
298 errors.push(format!(
299 "{agent_id}: unsupported remote backend '{}'",
300 endpoint.backend
301 ));
302 continue;
303 };
304 if let Err(error) = factory.validate(&endpoint) {
305 errors.push(format!("{agent_id}: {error}"));
306 }
307 continue;
308 }
309 }
310
311 if let Err(e) = resolver.resolve(&agent_id) {
312 errors.push(format!("{agent_id}: {e}"));
313 }
314 }
315 if !errors.is_empty() {
316 return Err(BuildError::ValidationFailed(errors.join("; ")));
317 }
318 Ok(runtime)
319 }
320
321 pub fn build_unchecked(mut self) -> Result<AgentRuntime, BuildError> {
327 if !self.errors.is_empty() {
328 return Err(self.errors.remove(0));
329 }
330
331 #[cfg(feature = "a2a")]
332 let (agents, composite_registry): (Arc<dyn AgentSpecRegistry>, _) =
333 if self.remote_sources.is_empty() {
334 (Arc::new(self.agents), None)
335 } else {
336 let mut composite = CompositeAgentSpecRegistry::new(Arc::new(self.agents));
337 for source in self.remote_sources {
338 composite.add_remote(source);
339 }
340 let arc = Arc::new(composite);
341 (Arc::clone(&arc) as Arc<dyn AgentSpecRegistry>, Some(arc))
342 };
343 #[cfg(not(feature = "a2a"))]
344 let agents: Arc<dyn AgentSpecRegistry> = Arc::new(self.agents);
345
346 let registry_set = RegistrySet {
347 agents,
348 tools: Arc::new(self.tools),
349 models: Arc::new(self.models),
350 providers: Arc::new(self.providers),
351 plugins: Arc::new(self.plugins),
352 #[cfg(feature = "a2a")]
353 backends: Arc::new(self.backends) as Arc<dyn BackendRegistry>,
354 };
355
356 let registry_handle = RegistryHandle::new(registry_set.clone());
357 let resolver_impl = Arc::new(crate::registry::resolve::DynamicRegistryResolver::new(
358 registry_handle.clone(),
359 ));
360 let resolver: Arc<dyn crate::registry::AgentResolver> = resolver_impl.clone();
361 let run_resolver: Arc<dyn crate::resolution::Resolver> = resolver_impl;
362
363 let mut runtime = AgentRuntime::new_with_execution_resolver(resolver)
364 .with_run_resolver(run_resolver)
365 .with_registry_handle(registry_handle);
366
367 #[cfg(feature = "a2a")]
368 if let Some(composite) = composite_registry {
369 runtime = runtime.with_composite_registry(composite);
370 }
371
372 if let Some(coordinator) = self.commit_coordinator {
373 runtime = runtime.with_commit_coordinator(coordinator);
374 }
375
376 if let Some(store) = self.profile_store {
377 runtime = runtime.with_profile_store(store);
378 }
379
380 Ok(runtime)
381 }
382
383 #[cfg(feature = "a2a")]
385 pub async fn build_and_discover(self) -> Result<AgentRuntime, BuildError> {
386 let runtime = self.build_unchecked()?;
387 if let Some(composite) = runtime.composite_registry() {
388 composite.discover().await?;
389 }
390 Ok(runtime)
391 }
392}
393
394impl Default for AgentRuntimeBuilder {
395 fn default() -> Self {
396 Self::new()
397 }
398}
399
400#[cfg(test)]
401#[path = "builder_tests.rs"]
402mod tests;