Skip to main content

sim_lib_skill/
expr.rs

1use sim_kernel::{CapabilityName, Cx, Error, Expr, Result, ShapeRef, Symbol};
2use sim_shape::{parse_shape_expr, shape_value};
3
4use crate::{
5    SkillCacheMode, SkillCard, SkillCassetteMode, SkillPolicy, SkillPrivacyPolicy, SkillRole,
6};
7
8impl SkillRole {
9    /// Parses a role from a symbol or string [`Expr`], erroring on an unknown
10    /// or non-role expression.
11    pub fn from_expr(expr: &Expr) -> Result<Self> {
12        let name = match expr {
13            Expr::Symbol(symbol) => symbol.name.as_ref(),
14            Expr::String(text) => text.as_str(),
15            _ => {
16                return Err(Error::TypeMismatch {
17                    expected: "skill role",
18                    found: "non-role",
19                });
20            }
21        };
22        match name {
23            "tool" => Ok(Self::Tool),
24            "model" => Ok(Self::Model),
25            "resource" => Ok(Self::Resource),
26            "prompt" => Ok(Self::Prompt),
27            "memory" => Ok(Self::Memory),
28            "retriever" => Ok(Self::Retriever),
29            "judge" => Ok(Self::Judge),
30            "router" => Ok(Self::Router),
31            _ => Err(Error::Eval(format!("unknown skill role {name}"))),
32        }
33    }
34}
35
36impl SkillCard {
37    /// Encodes the card as a `skill/card` map [`Expr`], the inverse of
38    /// [`SkillCard::from_expr`].
39    pub fn to_expr(&self, cx: &mut Cx) -> Result<Expr> {
40        Ok(Expr::Map(vec![
41            field("kind", Expr::Symbol(Symbol::qualified("skill", "card"))),
42            field("id", Expr::String(self.id.clone())),
43            field("symbol", Expr::Symbol(self.symbol.clone())),
44            field(
45                "aliases",
46                Expr::List(self.aliases.iter().cloned().map(Expr::Symbol).collect()),
47            ),
48            field("origin", Expr::Symbol(self.origin.clone())),
49            field("title", Expr::String(self.title.clone())),
50            field("description", Expr::String(self.description.clone())),
51            field("input-shape", self.input_shape.object().as_expr(cx)?),
52            field("output-shape", self.output_shape.object().as_expr(cx)?),
53            field(
54                "roles",
55                Expr::List(
56                    self.roles
57                        .iter()
58                        .map(|role| Expr::Symbol(role.as_symbol()))
59                        .collect(),
60                ),
61            ),
62            field(
63                "capabilities",
64                Expr::List(
65                    self.capabilities
66                        .iter()
67                        .map(|capability| Expr::String(capability.as_str().to_owned()))
68                        .collect(),
69                ),
70            ),
71            field("policy", policy_expr(&self.policy)),
72            field(
73                "transport",
74                Expr::Map(vec![
75                    field("id", Expr::String(self.transport_id.clone())),
76                    field(
77                        "kind",
78                        Expr::Symbol(Symbol::new(self.transport_kind.clone())),
79                    ),
80                    field("operation", Expr::String(self.operation.clone())),
81                ]),
82            ),
83        ]))
84    }
85
86    /// Decodes a card from a `skill/card` map [`Expr`], the inverse of
87    /// [`SkillCard::to_expr`].
88    ///
89    /// Missing policy fields fall back to [`SkillPolicy::default`].
90    pub fn from_expr(expr: &Expr) -> Result<Self> {
91        let fields = map_fields(expr, "SkillCard")?;
92        expect_kind(fields)?;
93        let id = required_string(fields, "id")?;
94        let symbol = required_symbol(fields, "symbol")?;
95        let aliases = optional_list(fields, "aliases")
96            .unwrap_or(&[])
97            .iter()
98            .map(symbol_from_expr)
99            .collect::<Result<Vec<_>>>()?;
100        let origin = required_symbol(fields, "origin")?;
101        let title = required_string(fields, "title")?;
102        let description = required_string(fields, "description")?;
103        let input_shape_expr = required_field(fields, "input-shape")?;
104        let output_shape_expr = required_field(fields, "output-shape")?;
105        let roles = optional_list(fields, "roles")
106            .unwrap_or(&[])
107            .iter()
108            .map(SkillRole::from_expr)
109            .collect::<Result<Vec<_>>>()?;
110        let capabilities = optional_list(fields, "capabilities")
111            .unwrap_or(&[])
112            .iter()
113            .map(capability_from_expr)
114            .collect::<Result<Vec<_>>>()?;
115        let policy = match required_field(fields, "policy") {
116            Ok(expr) => policy_from_expr(expr)?,
117            Err(_) => SkillPolicy::default(),
118        };
119        let transport = map_fields(required_field(fields, "transport")?, "SkillCard transport")?;
120
121        Ok(Self {
122            id: id.clone(),
123            symbol: symbol.clone(),
124            aliases,
125            origin,
126            title,
127            description,
128            input_shape: shape_ref(
129                shape_symbol(
130                    Symbol::qualified(symbol.to_string(), "args"),
131                    input_shape_expr,
132                ),
133                input_shape_expr,
134            )?,
135            output_shape: shape_ref(
136                shape_symbol(
137                    Symbol::qualified(symbol.to_string(), "result"),
138                    output_shape_expr,
139                ),
140                output_shape_expr,
141            )?,
142            roles,
143            capabilities,
144            policy,
145            transport_id: required_string(transport, "id")?,
146            transport_kind: transport_kind(transport)?,
147            operation: required_string(transport, "operation")?,
148        })
149    }
150}
151
152use sim_value::build::entry as field;
153
154fn shape_ref(symbol: Symbol, expr: &Expr) -> Result<ShapeRef> {
155    let shape = parse_shape_expr(expr)?;
156    Ok(shape_value(symbol, shape))
157}
158
159fn shape_symbol(default: Symbol, expr: &Expr) -> Symbol {
160    match expr {
161        Expr::Symbol(symbol) => symbol.clone(),
162        _ => default,
163    }
164}
165
166fn expect_kind(fields: &[(Expr, Expr)]) -> Result<()> {
167    let kind = required_symbol(fields, "kind")?;
168    if kind == Symbol::qualified("skill", "card") {
169        Ok(())
170    } else {
171        Err(Error::TypeMismatch {
172            expected: "skill/card",
173            found: "other map",
174        })
175    }
176}
177
178use sim_value::access::map_entries as map_fields;
179
180fn required_field<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
181    sim_value::access::entry_field(fields, name)
182        .ok_or_else(|| Error::Eval(format!("SkillCard is missing field {name}")))
183}
184
185fn required_string(fields: &[(Expr, Expr)], name: &str) -> Result<String> {
186    match required_field(fields, name)? {
187        Expr::String(value) => Ok(value.clone()),
188        _ => Err(Error::TypeMismatch {
189            expected: "string",
190            found: "non-string",
191        }),
192    }
193}
194
195fn required_symbol(fields: &[(Expr, Expr)], name: &str) -> Result<Symbol> {
196    symbol_from_expr(required_field(fields, name)?)
197}
198
199fn symbol_from_expr(expr: &Expr) -> Result<Symbol> {
200    match expr {
201        Expr::Symbol(symbol) => Ok(symbol.clone()),
202        Expr::String(text) => Ok(parse_symbol_text(text)),
203        _ => Err(Error::TypeMismatch {
204            expected: "symbol",
205            found: "non-symbol",
206        }),
207    }
208}
209
210fn optional_list<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Option<&'a [Expr]> {
211    match required_field(fields, name).ok()? {
212        Expr::List(items) => Some(items),
213        _ => None,
214    }
215}
216
217fn capability_from_expr(expr: &Expr) -> Result<CapabilityName> {
218    match expr {
219        Expr::String(text) => Ok(CapabilityName::new(text.clone())),
220        Expr::Symbol(symbol) if symbol.namespace.as_deref() == Some("capability") => {
221            Ok(CapabilityName::new(symbol.name.to_string()))
222        }
223        Expr::Symbol(symbol) => Ok(CapabilityName::new(symbol.to_string())),
224        _ => Err(Error::TypeMismatch {
225            expected: "capability",
226            found: "non-capability",
227        }),
228    }
229}
230
231fn policy_expr(policy: &SkillPolicy) -> Expr {
232    let mut fields = vec![
233        field("privacy", Expr::Symbol(policy.privacy.as_symbol())),
234        field("cache", Expr::Symbol(policy.cache.as_symbol())),
235        field("cassette", Expr::Symbol(policy.cassette.as_symbol())),
236        field("idempotent", Expr::Bool(policy.idempotent)),
237    ];
238    if let Some(semantic_key) = &policy.semantic_key {
239        fields.push(field("semantic-key", Expr::String(semantic_key.clone())));
240    }
241    Expr::Map(fields)
242}
243
244fn policy_from_expr(expr: &Expr) -> Result<SkillPolicy> {
245    let fields = map_fields(expr, "SkillCard policy")?;
246    Ok(SkillPolicy {
247        privacy: optional_field(fields, "privacy")
248            .map(privacy_from_expr)
249            .transpose()?
250            .unwrap_or(SkillPrivacyPolicy::NoRaw),
251        cache: optional_field(fields, "cache")
252            .map(cache_mode_from_expr)
253            .transpose()?
254            .unwrap_or(SkillCacheMode::Disabled),
255        cassette: optional_field(fields, "cassette")
256            .map(cassette_mode_from_expr)
257            .transpose()?
258            .unwrap_or(SkillCassetteMode::Disabled),
259        idempotent: optional_field(fields, "idempotent")
260            .map(bool_from_expr)
261            .transpose()?
262            .unwrap_or(false),
263        semantic_key: optional_field(fields, "semantic-key")
264            .map(stringish_from_expr)
265            .transpose()?,
266    })
267}
268
269fn privacy_from_expr(expr: &Expr) -> Result<SkillPrivacyPolicy> {
270    match symbol_name(expr)?.as_str() {
271        "metadata-only" => Ok(SkillPrivacyPolicy::MetadataOnly),
272        "no-raw" => Ok(SkillPrivacyPolicy::NoRaw),
273        "local-only" => Ok(SkillPrivacyPolicy::LocalOnly),
274        "allow-raw" => Ok(SkillPrivacyPolicy::AllowRaw),
275        other => Err(Error::Eval(format!("unknown skill privacy policy {other}"))),
276    }
277}
278
279fn cache_mode_from_expr(expr: &Expr) -> Result<SkillCacheMode> {
280    match symbol_name(expr)?.as_str() {
281        "disabled" => Ok(SkillCacheMode::Disabled),
282        "read-through" => Ok(SkillCacheMode::ReadThrough),
283        "read-only" => Ok(SkillCacheMode::ReadOnly),
284        "write-only" => Ok(SkillCacheMode::WriteOnly),
285        "refresh" => Ok(SkillCacheMode::Refresh),
286        other => Err(Error::Eval(format!("unknown skill cache mode {other}"))),
287    }
288}
289
290fn cassette_mode_from_expr(expr: &Expr) -> Result<SkillCassetteMode> {
291    match symbol_name(expr)?.as_str() {
292        "disabled" => Ok(SkillCassetteMode::Disabled),
293        "record-replay" => Ok(SkillCassetteMode::RecordReplay),
294        "replay-only" => Ok(SkillCassetteMode::ReplayOnly),
295        "record-only" => Ok(SkillCassetteMode::RecordOnly),
296        other => Err(Error::Eval(format!("unknown skill cassette mode {other}"))),
297    }
298}
299
300fn bool_from_expr(expr: &Expr) -> Result<bool> {
301    match expr {
302        Expr::Bool(value) => Ok(*value),
303        _ => Err(Error::TypeMismatch {
304            expected: "bool",
305            found: "non-bool",
306        }),
307    }
308}
309
310fn symbol_name(expr: &Expr) -> Result<String> {
311    match expr {
312        Expr::Symbol(symbol) => Ok(symbol.name.to_string()),
313        Expr::String(text) => Ok(text.clone()),
314        _ => Err(Error::TypeMismatch {
315            expected: "symbol or string",
316            found: "invalid policy value",
317        }),
318    }
319}
320
321fn stringish_from_expr(expr: &Expr) -> Result<String> {
322    match expr {
323        Expr::String(text) => Ok(text.clone()),
324        Expr::Symbol(symbol) => Ok(symbol.to_string()),
325        _ => Err(Error::TypeMismatch {
326            expected: "string",
327            found: "non-string",
328        }),
329    }
330}
331
332fn optional_field<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
333    let key = Symbol::new(name.to_owned());
334    fields
335        .iter()
336        .find_map(|(candidate, value)| match candidate {
337            Expr::Symbol(symbol) if symbol == &key => Some(value),
338            _ => None,
339        })
340}
341
342fn transport_kind(fields: &[(Expr, Expr)]) -> Result<String> {
343    match required_field(fields, "kind")? {
344        Expr::String(value) => Ok(value.clone()),
345        Expr::Symbol(symbol) if symbol.namespace.is_none() => Ok(symbol.name.to_string()),
346        Expr::Symbol(symbol) => Ok(symbol.to_string()),
347        _ => Err(Error::TypeMismatch {
348            expected: "transport kind",
349            found: "invalid transport kind",
350        }),
351    }
352}
353
354fn parse_symbol_text(text: &str) -> Symbol {
355    match text.split_once('/') {
356        Some((namespace, name)) if !namespace.is_empty() && !name.is_empty() => {
357            Symbol::qualified(namespace.to_owned(), name.to_owned())
358        }
359        _ => Symbol::new(text.to_owned()),
360    }
361}