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