Skip to main content

harn_vm/skills/
runtime.rs

1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::rc::Rc;
4use std::sync::Arc;
5
6use crate::value::{VmError, VmValue};
7
8use super::{skill_entry_to_vm, substitute_skill_body, Skill, SubstitutionContext};
9
10pub type SkillFetcher = Arc<dyn Fn(&str) -> Result<Skill, String> + Send + Sync>;
11
12#[derive(Clone)]
13pub struct BoundSkillRegistry {
14    pub registry: VmValue,
15    pub fetcher: SkillFetcher,
16}
17
18pub struct LoadedSkill {
19    pub id: String,
20    pub entry: BTreeMap<String, VmValue>,
21    pub rendered_body: String,
22}
23
24thread_local! {
25    static CURRENT_SKILL_REGISTRY: RefCell<Option<BoundSkillRegistry>> = const { RefCell::new(None) };
26}
27
28pub fn install_current_skill_registry(
29    binding: Option<BoundSkillRegistry>,
30) -> Option<BoundSkillRegistry> {
31    CURRENT_SKILL_REGISTRY.with(|slot| slot.replace(binding))
32}
33
34pub fn current_skill_registry() -> Option<BoundSkillRegistry> {
35    CURRENT_SKILL_REGISTRY.with(|slot| slot.borrow().clone())
36}
37
38pub fn clear_current_skill_registry() {
39    CURRENT_SKILL_REGISTRY.with(|slot| {
40        *slot.borrow_mut() = None;
41    });
42}
43
44pub fn skill_entry_id(entry: &BTreeMap<String, VmValue>) -> String {
45    let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
46    let namespace = entry
47        .get("namespace")
48        .map(|v| v.display())
49        .filter(|value| !value.is_empty());
50    match namespace {
51        Some(ns) => format!("{ns}/{name}"),
52        None => name,
53    }
54}
55
56pub fn resolve_skill_entry(
57    registry: &VmValue,
58    target: &str,
59    builtin_name: &str,
60) -> Result<BTreeMap<String, VmValue>, String> {
61    let dict = registry
62        .as_dict()
63        .ok_or_else(|| format!("{builtin_name}: bound skill registry is not a dict"))?;
64    let skills = match dict.get("skills") {
65        Some(VmValue::List(list)) => list,
66        _ => {
67            return Err(format!("{builtin_name}: bound skill registry is malformed"));
68        }
69    };
70
71    let mut bare_matches: Vec<BTreeMap<String, VmValue>> = Vec::new();
72    for skill in skills.iter() {
73        let Some(entry) = skill.as_dict() else {
74            continue;
75        };
76        if skill_entry_id(entry) == target {
77            return Ok(entry.clone());
78        }
79        if entry
80            .get("name")
81            .map(|value| value.display())
82            .is_some_and(|name| name == target)
83        {
84            bare_matches.push(entry.clone());
85        }
86    }
87
88    match bare_matches.len() {
89        1 => Ok(bare_matches.remove(0)),
90        0 => Err(format!("skill '{target}' not found")),
91        _ => Err(format!(
92            "skill '{target}' is ambiguous; use the fully qualified id from the catalog"
93        )),
94    }
95}
96
97fn entry_has_inline_body(entry: &BTreeMap<String, VmValue>) -> bool {
98    entry
99        .get("body")
100        .map(|value| value.display())
101        .filter(|value| !value.is_empty())
102        .is_some()
103        || entry
104            .get("prompt")
105            .map(|value| value.display())
106            .filter(|value| !value.is_empty())
107            .is_some()
108}
109
110fn body_from_entry(entry: &BTreeMap<String, VmValue>) -> String {
111    entry
112        .get("body")
113        .map(|value| value.display())
114        .filter(|value| !value.is_empty())
115        .or_else(|| {
116            entry
117                .get("prompt")
118                .map(|value| value.display())
119                .filter(|value| !value.is_empty())
120        })
121        .unwrap_or_default()
122}
123
124fn hydrate_skill_entry(
125    entry: BTreeMap<String, VmValue>,
126    fetcher: Option<&SkillFetcher>,
127    builtin_name: &str,
128) -> Result<BTreeMap<String, VmValue>, String> {
129    if entry_has_inline_body(&entry) {
130        return Ok(entry);
131    }
132
133    let skill_id = skill_entry_id(&entry);
134    let Some(fetcher) = fetcher else {
135        return Err(format!(
136            "{builtin_name}: skill '{skill_id}' is not lazily loadable in this scope"
137        ));
138    };
139
140    let loaded = fetcher(&skill_id)?;
141    match skill_entry_to_vm(&loaded) {
142        VmValue::Dict(dict) => Ok((*dict).clone()),
143        _ => Err(format!(
144            "{builtin_name}: failed to hydrate skill '{skill_id}'"
145        )),
146    }
147}
148
149fn render_skill_entry(entry: &BTreeMap<String, VmValue>, session_id: Option<&str>) -> String {
150    let skill_dir = entry
151        .get("skill_dir")
152        .map(|value| value.display())
153        .filter(|value| !value.is_empty());
154    substitute_skill_body(
155        &body_from_entry(entry),
156        &SubstitutionContext {
157            arguments: Vec::new(),
158            skill_dir,
159            session_id: session_id.map(str::to_string),
160            extra_env: Default::default(),
161        },
162    )
163}
164
165pub fn load_bound_skill_by_name(
166    requested: &str,
167    session_id: Option<&str>,
168) -> Result<LoadedSkill, String> {
169    let Some(binding) = current_skill_registry() else {
170        return Err(
171            "load_skill: no skill registry is bound to this scope. Start the VM with discovered skills first."
172                .to_string(),
173        );
174    };
175    load_skill_from_registry(
176        &binding.registry,
177        Some(&binding.fetcher),
178        requested,
179        session_id,
180        "load_skill",
181    )
182}
183
184pub fn load_skill_from_registry(
185    registry: &VmValue,
186    fetcher: Option<&SkillFetcher>,
187    requested: &str,
188    session_id: Option<&str>,
189    builtin_name: &str,
190) -> Result<LoadedSkill, String> {
191    let entry = resolve_skill_entry(registry, requested, builtin_name)?;
192    let entry = hydrate_skill_entry(entry, fetcher, builtin_name)?;
193    let id = skill_entry_id(&entry);
194    let rendered_body = render_skill_entry(&entry, session_id);
195    Ok(LoadedSkill {
196        id,
197        entry,
198        rendered_body,
199    })
200}
201
202pub fn vm_error(message: impl Into<String>) -> VmError {
203    VmError::Thrown(VmValue::String(Rc::from(message.into())))
204}