1use super::*;
2use crate::contracts::runtime::tool_call::Tool;
3use crate::contracts::runtime::AgentBehavior;
4use crate::contracts::storage::ThreadStore;
5#[cfg(feature = "skills")]
6use crate::extensions::skills::{
7 CompositeSkillRegistry, InMemorySkillRegistry, Skill, SkillRegistry, SkillRegistryManagerError,
8};
9#[cfg(feature = "skills")]
10use crate::runtime::resolve::SkillsSystemWiring;
11use crate::runtime::StopPolicy;
12use crate::runtime::{AgentOs, RuntimeServices};
13use genai::Client;
14use std::collections::HashMap;
15use std::sync::Arc;
16#[cfg(feature = "skills")]
17use std::time::Duration;
18
19pub struct AgentOsBuilder {
20 pub(crate) client: Option<Client>,
21 pub(crate) bundles: Vec<Arc<dyn RegistryBundle>>,
22 pub(crate) agents: HashMap<String, AgentDefinition>,
23 pub(crate) agent_registries: Vec<Arc<dyn AgentRegistry>>,
24 pub(crate) resolved_agents: HashMap<String, ResolvedAgent>,
25 pub(crate) agent_catalogs: Vec<Arc<dyn AgentCatalog>>,
26 pub(crate) base_tools: HashMap<String, Arc<dyn Tool>>,
27 pub(crate) base_tool_registries: Vec<Arc<dyn ToolRegistry>>,
28 pub(crate) behaviors: HashMap<String, Arc<dyn AgentBehavior>>,
29 pub(crate) behavior_registries: Vec<Arc<dyn BehaviorRegistry>>,
30 pub(crate) stop_policies: HashMap<String, Arc<dyn StopPolicy>>,
31 pub(crate) stop_policy_registries: Vec<Arc<dyn StopPolicyRegistry>>,
32 pub(crate) providers: HashMap<String, Client>,
33 pub(crate) provider_registries: Vec<Arc<dyn ProviderRegistry>>,
34 pub(crate) models: HashMap<String, ModelDefinition>,
35 pub(crate) model_registries: Vec<Arc<dyn ModelRegistry>>,
36 #[cfg(feature = "skills")]
37 pub(crate) skills: Vec<Arc<dyn Skill>>,
38 #[cfg(feature = "skills")]
39 pub(crate) skill_registries: Vec<Arc<dyn SkillRegistry>>,
40 #[cfg(feature = "skills")]
41 pub(crate) skills_refresh_interval: Option<Duration>,
42 #[cfg(feature = "skills")]
43 pub(crate) skills_config: SkillsConfig,
44 pub(crate) system_wirings: Vec<Arc<dyn SystemWiring>>,
45 pub(crate) agent_tools: AgentToolsConfig,
46 pub(crate) agent_state_store: Option<Arc<dyn ThreadStore>>,
47}
48
49fn merge_registry<R: ?Sized, M>(
50 memory: M,
51 mut external: Vec<Arc<R>>,
52 is_memory_empty: impl Fn(&M) -> bool,
53 into_memory_registry: impl FnOnce(M) -> Arc<R>,
54 compose: impl FnOnce(Vec<Arc<R>>) -> Result<Arc<R>, AgentOsBuildError>,
55) -> Result<Arc<R>, AgentOsBuildError> {
56 if external.is_empty() {
57 return Ok(into_memory_registry(memory));
58 }
59
60 let mut registries: Vec<Arc<R>> = Vec::new();
61 if !is_memory_empty(&memory) {
62 registries.push(into_memory_registry(memory));
63 }
64 registries.append(&mut external);
65
66 if registries.len() == 1 {
67 Ok(registries.pop().expect("single registry must exist"))
68 } else {
69 compose(registries)
70 }
71}
72
73impl std::fmt::Debug for AgentOs {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 let mut s = f.debug_struct("AgentOs");
76 s.field("default_client", &"[genai::Client]")
77 .field("agents", &self.agents.len())
78 .field("agent_catalog", &self.agent_catalog.len())
79 .field("base_tools", &self.base_tools.len())
80 .field("behaviors", &self.behaviors.len())
81 .field("stop_policies", &self.stop_policies.len())
82 .field("providers", &self.providers.len())
83 .field("models", &self.models.len());
84 #[cfg(feature = "skills")]
85 s.field(
86 "skills",
87 &self.skills_registry.as_ref().map(|registry| registry.len()),
88 );
89 s.field("system_wirings", &self.system_wirings.len())
90 .field("active_runs", &"[internal]")
91 .field("agent_tools", &self.agent_tools)
92 .field("agent_state_store", &self.agent_state_store.is_some())
93 .finish()
94 }
95}
96
97impl std::fmt::Debug for AgentOsBuilder {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 let mut s = f.debug_struct("AgentOsBuilder");
100 s.field("client", &self.client.is_some())
101 .field("bundles", &self.bundles.len())
102 .field("agents", &self.agents.len())
103 .field("resolved_agents", &self.resolved_agents.len())
104 .field("agent_catalogs", &self.agent_catalogs.len())
105 .field("base_tools", &self.base_tools.len())
106 .field("behaviors", &self.behaviors.len())
107 .field("stop_policies", &self.stop_policies.len())
108 .field("stop_policy_registries", &self.stop_policy_registries.len())
109 .field("providers", &self.providers.len())
110 .field("models", &self.models.len());
111 #[cfg(feature = "skills")]
112 {
113 s.field("skills", &self.skills.len())
114 .field("skill_registries", &self.skill_registries.len())
115 .field("skills_refresh_interval", &self.skills_refresh_interval)
116 .field("skills_config", &self.skills_config);
117 }
118 s.field("system_wirings", &self.system_wirings.len())
119 .field("agent_tools", &self.agent_tools)
120 .field("agent_state_store", &self.agent_state_store.is_some())
121 .finish()
122 }
123}
124
125impl AgentOsBuilder {
126 fn insert_local_agent_definition(&mut self, agent_id: String, mut definition: AgentDefinition) {
127 definition.id = agent_id.clone();
128 self.agents.insert(agent_id, definition);
129 }
130
131 fn insert_resolved_agent_definition(&mut self, agent_id: String, mut agent: ResolvedAgent) {
132 agent.descriptor.id = agent_id.clone();
133 if agent.descriptor.name.trim().is_empty() {
134 agent.descriptor.name = agent_id.clone();
135 }
136 self.resolved_agents.insert(agent_id, agent);
137 }
138
139 fn insert_agent_spec(&mut self, spec: AgentDefinitionSpec) {
140 match spec {
141 AgentDefinitionSpec::Local(definition) => {
142 let definition = *definition;
143 let agent_id = definition.id.clone();
144 self.insert_local_agent_definition(agent_id, definition);
145 }
146 AgentDefinitionSpec::Remote(definition) => {
147 let agent_id = definition.id().to_string();
148 self.insert_resolved_agent_definition(agent_id, definition.into_resolved_agent());
149 }
150 }
151 }
152
153 pub fn new() -> Self {
154 Self {
155 client: None,
156 bundles: Vec::new(),
157 agents: HashMap::new(),
158 agent_registries: Vec::new(),
159 resolved_agents: HashMap::new(),
160 agent_catalogs: Vec::new(),
161 base_tools: HashMap::new(),
162 base_tool_registries: Vec::new(),
163 behaviors: HashMap::new(),
164 behavior_registries: Vec::new(),
165 stop_policies: HashMap::new(),
166 stop_policy_registries: Vec::new(),
167 providers: HashMap::new(),
168 provider_registries: Vec::new(),
169 models: HashMap::new(),
170 model_registries: Vec::new(),
171 #[cfg(feature = "skills")]
172 skills: Vec::new(),
173 #[cfg(feature = "skills")]
174 skill_registries: Vec::new(),
175 #[cfg(feature = "skills")]
176 skills_refresh_interval: None,
177 #[cfg(feature = "skills")]
178 skills_config: SkillsConfig::default(),
179 system_wirings: Vec::new(),
180 agent_tools: AgentToolsConfig::default(),
181 agent_state_store: None,
182 }
183 }
184
185 pub fn with_client(mut self, client: Client) -> Self {
186 self.client = Some(client);
187 self
188 }
189
190 pub fn with_bundle(mut self, bundle: Arc<dyn RegistryBundle>) -> Self {
191 self.bundles.push(bundle);
192 self
193 }
194
195 pub fn with_agent_spec(mut self, spec: AgentDefinitionSpec) -> Self {
196 self.insert_agent_spec(spec);
197 self
198 }
199
200 pub fn with_agent_specs(
201 mut self,
202 specs: impl IntoIterator<Item = AgentDefinitionSpec>,
203 ) -> Self {
204 for spec in specs {
205 self = self.with_agent_spec(spec);
206 }
207 self
208 }
209
210 pub fn with_agent_registry(mut self, registry: Arc<dyn AgentRegistry>) -> Self {
211 self.agent_registries.push(registry);
212 self
213 }
214
215 pub fn with_agent_catalog(mut self, catalog: Arc<dyn AgentCatalog>) -> Self {
216 self.agent_catalogs.push(catalog);
217 self
218 }
219
220 pub fn with_tools(mut self, tools: HashMap<String, Arc<dyn Tool>>) -> Self {
221 self.base_tools = tools;
222 self
223 }
224
225 pub fn with_tool_registry(mut self, registry: Arc<dyn ToolRegistry>) -> Self {
226 self.base_tool_registries.push(registry);
227 self
228 }
229
230 pub fn with_registered_behavior(
231 mut self,
232 behavior_id: impl Into<String>,
233 behavior: Arc<dyn AgentBehavior>,
234 ) -> Self {
235 self.behaviors.insert(behavior_id.into(), behavior);
236 self
237 }
238
239 pub fn with_behavior_registry(mut self, registry: Arc<dyn BehaviorRegistry>) -> Self {
240 self.behavior_registries.push(registry);
241 self
242 }
243
244 pub fn with_stop_policy(mut self, id: impl Into<String>, policy: Arc<dyn StopPolicy>) -> Self {
245 self.stop_policies.insert(id.into(), policy);
246 self
247 }
248
249 pub fn with_stop_policy_registry(mut self, registry: Arc<dyn StopPolicyRegistry>) -> Self {
250 self.stop_policy_registries.push(registry);
251 self
252 }
253
254 pub fn with_provider(mut self, provider_id: impl Into<String>, client: Client) -> Self {
255 self.providers.insert(provider_id.into(), client);
256 self
257 }
258
259 pub fn with_provider_registry(mut self, registry: Arc<dyn ProviderRegistry>) -> Self {
260 self.provider_registries.push(registry);
261 self
262 }
263
264 pub fn with_model(mut self, model_id: impl Into<String>, def: ModelDefinition) -> Self {
265 self.models.insert(model_id.into(), def);
266 self
267 }
268
269 pub fn with_models(mut self, defs: HashMap<String, ModelDefinition>) -> Self {
270 self.models = defs;
271 self
272 }
273
274 pub fn with_model_registry(mut self, registry: Arc<dyn ModelRegistry>) -> Self {
275 self.model_registries.push(registry);
276 self
277 }
278
279 #[cfg(feature = "skills")]
280 pub fn with_skills(mut self, skills: Vec<Arc<dyn Skill>>) -> Self {
281 self.skills = skills;
282 self
283 }
284
285 #[cfg(feature = "skills")]
286 pub fn with_skill_registry(mut self, registry: Arc<dyn SkillRegistry>) -> Self {
287 self.skill_registries.push(registry);
288 self
289 }
290
291 #[cfg(feature = "skills")]
292 pub fn with_skill_registry_refresh_interval(mut self, interval: Duration) -> Self {
293 self.skills_refresh_interval = Some(interval);
294 self
295 }
296
297 #[cfg(feature = "skills")]
298 pub fn with_skills_config(mut self, cfg: SkillsConfig) -> Self {
299 self.skills_config = cfg;
300 self
301 }
302
303 pub fn with_system_wiring(mut self, wiring: Arc<dyn SystemWiring>) -> Self {
305 self.system_wirings.push(wiring);
306 self
307 }
308
309 pub fn with_agent_tools_config(mut self, cfg: AgentToolsConfig) -> Self {
310 self.agent_tools = cfg;
311 self
312 }
313
314 pub fn with_agent_state_store(mut self, agent_state_store: Arc<dyn ThreadStore>) -> Self {
315 self.agent_state_store = Some(agent_state_store);
316 self
317 }
318
319 pub fn build(self) -> Result<AgentOs, AgentOsBuildError> {
320 let AgentOsBuilder {
321 client,
322 bundles,
323 agents: mut agents_defs,
324 mut agent_registries,
325 resolved_agents: mut resolved_agent_defs,
326 mut agent_catalogs,
327 base_tools: mut base_tools_defs,
328 mut base_tool_registries,
329 behaviors: mut behavior_defs,
330 mut behavior_registries,
331 stop_policies: stop_policy_defs,
332 stop_policy_registries,
333 providers: mut provider_defs,
334 mut provider_registries,
335 models: mut model_defs,
336 mut model_registries,
337 #[cfg(feature = "skills")]
338 skills,
339 #[cfg(feature = "skills")]
340 mut skill_registries,
341 #[cfg(feature = "skills")]
342 skills_refresh_interval,
343 #[cfg(feature = "skills")]
344 skills_config,
345 system_wirings,
346 agent_tools,
347 agent_state_store,
348 } = self;
349
350 BundleComposer::apply(
351 &bundles,
352 BundleRegistryAccumulator {
353 agent_definitions: &mut agents_defs,
354 agent_registries: &mut agent_registries,
355 tool_definitions: &mut base_tools_defs,
356 tool_registries: &mut base_tool_registries,
357 behavior_definitions: &mut behavior_defs,
358 behavior_registries: &mut behavior_registries,
359 provider_definitions: &mut provider_defs,
360 provider_registries: &mut provider_registries,
361 model_definitions: &mut model_defs,
362 model_registries: &mut model_registries,
363 },
364 )?;
365
366 #[allow(unused_mut)]
368 let mut system_wirings = system_wirings;
369 #[cfg(feature = "skills")]
370 let skills_registry = {
371 if skills_config.enabled && skills.is_empty() && skill_registries.is_empty() {
372 return Err(AgentOsBuildError::SkillsNotConfigured);
373 }
374
375 let mut in_memory_skills = InMemorySkillRegistry::new();
376 in_memory_skills.extend_upsert(skills);
377
378 let registry = if in_memory_skills.is_empty() && skill_registries.is_empty() {
379 None
380 } else {
381 Some(merge_registry(
382 in_memory_skills,
383 std::mem::take(&mut skill_registries),
384 |reg: &InMemorySkillRegistry| reg.is_empty(),
385 |reg| Arc::new(reg),
386 |regs| Ok(Arc::new(CompositeSkillRegistry::try_new(regs)?)),
387 )?)
388 };
389
390 if let (Some(r), Some(interval)) = (®istry, skills_refresh_interval) {
391 match r.start_periodic_refresh(interval) {
392 Ok(()) | Err(SkillRegistryManagerError::PeriodicRefreshAlreadyRunning) => {}
393 Err(err) => return Err(err.into()),
394 }
395 }
396
397 if skills_config.enabled {
399 if let Some(ref reg) = registry {
400 system_wirings.push(Arc::new(SkillsSystemWiring::new(
401 reg.clone(),
402 skills_config.clone(),
403 )));
404 }
405 }
406
407 registry
408 };
409
410 let mut base_tools = InMemoryToolRegistry::new();
411 base_tools.extend_named(base_tools_defs)?;
412
413 let base_tools: Arc<dyn ToolRegistry> = merge_registry(
414 base_tools,
415 base_tool_registries,
416 |reg: &InMemoryToolRegistry| reg.is_empty(),
417 |reg| Arc::new(reg),
418 |regs| Ok(Arc::new(CompositeToolRegistry::try_new(regs)?)),
419 )?;
420
421 let mut behaviors = InMemoryBehaviorRegistry::new();
422 behaviors.extend_named(behavior_defs)?;
423
424 let behaviors: Arc<dyn BehaviorRegistry> = merge_registry(
425 behaviors,
426 behavior_registries,
427 |reg: &InMemoryBehaviorRegistry| reg.is_empty(),
428 |reg| Arc::new(reg),
429 |regs| Ok(Arc::new(CompositeBehaviorRegistry::try_new(regs)?)),
430 )?;
431
432 let mut stop_policies_mem = InMemoryStopPolicyRegistry::new();
433 stop_policies_mem.extend_named(stop_policy_defs)?;
434
435 let stop_policies: Arc<dyn StopPolicyRegistry> = merge_registry(
436 stop_policies_mem,
437 stop_policy_registries,
438 |reg: &InMemoryStopPolicyRegistry| reg.is_empty(),
439 |reg| Arc::new(reg),
440 |regs| Ok(Arc::new(CompositeStopPolicyRegistry::try_new(regs)?)),
441 )?;
442
443 {
445 let reserved = AgentOs::reserved_behavior_ids(&system_wirings);
446 for (agent_id, def) in &agents_defs {
447 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
448 for id in &def.behavior_ids {
449 let id = id.trim();
450 if id.is_empty() {
451 return Err(AgentOsBuildError::AgentEmptyBehaviorRef {
452 agent_id: agent_id.clone(),
453 });
454 }
455 if reserved.contains(&id) {
456 return Err(AgentOsBuildError::AgentReservedBehaviorId {
457 agent_id: agent_id.clone(),
458 behavior_id: id.to_string(),
459 });
460 }
461 if !seen.insert(id.to_string()) {
462 return Err(AgentOsBuildError::AgentDuplicateBehaviorRef {
463 agent_id: agent_id.clone(),
464 behavior_id: id.to_string(),
465 });
466 }
467 if behaviors.get(id).is_none() {
468 return Err(AgentOsBuildError::AgentBehaviorNotFound {
469 agent_id: agent_id.clone(),
470 behavior_id: id.to_string(),
471 });
472 }
473 }
474
475 let mut sc_seen: std::collections::HashSet<String> =
477 std::collections::HashSet::new();
478 for sc_id in &def.stop_condition_ids {
479 let sc_id = sc_id.trim();
480 if sc_id.is_empty() {
481 return Err(AgentOsBuildError::AgentEmptyStopConditionRef {
482 agent_id: agent_id.clone(),
483 });
484 }
485 if !sc_seen.insert(sc_id.to_string()) {
486 return Err(AgentOsBuildError::AgentDuplicateStopConditionRef {
487 agent_id: agent_id.clone(),
488 stop_condition_id: sc_id.to_string(),
489 });
490 }
491 if stop_policies.get(sc_id).is_none() {
492 return Err(AgentOsBuildError::AgentStopConditionNotFound {
493 agent_id: agent_id.clone(),
494 stop_condition_id: sc_id.to_string(),
495 });
496 }
497 }
498 }
499 }
500
501 let mut providers = InMemoryProviderRegistry::new();
502 providers.extend(provider_defs)?;
503
504 let providers: Arc<dyn ProviderRegistry> = merge_registry(
505 providers,
506 provider_registries,
507 |reg: &InMemoryProviderRegistry| reg.is_empty(),
508 |reg| Arc::new(reg),
509 |regs| Ok(Arc::new(CompositeProviderRegistry::try_new(regs)?)),
510 )?;
511
512 let mut models = InMemoryModelRegistry::new();
513 models.extend(model_defs.clone())?;
514
515 let models: Arc<dyn ModelRegistry> = merge_registry(
516 models,
517 model_registries,
518 |reg: &InMemoryModelRegistry| reg.is_empty(),
519 |reg| Arc::new(reg),
520 |regs| Ok(Arc::new(CompositeModelRegistry::try_new(regs)?)),
521 )?;
522
523 if !models.is_empty() && providers.is_empty() {
524 return Err(AgentOsBuildError::ProvidersNotConfigured);
525 }
526
527 for (model_id, def) in models.snapshot() {
528 if providers.get(&def.provider).is_none() {
529 return Err(AgentOsBuildError::ProviderNotFound {
530 provider_id: def.provider,
531 model_id,
532 });
533 }
534 }
535
536 let mut agents = InMemoryAgentRegistry::new();
537 agents.extend_upsert(agents_defs);
538
539 let agents: Arc<dyn AgentRegistry> = merge_registry(
540 agents,
541 agent_registries,
542 |reg: &InMemoryAgentRegistry| reg.is_empty(),
543 |reg| Arc::new(reg),
544 |regs| Ok(Arc::new(CompositeAgentRegistry::try_new(regs)?)),
545 )?;
546
547 let mut static_agents = InMemoryAgentCatalog::new();
548 static_agents.extend_upsert(std::mem::take(&mut resolved_agent_defs));
549 agent_catalogs.insert(
550 0,
551 Arc::new(HostedAgentCatalog::new(agents.clone())) as Arc<dyn AgentCatalog>,
552 );
553 let agent_catalog: Arc<dyn AgentCatalog> = merge_registry(
554 static_agents,
555 agent_catalogs,
556 |catalog: &InMemoryAgentCatalog| catalog.is_empty(),
557 |catalog| Arc::new(catalog),
558 |catalogs| Ok(Arc::new(CompositeAgentCatalog::try_new(catalogs)?)),
559 )?;
560
561 let registries = RegistrySet::new(
562 agents,
563 base_tools,
564 behaviors,
565 providers,
566 models,
567 stop_policies,
568 #[cfg(feature = "skills")]
569 skills_registry,
570 );
571 let services = RuntimeServices {
572 default_client: client.unwrap_or_default(),
573 system_wirings,
574 agent_tools,
575 agent_state_store,
576 agent_catalog,
577 };
578
579 Ok(AgentOs::from_registry_set(registries, services))
580 }
581}
582
583impl Default for AgentOsBuilder {
584 fn default() -> Self {
585 Self::new()
586 }
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592
593 #[test]
594 fn with_agent_spec_routes_local_and_remote_into_runtime_surfaces() {
595 let mut local_definition = AgentDefinition::new("mock");
596 local_definition.id = "local-worker".to_string();
597 let os = AgentOsBuilder::new()
598 .with_agent_spec(AgentDefinitionSpec::local(
599 local_definition
600 .with_name("Local Worker")
601 .with_description("Hosted locally"),
602 ))
603 .with_agent_spec(AgentDefinitionSpec::a2a(
604 AgentDescriptor::new("remote-worker")
605 .with_name("Remote Worker")
606 .with_description("Delegated over A2A"),
607 A2aAgentBinding::new("https://example.test/v1/a2a", "remote-worker"),
608 ))
609 .build()
610 .expect("builder should accept unified agent specs");
611
612 assert_eq!(
613 os.agent("local-worker")
614 .expect("local agent should be registered")
615 .display_name(),
616 "Local Worker"
617 );
618
619 let remote = os
620 .agent_catalog()
621 .get("remote-worker")
622 .expect("remote agent should be discoverable");
623 assert_eq!(remote.descriptor.name, "Remote Worker");
624 assert!(matches!(remote.binding, AgentBinding::A2a(_)));
625 }
626
627 #[test]
628 fn unified_builder_entrypoints_normalize_ids_and_names() {
629 let os = AgentOsBuilder::new()
630 .with_agent_spec(AgentDefinitionSpec::local_with_id(
631 "local-worker",
632 AgentDefinition::new("mock")
633 .with_name("Local Worker")
634 .with_description("Hosted locally"),
635 ))
636 .with_agent_spec(AgentDefinitionSpec::a2a(
637 AgentDescriptor::new("remote-worker")
638 .with_name(" ")
639 .with_description("Delegated over A2A"),
640 A2aAgentBinding::new("https://example.test/v1/a2a", "remote-worker"),
641 ))
642 .build()
643 .expect("unified builder entrypoints should build");
644
645 assert_eq!(
646 os.agent("local-worker")
647 .expect("local agent should be registered")
648 .display_name(),
649 "Local Worker"
650 );
651
652 let remote = os
653 .agent_catalog()
654 .get("remote-worker")
655 .expect("remote agent should be discoverable");
656 assert_eq!(remote.descriptor.id, "remote-worker");
657 assert_eq!(remote.descriptor.name, "remote-worker");
658 assert_eq!(remote.descriptor.description, "Delegated over A2A");
659 }
660
661 #[test]
662 fn build_rejects_duplicate_local_and_remote_agent_ids() {
663 let err = AgentOsBuilder::new()
664 .with_agent_spec(AgentDefinitionSpec::local_with_id(
665 "worker",
666 AgentDefinition::new("mock"),
667 ))
668 .with_agent_spec(AgentDefinitionSpec::a2a_with_id(
669 "worker",
670 A2aAgentBinding::new("https://example.test/v1/a2a", "worker"),
671 ))
672 .build()
673 .expect_err("duplicate local/remote ids should be rejected");
674
675 assert!(matches!(
676 err,
677 AgentOsBuildError::AgentCatalog(AgentCatalogError::AgentIdConflict(id)) if id == "worker"
678 ));
679 }
680}