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 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 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 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}