use std::{
collections::BTreeMap,
sync::{Arc, Mutex},
};
use sim_citizen_derive::non_citizen;
use sim_kernel::{Cx, Error, Object, ObjectCompat, Result, Symbol, Value};
#[cfg(feature = "openai")]
use crate::SkillRole;
#[cfg(any(feature = "cache", feature = "cassette"))]
use crate::record::SkillAuditEntry;
use crate::{SkillCallable, SkillCard, SkillTransport, SkillTransportValue};
#[derive(Clone, Default)]
#[non_citizen(
reason = "live skill registry handle; cards use skill/Card descriptor",
kind = "handle",
descriptor = "skill/Card"
)]
pub struct SkillRegistry {
state: Arc<Mutex<SkillRegistryState>>,
}
#[derive(Default)]
struct SkillRegistryState {
transports: BTreeMap<String, Arc<dyn SkillTransport>>,
cards: BTreeMap<String, SkillCard>,
#[cfg(any(feature = "cache", feature = "cassette"))]
audit: Vec<SkillAuditEntry>,
}
impl SkillRegistry {
pub fn install_transport(&self, transport: Arc<dyn SkillTransport>) -> Result<()> {
self.state
.lock()
.map_err(|_| Error::PoisonedLock("skill registry"))?
.transports
.insert(transport.id().to_owned(), transport);
Ok(())
}
pub fn bind_card(&self, cx: &mut Cx, card: SkillCard) -> Result<Value> {
let transport = {
let state = self
.state
.lock()
.map_err(|_| Error::PoisonedLock("skill registry"))?;
state
.transports
.get(&card.transport_id)
.cloned()
.ok_or_else(|| {
Error::Eval(format!("missing skill transport {}", card.transport_id))
})?
};
#[cfg(any(feature = "cache", feature = "cassette"))]
let skill_callable = SkillCallable::new_bound(card.clone(), transport, self.clone());
#[cfg(not(any(feature = "cache", feature = "cassette")))]
let skill_callable = SkillCallable::new(card.clone(), transport);
let callable = cx.factory().opaque(Arc::new(skill_callable))?;
cx.registry_mut()
.register_function_value(card.symbol.clone(), callable.clone())?;
for alias in &card.aliases {
if alias != &card.symbol {
cx.registry_mut()
.register_function_value(alias.clone(), callable.clone())?;
}
}
#[cfg(feature = "openai")]
if card.roles.contains(&SkillRole::Tool) {
let alias = openai_tool_alias(&card.symbol);
if alias != card.symbol && !card.aliases.iter().any(|existing| existing == &alias) {
cx.registry_mut()
.register_function_value(alias, callable.clone())?;
}
}
crate::browse::publish_card_claims(cx, &card)?;
self.state
.lock()
.map_err(|_| Error::PoisonedLock("skill registry"))?
.cards
.insert(card.id.clone(), card);
Ok(callable)
}
pub fn cards(&self) -> Result<Vec<SkillCard>> {
Ok(self
.state
.lock()
.map_err(|_| Error::PoisonedLock("skill registry"))?
.cards
.values()
.cloned()
.collect())
}
pub fn card_by_id(&self, id: &str) -> Result<Option<SkillCard>> {
Ok(self
.state
.lock()
.map_err(|_| Error::PoisonedLock("skill registry"))?
.cards
.get(id)
.cloned())
}
pub fn card_by_symbol(&self, symbol: &Symbol) -> Result<Option<SkillCard>> {
Ok(self
.state
.lock()
.map_err(|_| Error::PoisonedLock("skill registry"))?
.cards
.values()
.find(|card| card_matches_symbol(card, symbol))
.cloned())
}
#[cfg(any(feature = "cache", feature = "cassette"))]
pub(crate) fn record_audit(&self, entry: SkillAuditEntry) -> Result<()> {
self.state
.lock()
.map_err(|_| Error::PoisonedLock("skill registry"))?
.audit
.push(entry);
Ok(())
}
#[cfg(any(feature = "cache", feature = "cassette"))]
pub(crate) fn audit_values(&self, cx: &mut Cx) -> Result<Value> {
let entries = self
.state
.lock()
.map_err(|_| Error::PoisonedLock("skill registry"))?
.audit
.clone();
let values = entries
.iter()
.map(|entry| entry.value(cx))
.collect::<Result<Vec<_>>>()?;
cx.factory().list(values)
}
}
impl Object for SkillRegistry {
fn display(&self, _cx: &mut Cx) -> Result<String> {
Ok("#<skill-registry>".to_owned())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl ObjectCompat for SkillRegistry {
fn as_table(&self, cx: &mut Cx) -> Result<Value> {
cx.factory().table(vec![
(
Symbol::new("kind"),
cx.factory().symbol(Symbol::new("skill/registry"))?,
),
(
Symbol::new("cards"),
cx.factory().string(self.cards()?.len().to_string())?,
),
])
}
}
pub fn skill_registry_symbol() -> Symbol {
Symbol::qualified("skill", "registry")
}
pub fn skill_registry(cx: &mut Cx) -> Result<SkillRegistry> {
let value = cx.resolve_value(&skill_registry_symbol())?;
value
.object()
.downcast_ref::<SkillRegistry>()
.cloned()
.ok_or(Error::TypeMismatch {
expected: "skill registry",
found: "non-registry",
})
}
pub(crate) fn transport_from_value(value: &Value) -> Result<Arc<dyn SkillTransport>> {
value
.object()
.downcast_ref::<SkillTransportValue>()
.map(SkillTransportValue::transport)
.ok_or(Error::TypeMismatch {
expected: "skill transport",
found: "non-transport",
})
}
pub(crate) fn card_from_value(cx: &mut Cx, value: &Value) -> Result<SkillCard> {
if let Some(card) = value.object().downcast_ref::<SkillCard>() {
return Ok(card.clone());
}
let expr = value.object().as_expr(cx)?;
SkillCard::from_expr(&expr)
}
fn card_matches_symbol(card: &SkillCard, symbol: &Symbol) -> bool {
&card.symbol == symbol || card.aliases.iter().any(|alias| alias == symbol) || {
#[cfg(feature = "openai")]
{
card.roles.contains(&SkillRole::Tool) && openai_tool_alias(&card.symbol) == *symbol
}
#[cfg(not(feature = "openai"))]
{
false
}
}
}
#[cfg(feature = "openai")]
fn openai_tool_alias(symbol: &Symbol) -> Symbol {
let name = sim_lib_surface_card::external_name(
symbol,
sim_lib_surface_card::ExternalNamePolicy::OpenAiTool,
);
let name = if name.is_empty() {
"skill".to_owned()
} else {
name
};
openai_name_to_symbol(&name)
}
#[cfg(feature = "openai")]
fn openai_name_to_symbol(name: &str) -> Symbol {
if let Some((namespace, local)) = name.split_once('_') {
Symbol::qualified(namespace.replace('_', "-"), local.replace('_', "-"))
} else {
Symbol::new(name.replace('_', "-"))
}
}