sim_lib_skill/
registry.rs1use std::{
2 collections::BTreeMap,
3 sync::{Arc, Mutex},
4};
5
6use sim_citizen_derive::non_citizen;
7use sim_kernel::{Cx, Error, Object, ObjectCompat, Result, Symbol, Value};
8
9#[cfg(feature = "openai")]
10use crate::SkillRole;
11#[cfg(any(feature = "cache", feature = "cassette"))]
12use crate::record::SkillAuditEntry;
13use crate::{SkillCallable, SkillCard, SkillTransport, SkillTransportValue};
14
15#[derive(Clone, Default)]
23#[non_citizen(
24 reason = "live skill registry handle; cards use skill/Card descriptor",
25 kind = "handle",
26 descriptor = "skill/Card"
27)]
28pub struct SkillRegistry {
29 state: Arc<Mutex<SkillRegistryState>>,
30}
31
32#[derive(Default)]
33struct SkillRegistryState {
34 transports: BTreeMap<String, Arc<dyn SkillTransport>>,
35 cards: BTreeMap<String, SkillCard>,
36 #[cfg(any(feature = "cache", feature = "cassette"))]
37 audit: Vec<SkillAuditEntry>,
38}
39
40impl SkillRegistry {
41 pub fn install_transport(&self, transport: Arc<dyn SkillTransport>) -> Result<()> {
44 self.state
45 .lock()
46 .map_err(|_| Error::PoisonedLock("skill registry"))?
47 .transports
48 .insert(transport.id().to_owned(), transport);
49 Ok(())
50 }
51
52 pub fn bind_card(&self, cx: &mut Cx, card: SkillCard) -> Result<Value> {
59 let transport = {
60 let state = self
61 .state
62 .lock()
63 .map_err(|_| Error::PoisonedLock("skill registry"))?;
64 state
65 .transports
66 .get(&card.transport_id)
67 .cloned()
68 .ok_or_else(|| {
69 Error::Eval(format!("missing skill transport {}", card.transport_id))
70 })?
71 };
72 #[cfg(any(feature = "cache", feature = "cassette"))]
73 let skill_callable = SkillCallable::new_bound(card.clone(), transport, self.clone());
74 #[cfg(not(any(feature = "cache", feature = "cassette")))]
75 let skill_callable = SkillCallable::new(card.clone(), transport);
76 let callable = cx.factory().opaque(Arc::new(skill_callable))?;
77 cx.registry_mut()
78 .register_function_value(card.symbol.clone(), callable.clone())?;
79 for alias in &card.aliases {
80 if alias != &card.symbol {
81 cx.registry_mut()
82 .register_function_value(alias.clone(), callable.clone())?;
83 }
84 }
85 #[cfg(feature = "openai")]
86 if card.roles.contains(&SkillRole::Tool) {
87 let alias = openai_tool_alias(&card.symbol);
88 if alias != card.symbol && !card.aliases.iter().any(|existing| existing == &alias) {
89 cx.registry_mut()
90 .register_function_value(alias, callable.clone())?;
91 }
92 }
93 crate::browse::publish_card_claims(cx, &card)?;
94 self.state
95 .lock()
96 .map_err(|_| Error::PoisonedLock("skill registry"))?
97 .cards
98 .insert(card.id.clone(), card);
99 Ok(callable)
100 }
101
102 pub fn cards(&self) -> Result<Vec<SkillCard>> {
104 Ok(self
105 .state
106 .lock()
107 .map_err(|_| Error::PoisonedLock("skill registry"))?
108 .cards
109 .values()
110 .cloned()
111 .collect())
112 }
113
114 pub fn card_by_id(&self, id: &str) -> Result<Option<SkillCard>> {
116 Ok(self
117 .state
118 .lock()
119 .map_err(|_| Error::PoisonedLock("skill registry"))?
120 .cards
121 .get(id)
122 .cloned())
123 }
124
125 pub fn card_by_symbol(&self, symbol: &Symbol) -> Result<Option<SkillCard>> {
127 Ok(self
128 .state
129 .lock()
130 .map_err(|_| Error::PoisonedLock("skill registry"))?
131 .cards
132 .values()
133 .find(|card| card_matches_symbol(card, symbol))
134 .cloned())
135 }
136
137 #[cfg(any(feature = "cache", feature = "cassette"))]
138 pub(crate) fn record_audit(&self, entry: SkillAuditEntry) -> Result<()> {
139 self.state
140 .lock()
141 .map_err(|_| Error::PoisonedLock("skill registry"))?
142 .audit
143 .push(entry);
144 Ok(())
145 }
146
147 #[cfg(any(feature = "cache", feature = "cassette"))]
148 pub(crate) fn audit_values(&self, cx: &mut Cx) -> Result<Value> {
149 let entries = self
150 .state
151 .lock()
152 .map_err(|_| Error::PoisonedLock("skill registry"))?
153 .audit
154 .clone();
155 let values = entries
156 .iter()
157 .map(|entry| entry.value(cx))
158 .collect::<Result<Vec<_>>>()?;
159 cx.factory().list(values)
160 }
161}
162
163impl Object for SkillRegistry {
164 fn display(&self, _cx: &mut Cx) -> Result<String> {
165 Ok("#<skill-registry>".to_owned())
166 }
167
168 fn as_any(&self) -> &dyn std::any::Any {
169 self
170 }
171}
172
173impl ObjectCompat for SkillRegistry {
174 fn as_table(&self, cx: &mut Cx) -> Result<Value> {
175 cx.factory().table(vec![
176 (
177 Symbol::new("kind"),
178 cx.factory().symbol(Symbol::new("skill/registry"))?,
179 ),
180 (
181 Symbol::new("cards"),
182 cx.factory().string(self.cards()?.len().to_string())?,
183 ),
184 ])
185 }
186}
187
188pub fn skill_registry_symbol() -> Symbol {
190 Symbol::qualified("skill", "registry")
191}
192
193pub fn skill_registry(cx: &mut Cx) -> Result<SkillRegistry> {
197 let value = cx.resolve_value(&skill_registry_symbol())?;
198 value
199 .object()
200 .downcast_ref::<SkillRegistry>()
201 .cloned()
202 .ok_or(Error::TypeMismatch {
203 expected: "skill registry",
204 found: "non-registry",
205 })
206}
207
208pub(crate) fn transport_from_value(value: &Value) -> Result<Arc<dyn SkillTransport>> {
209 value
210 .object()
211 .downcast_ref::<SkillTransportValue>()
212 .map(SkillTransportValue::transport)
213 .ok_or(Error::TypeMismatch {
214 expected: "skill transport",
215 found: "non-transport",
216 })
217}
218
219pub(crate) fn card_from_value(cx: &mut Cx, value: &Value) -> Result<SkillCard> {
220 if let Some(card) = value.object().downcast_ref::<SkillCard>() {
221 return Ok(card.clone());
222 }
223 let expr = value.object().as_expr(cx)?;
224 SkillCard::from_expr(&expr)
225}
226
227fn card_matches_symbol(card: &SkillCard, symbol: &Symbol) -> bool {
228 &card.symbol == symbol || card.aliases.iter().any(|alias| alias == symbol) || {
229 #[cfg(feature = "openai")]
230 {
231 card.roles.contains(&SkillRole::Tool) && openai_tool_alias(&card.symbol) == *symbol
232 }
233 #[cfg(not(feature = "openai"))]
234 {
235 false
236 }
237 }
238}
239
240#[cfg(feature = "openai")]
241fn openai_tool_alias(symbol: &Symbol) -> Symbol {
242 let name = sim_lib_surface_card::external_name(
243 symbol,
244 sim_lib_surface_card::ExternalNamePolicy::OpenAiTool,
245 );
246 let name = if name.is_empty() {
247 "skill".to_owned()
248 } else {
249 name
250 };
251 openai_name_to_symbol(&name)
252}
253
254#[cfg(feature = "openai")]
255fn openai_name_to_symbol(name: &str) -> Symbol {
256 if let Some((namespace, local)) = name.split_once('_') {
257 Symbol::qualified(namespace.replace('_', "-"), local.replace('_', "-"))
258 } else {
259 Symbol::new(name.replace('_', "-"))
260 }
261}