1use std::collections::HashSet;
2use std::fmt;
3
4use awaken_contract::registry_spec::AgentSpec;
5use serde::Serialize;
6
7use super::traits::RegistrySet;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub enum RegistryDiagnostic {
11 AgentMissingModel {
12 agent_id: String,
13 model_id: String,
14 },
15 ModelMissingProvider {
16 model_id: String,
17 provider_id: String,
18 },
19 AgentMissingPlugin {
20 agent_id: String,
21 plugin_id: String,
22 },
23 AgentMissingDelegate {
24 agent_id: String,
25 delegate_id: String,
26 },
27 AgentHookFilterPluginNotLoaded {
28 agent_id: String,
29 plugin_id: String,
30 },
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
34#[serde(rename_all = "snake_case")]
35pub enum RegistryDiagnosticSeverity {
36 Error,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
40pub struct RegistryResourceRef {
41 pub namespace: &'static str,
42 pub id: String,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
46pub struct SerializableRegistryDiagnostic {
47 pub code: &'static str,
48 pub severity: RegistryDiagnosticSeverity,
49 pub resource: RegistryResourceRef,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub depends_on: Option<RegistryResourceRef>,
52 pub message: String,
53}
54
55impl RegistryDiagnostic {
56 pub fn code(&self) -> &'static str {
57 match self {
58 Self::AgentMissingModel { .. } => "agent_missing_model",
59 Self::ModelMissingProvider { .. } => "model_missing_provider",
60 Self::AgentMissingPlugin { .. } => "agent_missing_plugin",
61 Self::AgentMissingDelegate { .. } => "agent_missing_delegate",
62 Self::AgentHookFilterPluginNotLoaded { .. } => "agent_hook_filter_plugin_not_loaded",
63 }
64 }
65
66 pub fn resource(&self) -> RegistryResourceRef {
67 match self {
68 Self::AgentMissingModel { agent_id, .. }
69 | Self::AgentMissingPlugin { agent_id, .. }
70 | Self::AgentMissingDelegate { agent_id, .. }
71 | Self::AgentHookFilterPluginNotLoaded { agent_id, .. } => RegistryResourceRef {
72 namespace: "agents",
73 id: agent_id.clone(),
74 },
75 Self::ModelMissingProvider { model_id, .. } => RegistryResourceRef {
76 namespace: "models",
77 id: model_id.clone(),
78 },
79 }
80 }
81
82 pub fn depends_on(&self) -> Option<RegistryResourceRef> {
83 match self {
84 Self::AgentMissingModel { model_id, .. } => Some(RegistryResourceRef {
85 namespace: "models",
86 id: model_id.clone(),
87 }),
88 Self::ModelMissingProvider { provider_id, .. } => Some(RegistryResourceRef {
89 namespace: "providers",
90 id: provider_id.clone(),
91 }),
92 Self::AgentMissingPlugin { plugin_id, .. }
93 | Self::AgentHookFilterPluginNotLoaded { plugin_id, .. } => Some(RegistryResourceRef {
94 namespace: "plugins",
95 id: plugin_id.clone(),
96 }),
97 Self::AgentMissingDelegate { delegate_id, .. } => Some(RegistryResourceRef {
98 namespace: "agents",
99 id: delegate_id.clone(),
100 }),
101 }
102 }
103
104 pub fn to_serializable(&self) -> SerializableRegistryDiagnostic {
105 SerializableRegistryDiagnostic {
106 code: self.code(),
107 severity: RegistryDiagnosticSeverity::Error,
108 resource: self.resource(),
109 depends_on: self.depends_on(),
110 message: self.to_string(),
111 }
112 }
113}
114
115impl fmt::Display for RegistryDiagnostic {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 match self {
118 Self::AgentMissingModel { agent_id, model_id } => {
119 write!(
120 f,
121 "agent '{agent_id}' uses missing model binding '{model_id}'"
122 )
123 }
124 Self::ModelMissingProvider {
125 model_id,
126 provider_id,
127 } => write!(
128 f,
129 "model binding '{model_id}' points to missing provider '{provider_id}'"
130 ),
131 Self::AgentMissingPlugin {
132 agent_id,
133 plugin_id,
134 } => write!(f, "agent '{agent_id}' uses missing plugin '{plugin_id}'"),
135 Self::AgentMissingDelegate {
136 agent_id,
137 delegate_id,
138 } => write!(
139 f,
140 "agent '{agent_id}' delegates to missing agent '{delegate_id}'"
141 ),
142 Self::AgentHookFilterPluginNotLoaded {
143 agent_id,
144 plugin_id,
145 } => write!(
146 f,
147 "agent '{agent_id}' active hook filter references unloaded plugin '{plugin_id}'"
148 ),
149 }
150 }
151}
152
153#[derive(Debug, thiserror::Error)]
154#[error("registry validation failed: {message}")]
155pub struct RegistryValidationError {
156 diagnostics: Vec<RegistryDiagnostic>,
157 message: String,
158}
159
160impl RegistryValidationError {
161 pub fn from_diagnostics(diagnostics: Vec<RegistryDiagnostic>) -> Self {
162 let diagnostics = dedup_diagnostics(diagnostics);
163 let message = diagnostics
164 .iter()
165 .map(ToString::to_string)
166 .collect::<Vec<_>>()
167 .join("; ");
168 Self {
169 diagnostics,
170 message,
171 }
172 }
173
174 pub fn diagnostics(&self) -> &[RegistryDiagnostic] {
175 &self.diagnostics
176 }
177}
178
179pub fn diagnose_registry_set(registries: &RegistrySet) -> Vec<RegistryDiagnostic> {
180 let mut diagnostics = Vec::new();
181
182 for model_id in registries.models.model_ids() {
183 let Some(binding) = registries.models.get_model(&model_id) else {
184 continue;
185 };
186 if registries
187 .providers
188 .get_provider(&binding.provider_id)
189 .is_none()
190 {
191 diagnostics.push(RegistryDiagnostic::ModelMissingProvider {
192 model_id,
193 provider_id: binding.provider_id,
194 });
195 }
196 }
197
198 for agent_id in registries.agents.agent_ids() {
199 let Some(spec) = registries.agents.get_agent(&agent_id) else {
200 continue;
201 };
202 diagnostics.extend(diagnose_agent_spec(registries, &spec));
203 }
204
205 diagnostics
206}
207
208pub fn diagnose_registry_set_serializable(
209 registries: &RegistrySet,
210) -> Vec<SerializableRegistryDiagnostic> {
211 diagnose_registry_set(registries)
212 .into_iter()
213 .map(|diagnostic| diagnostic.to_serializable())
214 .collect()
215}
216
217pub fn validate_registry_set(registries: &RegistrySet) -> Result<(), RegistryValidationError> {
218 let diagnostics = diagnose_registry_set(registries);
219 if diagnostics.is_empty() {
220 Ok(())
221 } else {
222 Err(RegistryValidationError::from_diagnostics(diagnostics))
223 }
224}
225
226pub fn diagnose_agent_spec(registries: &RegistrySet, spec: &AgentSpec) -> Vec<RegistryDiagnostic> {
227 let mut diagnostics = Vec::new();
228 let agent_id = spec.id.clone();
229
230 if spec.endpoint.is_none() {
231 match registries.models.get_model(&spec.model_id) {
232 Some(binding) => {
233 if registries
234 .providers
235 .get_provider(&binding.provider_id)
236 .is_none()
237 {
238 diagnostics.push(RegistryDiagnostic::ModelMissingProvider {
239 model_id: spec.model_id.clone(),
240 provider_id: binding.provider_id,
241 });
242 }
243 }
244 None => diagnostics.push(RegistryDiagnostic::AgentMissingModel {
245 agent_id: agent_id.clone(),
246 model_id: spec.model_id.clone(),
247 }),
248 }
249 }
250
251 for plugin_id in &spec.plugin_ids {
252 if registries.plugins.get_plugin(plugin_id).is_none() {
253 diagnostics.push(RegistryDiagnostic::AgentMissingPlugin {
254 agent_id: agent_id.clone(),
255 plugin_id: plugin_id.clone(),
256 });
257 }
258 }
259
260 let loaded_plugins: HashSet<_> = spec.plugin_ids.iter().collect();
261 for plugin_id in &spec.active_hook_filter {
262 if !loaded_plugins.contains(plugin_id) {
263 diagnostics.push(RegistryDiagnostic::AgentHookFilterPluginNotLoaded {
264 agent_id: agent_id.clone(),
265 plugin_id: plugin_id.clone(),
266 });
267 }
268 }
269
270 let known_agents: HashSet<_> = registries.agents.agent_ids().into_iter().collect();
271 for delegate_id in &spec.delegates {
272 if !known_agents.contains(delegate_id) {
273 diagnostics.push(RegistryDiagnostic::AgentMissingDelegate {
274 agent_id: agent_id.clone(),
275 delegate_id: delegate_id.clone(),
276 });
277 }
278 }
279
280 diagnostics
281}
282
283fn dedup_diagnostics(diagnostics: Vec<RegistryDiagnostic>) -> Vec<RegistryDiagnostic> {
284 let mut seen = HashSet::new();
285 diagnostics
286 .into_iter()
287 .filter(|diagnostic| seen.insert(diagnostic.clone()))
288 .collect()
289}
290
291pub fn validate_agent_spec(
292 registries: &RegistrySet,
293 spec: &AgentSpec,
294) -> Result<(), RegistryValidationError> {
295 let diagnostics = diagnose_agent_spec(registries, spec);
296 if diagnostics.is_empty() {
297 Ok(())
298 } else {
299 Err(RegistryValidationError::from_diagnostics(diagnostics))
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use crate::registry::memory::{
307 MapAgentSpecRegistry, MapModelRegistry, MapPluginSource, MapProviderRegistry,
308 MapToolRegistry,
309 };
310 use crate::registry::traits::ModelBinding;
311 use std::sync::Arc;
312
313 fn empty_registry_set() -> RegistrySet {
314 RegistrySet {
315 agents: Arc::new(MapAgentSpecRegistry::new()),
316 tools: Arc::new(MapToolRegistry::new()),
317 models: Arc::new(MapModelRegistry::new()),
318 providers: Arc::new(MapProviderRegistry::new()),
319 plugins: Arc::new(MapPluginSource::new()),
320 #[cfg(feature = "a2a")]
321 backends: Arc::new(crate::registry::memory::MapBackendRegistry::new()),
322 }
323 }
324
325 #[test]
326 fn diagnose_agent_spec_reports_missing_model() {
327 let registries = empty_registry_set();
328 let spec = AgentSpec {
329 id: "agent".into(),
330 model_id: "missing".into(),
331 system_prompt: "s".into(),
332 ..Default::default()
333 };
334
335 let diagnostics = diagnose_agent_spec(®istries, &spec);
336 assert_eq!(
337 diagnostics,
338 vec![RegistryDiagnostic::AgentMissingModel {
339 agent_id: "agent".into(),
340 model_id: "missing".into(),
341 }]
342 );
343 }
344
345 #[test]
346 fn diagnose_registry_set_reports_model_missing_provider() {
347 let mut models = MapModelRegistry::new();
348 models
349 .register_model(
350 "m",
351 ModelBinding {
352 provider_id: "missing-provider".into(),
353 upstream_model: "upstream".into(),
354 },
355 )
356 .unwrap();
357 let registries = RegistrySet {
358 models: Arc::new(models),
359 ..empty_registry_set()
360 };
361
362 let diagnostics = diagnose_registry_set(®istries);
363 assert_eq!(
364 diagnostics,
365 vec![RegistryDiagnostic::ModelMissingProvider {
366 model_id: "m".into(),
367 provider_id: "missing-provider".into(),
368 }]
369 );
370 }
371
372 #[test]
373 fn diagnose_registry_set_dedups_model_missing_provider() {
374 let mut models = MapModelRegistry::new();
375 models
376 .register_model(
377 "m",
378 ModelBinding {
379 provider_id: "missing-provider".into(),
380 upstream_model: "upstream".into(),
381 },
382 )
383 .unwrap();
384 let mut agents = MapAgentSpecRegistry::new();
385 agents
386 .register_spec(AgentSpec {
387 id: "a".into(),
388 model_id: "m".into(),
389 system_prompt: "s".into(),
390 ..Default::default()
391 })
392 .unwrap();
393 let registries = RegistrySet {
394 agents: Arc::new(agents),
395 models: Arc::new(models),
396 ..empty_registry_set()
397 };
398
399 let error = validate_registry_set(®istries).expect_err("registry must be invalid");
400 assert_eq!(
401 error.diagnostics(),
402 &[RegistryDiagnostic::ModelMissingProvider {
403 model_id: "m".into(),
404 provider_id: "missing-provider".into(),
405 }]
406 );
407 }
408
409 #[test]
410 fn diagnose_agent_spec_reports_unloaded_active_hook_filter_plugin() {
411 let registries = empty_registry_set();
412 let spec = AgentSpec {
413 id: "agent".into(),
414 model_id: "m".into(),
415 system_prompt: "s".into(),
416 active_hook_filter: ["missing-plugin".to_string()].into_iter().collect(),
417 ..Default::default()
418 };
419
420 let diagnostics = diagnose_agent_spec(®istries, &spec);
421 assert!(
422 diagnostics.contains(&RegistryDiagnostic::AgentHookFilterPluginNotLoaded {
423 agent_id: "agent".into(),
424 plugin_id: "missing-plugin".into(),
425 })
426 );
427 }
428
429 #[test]
430 fn serializable_diagnostic_has_stable_code_resource_and_dependency() {
431 let diagnostic = RegistryDiagnostic::ModelMissingProvider {
432 model_id: "m".into(),
433 provider_id: "p".into(),
434 }
435 .to_serializable();
436
437 assert_eq!(diagnostic.code, "model_missing_provider");
438 assert_eq!(diagnostic.severity, RegistryDiagnosticSeverity::Error);
439 assert_eq!(diagnostic.resource.namespace, "models");
440 assert_eq!(diagnostic.resource.id, "m");
441 assert_eq!(
442 diagnostic.depends_on,
443 Some(RegistryResourceRef {
444 namespace: "providers",
445 id: "p".into(),
446 })
447 );
448 assert!(diagnostic.message.contains("missing provider"));
449 }
450}