Skip to main content

turul_a2a_patterns/
registry.rs

1//! `SkillRegistry` trait, `SkillDescriptor`, and the default in-memory
2//! implementation. Registry entries enforce a single-source-of-truth
3//! invariant on `params_schema`: for manifest-backed skills it is derived
4//! from the manifest's input schema; for programmatic skills it is supplied
5//! once at registration time. There is no second authoritative surface.
6
7use std::collections::BTreeMap;
8use std::sync::{Arc, Mutex};
9
10use async_trait::async_trait;
11use serde_json::Value;
12
13use turul_a2a_proto::AgentSkill;
14
15use crate::error::SkillError;
16use crate::handler::SkillHandler;
17use crate::manifest::SkillCard;
18
19/// Returned by `SkillRegistry::describe`.
20///
21/// `params_schema` is **Turul-local runtime-planning metadata** — never on
22/// the wire. Single-source-of-truth invariant: for manifest-backed skills
23/// it is derived from the manifest input schema; for programmatic skills
24/// it is supplied once at registration. There is no second authoritative
25/// surface.
26#[derive(Debug, Clone)]
27#[non_exhaustive]
28pub struct SkillDescriptor {
29    pub id: String,
30    pub agent_skill: AgentSkill,
31    pub params_schema: Option<Value>,
32}
33
34/// Registry mapping `AgentSkill.id` to a registered `SkillHandler`.
35#[async_trait]
36pub trait SkillRegistry: Send + Sync {
37    /// Register a manifest-backed skill. The `params_schema` field of the
38    /// returned descriptor is derived from `card.input_schema` (single
39    /// source of truth).
40    async fn register_manifest(
41        &self,
42        card: SkillCard,
43        handler: Arc<dyn SkillHandler>,
44    ) -> Result<(), SkillError>;
45
46    /// Register a programmatic skill with an optional one-shot schema.
47    /// The supplied schema becomes `params_schema`; there is no second
48    /// surface that can override it.
49    async fn register_programmatic(
50        &self,
51        agent_skill: AgentSkill,
52        params_schema: Option<Value>,
53        handler: Arc<dyn SkillHandler>,
54    ) -> Result<(), SkillError>;
55
56    /// Describe a registered skill.
57    async fn describe(&self, id: &str) -> Option<SkillDescriptor>;
58
59    /// List all registered descriptors.
60    async fn list(&self) -> Vec<SkillDescriptor>;
61
62    /// Look up the handler for `id`.
63    async fn handler(&self, id: &str) -> Option<Arc<dyn SkillHandler>>;
64}
65
66struct Registration {
67    descriptor: SkillDescriptor,
68    handler: Arc<dyn SkillHandler>,
69}
70
71/// Default in-memory `SkillRegistry`.
72///
73/// An empty skill id is rejected with `SkillError::InvalidRequest`.
74/// Registering an id that is already present replaces the previous
75/// descriptor and handler. `list` returns descriptors in ascending id
76/// order.
77#[derive(Default)]
78pub struct InMemorySkillRegistry {
79    inner: Mutex<BTreeMap<String, Registration>>,
80}
81
82impl InMemorySkillRegistry {
83    pub fn new() -> Self {
84        Self::default()
85    }
86}
87
88#[async_trait]
89impl SkillRegistry for InMemorySkillRegistry {
90    async fn register_manifest(
91        &self,
92        card: SkillCard,
93        handler: Arc<dyn SkillHandler>,
94    ) -> Result<(), SkillError> {
95        // Single source of truth: params_schema IS the manifest's
96        // input_schema, by construction.
97        let agent_skill = card.to_agent_skill();
98        let descriptor = SkillDescriptor {
99            id: card.id.clone(),
100            agent_skill,
101            params_schema: card.input_schema.clone(),
102        };
103        self.insert(descriptor, handler)
104    }
105
106    async fn register_programmatic(
107        &self,
108        agent_skill: AgentSkill,
109        params_schema: Option<Value>,
110        handler: Arc<dyn SkillHandler>,
111    ) -> Result<(), SkillError> {
112        let descriptor = SkillDescriptor {
113            id: agent_skill.id.clone(),
114            agent_skill,
115            params_schema,
116        };
117        self.insert(descriptor, handler)
118    }
119
120    async fn describe(&self, id: &str) -> Option<SkillDescriptor> {
121        let guard = self.inner.lock().ok()?;
122        guard.get(id).map(|r| r.descriptor.clone())
123    }
124
125    async fn list(&self) -> Vec<SkillDescriptor> {
126        match self.inner.lock() {
127            Ok(g) => g.values().map(|r| r.descriptor.clone()).collect(),
128            Err(_) => Vec::new(),
129        }
130    }
131
132    async fn handler(&self, id: &str) -> Option<Arc<dyn SkillHandler>> {
133        let guard = self.inner.lock().ok()?;
134        guard.get(id).map(|r| r.handler.clone())
135    }
136}
137
138impl InMemorySkillRegistry {
139    fn insert(
140        &self,
141        descriptor: SkillDescriptor,
142        handler: Arc<dyn SkillHandler>,
143    ) -> Result<(), SkillError> {
144        let mut guard = self
145            .inner
146            .lock()
147            .map_err(|e| SkillError::Internal(format!("registry mutex poisoned: {e}")))?;
148        if descriptor.id.is_empty() {
149            return Err(SkillError::InvalidRequest(
150                "skill id must not be empty".to_string(),
151            ));
152        }
153        guard.insert(
154            descriptor.id.clone(),
155            Registration {
156                descriptor,
157                handler,
158            },
159        );
160        Ok(())
161    }
162}