Skip to main content

awaken_runtime/registry/
diagnostics.rs

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(&registries, &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(&registries);
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(&registries).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(&registries, &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}