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    /// An absent policy block falls back to [`SkillPolicy::default`]. A present
90    /// policy block rejects unknown keys and malformed values, while missing
91    /// individual policy fields default independently.
92    pub fn from_expr(expr: &Expr) -> Result<Self> {
93        let fields = map_fields(expr, "SkillCard")?;
94        expect_kind(fields)?;
95        let id = required_string(fields, "id")?;
96        let symbol = required_symbol(fields, "symbol")?;
97        let aliases = optional_list(fields, "aliases")
98            .unwrap_or(&[])
99            .iter()
100            .map(symbol_from_expr)
101            .collect::<Result<Vec<_>>>()?;
102        let origin = required_symbol(fields, "origin")?;
103        let title = required_string(fields, "title")?;
104        let description = required_string(fields, "description")?;
105        let input_shape_expr = required_field(fields, "input-shape")?;
106        let output_shape_expr = required_field(fields, "output-shape")?;
107        let roles = optional_list(fields, "roles")
108            .unwrap_or(&[])
109            .iter()
110            .map(SkillRole::from_expr)
111            .collect::<Result<Vec<_>>>()?;
112        let capabilities = optional_list(fields, "capabilities")
113            .unwrap_or(&[])
114            .iter()
115            .map(capability_from_expr)
116            .collect::<Result<Vec<_>>>()?;
117        let policy = match optional_field(fields, "policy") {
118            Some(expr) => policy_from_expr(expr)?,
119            None => SkillPolicy::default(),
120        };
121        let transport = map_fields(required_field(fields, "transport")?, "SkillCard transport")?;
122
123        Ok(Self {
124            id: id.clone(),
125            symbol: symbol.clone(),
126            aliases,
127            origin,
128            title,
129            description,
130            input_shape: shape_ref(
131                shape_symbol(
132                    Symbol::qualified(symbol.to_string(), "args"),
133                    input_shape_expr,
134                ),
135                input_shape_expr,
136            )?,
137            output_shape: shape_ref(
138                shape_symbol(
139                    Symbol::qualified(symbol.to_string(), "result"),
140                    output_shape_expr,
141                ),
142                output_shape_expr,
143            )?,
144            roles,
145            capabilities,
146            policy,
147            transport_id: required_string(transport, "id")?,
148            transport_kind: transport_kind(transport)?,
149            operation: required_string(transport, "operation")?,
150        })
151    }
152}
153
154use sim_value::build::entry as field;
155
156fn shape_ref(symbol: Symbol, expr: &Expr) -> Result<ShapeRef> {
157    let shape = parse_shape_expr(expr)?;
158    Ok(shape_value(symbol, shape))
159}
160
161fn shape_symbol(default: Symbol, expr: &Expr) -> Symbol {
162    match expr {
163        Expr::Symbol(symbol) => symbol.clone(),
164        _ => default,
165    }
166}
167
168fn expect_kind(fields: &[(Expr, Expr)]) -> Result<()> {
169    let kind = required_symbol(fields, "kind")?;
170    if kind == Symbol::qualified("skill", "card") {
171        Ok(())
172    } else {
173        Err(Error::TypeMismatch {
174            expected: "skill/card",
175            found: "other map",
176        })
177    }
178}
179
180use sim_value::access::{entry_field_any, map_entries as map_fields};
181
182fn required_field<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
183    sim_value::access::entry_field(fields, name)
184        .ok_or_else(|| Error::Eval(format!("SkillCard is missing field {name}")))
185}
186
187fn required_string(fields: &[(Expr, Expr)], name: &str) -> Result<String> {
188    match required_field(fields, name)? {
189        Expr::String(value) => Ok(value.clone()),
190        _ => Err(Error::TypeMismatch {
191            expected: "string",
192            found: "non-string",
193        }),
194    }
195}
196
197fn required_symbol(fields: &[(Expr, Expr)], name: &str) -> Result<Symbol> {
198    symbol_from_expr(required_field(fields, name)?)
199}
200
201fn symbol_from_expr(expr: &Expr) -> Result<Symbol> {
202    match expr {
203        Expr::Symbol(symbol) => Ok(symbol.clone()),
204        Expr::String(text) => Ok(parse_symbol_text(text)),
205        _ => Err(Error::TypeMismatch {
206            expected: "symbol",
207            found: "non-symbol",
208        }),
209    }
210}
211
212fn optional_list<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Option<&'a [Expr]> {
213    match required_field(fields, name).ok()? {
214        Expr::List(items) => Some(items),
215        _ => None,
216    }
217}
218
219fn capability_from_expr(expr: &Expr) -> Result<CapabilityName> {
220    match expr {
221        Expr::String(text) => Ok(CapabilityName::new(text.clone())),
222        Expr::Symbol(symbol) if symbol.namespace.as_deref() == Some("capability") => {
223            Ok(CapabilityName::new(symbol.name.to_string()))
224        }
225        Expr::Symbol(symbol) => Ok(CapabilityName::new(symbol.to_string())),
226        _ => Err(Error::TypeMismatch {
227            expected: "capability",
228            found: "non-capability",
229        }),
230    }
231}
232
233fn policy_expr(policy: &SkillPolicy) -> Expr {
234    let mut fields = vec![
235        field("privacy", Expr::Symbol(policy.privacy.as_symbol())),
236        field("cache", Expr::Symbol(policy.cache.as_symbol())),
237        field("cassette", Expr::Symbol(policy.cassette.as_symbol())),
238        field("idempotent", Expr::Bool(policy.idempotent)),
239    ];
240    if let Some(semantic_key) = &policy.semantic_key {
241        fields.push(field("semantic-key", Expr::String(semantic_key.clone())));
242    }
243    Expr::Map(fields)
244}
245
246fn policy_from_expr(expr: &Expr) -> Result<SkillPolicy> {
247    let fields = map_fields(expr, "SkillCard policy")?;
248    reject_unknown_policy_fields(fields)?;
249    Ok(SkillPolicy {
250        privacy: optional_field(fields, "privacy")
251            .map(privacy_from_expr)
252            .transpose()?
253            .unwrap_or(SkillPrivacyPolicy::NoRaw),
254        cache: optional_field(fields, "cache")
255            .map(cache_mode_from_expr)
256            .transpose()?
257            .unwrap_or(SkillCacheMode::Disabled),
258        cassette: optional_field(fields, "cassette")
259            .map(cassette_mode_from_expr)
260            .transpose()?
261            .unwrap_or(SkillCassetteMode::Disabled),
262        idempotent: optional_field(fields, "idempotent")
263            .map(bool_from_expr)
264            .transpose()?
265            .unwrap_or(false),
266        semantic_key: optional_field(fields, "semantic-key")
267            .map(stringish_from_expr)
268            .transpose()?,
269    })
270}
271
272fn reject_unknown_policy_fields(fields: &[(Expr, Expr)]) -> Result<()> {
273    const KNOWN_FIELDS: [&str; 5] = ["privacy", "cache", "cassette", "idempotent", "semantic-key"];
274
275    for (key, _) in fields {
276        let Some(name) = key_name(key) else {
277            return Err(Error::TypeMismatch {
278                expected: "bare symbol or string policy key",
279                found: "invalid policy key",
280            });
281        };
282        if !KNOWN_FIELDS.contains(&name) {
283            return Err(Error::Eval(format!(
284                "SkillCard policy has unknown field {name}"
285            )));
286        }
287    }
288    Ok(())
289}
290
291fn key_name(key: &Expr) -> Option<&str> {
292    match key {
293        Expr::Symbol(symbol) if symbol.namespace.is_none() => Some(symbol.name.as_ref()),
294        Expr::String(text) => Some(text.as_str()),
295        _ => None,
296    }
297}
298
299fn privacy_from_expr(expr: &Expr) -> Result<SkillPrivacyPolicy> {
300    match symbol_name(expr)?.as_str() {
301        "metadata-only" => Ok(SkillPrivacyPolicy::MetadataOnly),
302        "no-raw" => Ok(SkillPrivacyPolicy::NoRaw),
303        "local-only" => Ok(SkillPrivacyPolicy::LocalOnly),
304        "allow-raw" => Ok(SkillPrivacyPolicy::AllowRaw),
305        other => Err(Error::Eval(format!("unknown skill privacy policy {other}"))),
306    }
307}
308
309fn cache_mode_from_expr(expr: &Expr) -> Result<SkillCacheMode> {
310    match symbol_name(expr)?.as_str() {
311        "disabled" => Ok(SkillCacheMode::Disabled),
312        "read-through" => Ok(SkillCacheMode::ReadThrough),
313        "read-only" => Ok(SkillCacheMode::ReadOnly),
314        "write-only" => Ok(SkillCacheMode::WriteOnly),
315        "refresh" => Ok(SkillCacheMode::Refresh),
316        other => Err(Error::Eval(format!("unknown skill cache mode {other}"))),
317    }
318}
319
320fn cassette_mode_from_expr(expr: &Expr) -> Result<SkillCassetteMode> {
321    match symbol_name(expr)?.as_str() {
322        "disabled" => Ok(SkillCassetteMode::Disabled),
323        "record-replay" => Ok(SkillCassetteMode::RecordReplay),
324        "replay-only" => Ok(SkillCassetteMode::ReplayOnly),
325        "record-only" => Ok(SkillCassetteMode::RecordOnly),
326        other => Err(Error::Eval(format!("unknown skill cassette mode {other}"))),
327    }
328}
329
330fn bool_from_expr(expr: &Expr) -> Result<bool> {
331    match expr {
332        Expr::Bool(value) => Ok(*value),
333        _ => Err(Error::TypeMismatch {
334            expected: "bool",
335            found: "non-bool",
336        }),
337    }
338}
339
340fn symbol_name(expr: &Expr) -> Result<String> {
341    match expr {
342        Expr::Symbol(symbol) => Ok(symbol.name.to_string()),
343        Expr::String(text) => Ok(text.clone()),
344        _ => Err(Error::TypeMismatch {
345            expected: "symbol or string",
346            found: "invalid policy value",
347        }),
348    }
349}
350
351fn stringish_from_expr(expr: &Expr) -> Result<String> {
352    match expr {
353        Expr::String(text) => Ok(text.clone()),
354        Expr::Symbol(symbol) => Ok(symbol.to_string()),
355        _ => Err(Error::TypeMismatch {
356            expected: "string",
357            found: "non-string",
358        }),
359    }
360}
361
362fn optional_field<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
363    entry_field_any(fields, name)
364}
365
366fn transport_kind(fields: &[(Expr, Expr)]) -> Result<String> {
367    match required_field(fields, "kind")? {
368        Expr::String(value) => Ok(value.clone()),
369        Expr::Symbol(symbol) if symbol.namespace.is_none() => Ok(symbol.name.to_string()),
370        Expr::Symbol(symbol) => Ok(symbol.to_string()),
371        _ => Err(Error::TypeMismatch {
372            expected: "transport kind",
373            found: "invalid transport kind",
374        }),
375    }
376}
377
378fn parse_symbol_text(text: &str) -> Symbol {
379    match text.split_once('/') {
380        Some((namespace, name)) if !namespace.is_empty() && !name.is_empty() => {
381            Symbol::qualified(namespace.to_owned(), name.to_owned())
382        }
383        _ => Symbol::new(text.to_owned()),
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    #[test]
392    fn skill_policy_keys_use_provider_key_policy() {
393        let entries = vec![
394            field("privacy", Expr::Symbol(Symbol::new("no-raw"))),
395            (
396                Expr::String("cache".to_owned()),
397                Expr::Symbol(Symbol::new("read-through")),
398            ),
399            (
400                Expr::Symbol(Symbol::qualified("skill", "cassette")),
401                Expr::Symbol(Symbol::new("record-replay")),
402            ),
403        ];
404
405        assert_eq!(key_name(&entries[0].0), Some("privacy"));
406        assert_eq!(key_name(&entries[1].0), Some("cache"));
407        assert_eq!(key_name(&entries[2].0), None);
408        assert!(optional_field(&entries, "privacy").is_some());
409        assert!(optional_field(&entries, "cache").is_some());
410        assert_eq!(optional_field(&entries, "cassette"), None);
411    }
412}