Skip to main content

sim_lib_agent/
pattern.rs

1//! Agent-pattern descriptors used by deterministic recipes and browse surfaces.
2
3use crate::{expr_to_value, keyword, stringish_from_value, symbol_from_value};
4use sim_kernel::{Args, ClassRef, Cx, Error, Expr, Object, Result, Symbol, Value};
5use std::{any::Any, collections::BTreeMap, sync::Arc};
6
7const PATTERN_KIND: &str = "agent-pattern";
8const FIELD_NAMES: [&str; 10] = [
9    "sense",
10    "model",
11    "plan",
12    "act",
13    "memory",
14    "tools",
15    "policy",
16    "evaluation",
17    "guardrail",
18    "trace",
19];
20
21/// Descriptor of an agent pattern: an id, description, and named slot groups.
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct AgentPattern {
24    /// Stable identifier for the pattern.
25    pub id: Symbol,
26    /// Optional human-readable description.
27    pub description: Option<String>,
28    /// Slots describing how the pattern senses its environment.
29    pub sense: Vec<AgentPatternSlot>,
30    /// Slots describing the models the pattern uses.
31    pub model: Vec<AgentPatternSlot>,
32    /// Slots describing planning behavior.
33    pub plan: Vec<AgentPatternSlot>,
34    /// Slots describing actions the pattern takes.
35    pub act: Vec<AgentPatternSlot>,
36    /// Slots describing memory backends.
37    pub memory: Vec<AgentPatternSlot>,
38    /// Slots describing available tools.
39    pub tools: Vec<AgentPatternSlot>,
40    /// Slots describing policy constraints.
41    pub policy: Vec<AgentPatternSlot>,
42    /// Slots describing evaluation steps.
43    pub evaluation: Vec<AgentPatternSlot>,
44    /// Slots describing guardrails.
45    pub guardrail: Vec<AgentPatternSlot>,
46    /// Slots describing trace and observability.
47    pub trace: Vec<AgentPatternSlot>,
48    /// Extra caller-supplied table entries preserved verbatim.
49    pub extra: Vec<(Symbol, Expr)>,
50}
51
52/// One named entry within an agent-pattern slot group.
53#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct AgentPatternSlot {
55    /// Slot name.
56    pub name: Symbol,
57    /// Optional human-readable description.
58    pub description: Option<String>,
59    /// Extra caller-supplied table entries preserved verbatim.
60    pub extra: Vec<(Symbol, Expr)>,
61}
62
63impl AgentPattern {
64    /// Encodes the pattern as a table expression for recipes and browse surfaces.
65    pub fn as_expr(&self) -> Expr {
66        let mut entries = vec![
67            key_expr("kind", Expr::Symbol(Symbol::new(PATTERN_KIND))),
68            key_expr("id", Expr::Symbol(self.id.clone())),
69        ];
70        if let Some(description) = &self.description {
71            entries.push(key_expr("description", Expr::String(description.clone())));
72        }
73        entries.push(key_expr("card", self.card_expr()));
74        for (name, slots) in self.slot_groups() {
75            entries.push(key_expr(name, slots_expr(slots)));
76        }
77        entries.extend(
78            self.extra
79                .iter()
80                .cloned()
81                .map(|(key, value)| (Expr::Symbol(key), value)),
82        );
83        Expr::Map(entries)
84    }
85
86    fn card_expr(&self) -> Expr {
87        let mut entries = vec![
88            key_expr("kind", Expr::Symbol(Symbol::new(PATTERN_KIND))),
89            key_expr("id", Expr::Symbol(self.id.clone())),
90            key_expr(
91                "fields",
92                Expr::List(
93                    FIELD_NAMES
94                        .iter()
95                        .map(|name| Expr::Symbol(Symbol::new(*name)))
96                        .collect(),
97                ),
98            ),
99        ];
100        if let Some(description) = &self.description {
101            entries.push(key_expr("description", Expr::String(description.clone())));
102        }
103        Expr::Map(entries)
104    }
105
106    fn slot_groups(&self) -> [(&'static str, &[AgentPatternSlot]); 10] {
107        [
108            ("sense", &self.sense),
109            ("model", &self.model),
110            ("plan", &self.plan),
111            ("act", &self.act),
112            ("memory", &self.memory),
113            ("tools", &self.tools),
114            ("policy", &self.policy),
115            ("evaluation", &self.evaluation),
116            ("guardrail", &self.guardrail),
117            ("trace", &self.trace),
118        ]
119    }
120}
121
122impl Object for AgentPattern {
123    fn display(&self, _cx: &mut Cx) -> Result<String> {
124        Ok(format!("#<agent-pattern {}>", self.id))
125    }
126
127    fn as_any(&self) -> &dyn Any {
128        self
129    }
130}
131
132impl sim_kernel::ObjectCompat for AgentPattern {
133    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
134        if let Some(value) = cx
135            .registry()
136            .class_by_symbol(&Symbol::qualified("core", "Table"))
137        {
138            return Ok(value.clone());
139        }
140        cx.factory().class_stub(
141            sim_kernel::CORE_TABLE_CLASS_ID,
142            Symbol::qualified("core", "Table"),
143        )
144    }
145
146    fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
147        Ok(self.as_expr())
148    }
149
150    fn as_table(&self, cx: &mut Cx) -> Result<Value> {
151        expr_to_value(cx, &self.as_expr())
152    }
153}
154
155impl AgentPatternSlot {
156    fn as_expr(&self) -> Expr {
157        let mut entries = vec![key_expr("name", Expr::Symbol(self.name.clone()))];
158        if let Some(description) = &self.description {
159            entries.push(key_expr("description", Expr::String(description.clone())));
160        }
161        entries.extend(
162            self.extra
163                .iter()
164                .cloned()
165                .map(|(key, value)| (Expr::Symbol(key), value)),
166        );
167        Expr::Map(entries)
168    }
169}
170
171pub(crate) fn agent_pattern_value(cx: &mut Cx, args: Args) -> Result<Value> {
172    let mut options = parse_pattern_options(cx, args)?;
173    let id = required_symbol_option(cx, &mut options, "id", "agent/pattern requires :id")?;
174    let description = optional_string(cx, &mut options, "description")?;
175    let pattern = AgentPattern {
176        id,
177        description,
178        sense: slot_option(cx, &mut options, "sense")?,
179        model: slot_option(cx, &mut options, "model")?,
180        plan: slot_option(cx, &mut options, "plan")?,
181        act: slot_option(cx, &mut options, "act")?,
182        memory: slot_option(cx, &mut options, "memory")?,
183        tools: slot_option(cx, &mut options, "tools")?,
184        policy: slot_option(cx, &mut options, "policy")?,
185        evaluation: slot_option(cx, &mut options, "evaluation")?,
186        guardrail: slot_option(cx, &mut options, "guardrail")?,
187        trace: slot_option(cx, &mut options, "trace")?,
188        extra: extra_options(cx, options)?,
189    };
190    cx.factory().opaque(Arc::new(pattern))
191}
192
193fn parse_pattern_options(cx: &mut Cx, args: Args) -> Result<BTreeMap<String, Value>> {
194    if !args.values().len().is_multiple_of(2) {
195        return Err(Error::Eval(
196            "agent/pattern options must be key/value pairs".to_owned(),
197        ));
198    }
199    let mut options = BTreeMap::new();
200    for pair in args.values().chunks(2) {
201        let key = keyword(&pair[0].object().as_expr(cx)?)?;
202        options.insert(key, pair[1].clone());
203    }
204    Ok(options)
205}
206
207fn required_symbol_option(
208    cx: &mut Cx,
209    options: &mut BTreeMap<String, Value>,
210    key: &str,
211    message: &'static str,
212) -> Result<Symbol> {
213    let Some(value) = options.remove(key) else {
214        return Err(Error::Eval(message.to_owned()));
215    };
216    symbol_from_value(cx, value, "expected symbol option")
217}
218
219fn optional_string(
220    cx: &mut Cx,
221    options: &mut BTreeMap<String, Value>,
222    key: &str,
223) -> Result<Option<String>> {
224    options
225        .remove(key)
226        .map(|value| stringish_from_value(cx, value, "expected string or symbol option"))
227        .transpose()
228}
229
230fn slot_option(
231    cx: &mut Cx,
232    options: &mut BTreeMap<String, Value>,
233    key: &str,
234) -> Result<Vec<AgentPatternSlot>> {
235    let Some(value) = options.remove(key) else {
236        return Ok(Vec::new());
237    };
238    match value.object().as_expr(cx)? {
239        Expr::Nil => Ok(Vec::new()),
240        Expr::List(items) | Expr::Vector(items) => items.iter().map(slot_from_expr).collect(),
241        expr => Ok(vec![slot_from_expr(&expr)?]),
242    }
243}
244
245fn slot_from_expr(expr: &Expr) -> Result<AgentPatternSlot> {
246    match expr {
247        Expr::Symbol(symbol) => Ok(slot(symbol.clone(), None, Vec::new())),
248        Expr::String(text) => Ok(slot(Symbol::new(text.clone()), None, Vec::new())),
249        Expr::Map(entries) => slot_from_map(entries),
250        _ => Err(Error::TypeMismatch {
251            expected: "agent-pattern slot",
252            found: "non-slot",
253        }),
254    }
255}
256
257fn slot_from_map(entries: &[(Expr, Expr)]) -> Result<AgentPatternSlot> {
258    let name = map_field(entries, "name")
259        .map(symbol_from_expr)
260        .transpose()?
261        .ok_or_else(|| Error::Eval("agent-pattern slot requires name".to_owned()))?;
262    let description = map_field(entries, "description")
263        .map(stringish_from_expr)
264        .transpose()?;
265    let extra = entries
266        .iter()
267        .filter_map(|(key, value)| {
268            let Expr::Symbol(symbol) = key else {
269                return Some(Err(Error::TypeMismatch {
270                    expected: "symbol table key",
271                    found: "non-symbol",
272                }));
273            };
274            if symbol.name.as_ref() == "name" || symbol.name.as_ref() == "description" {
275                return None;
276            }
277            Some(Ok((symbol.clone(), value.clone())))
278        })
279        .collect::<Result<Vec<_>>>()?;
280    Ok(slot(name, description, extra))
281}
282
283fn extra_options(cx: &mut Cx, options: BTreeMap<String, Value>) -> Result<Vec<(Symbol, Expr)>> {
284    options
285        .into_iter()
286        .map(|(key, value)| Ok((Symbol::new(key), value.object().as_expr(cx)?)))
287        .collect()
288}
289
290fn slot(name: Symbol, description: Option<String>, extra: Vec<(Symbol, Expr)>) -> AgentPatternSlot {
291    AgentPatternSlot {
292        name,
293        description,
294        extra,
295    }
296}
297
298fn slots_expr(slots: &[AgentPatternSlot]) -> Expr {
299    Expr::List(slots.iter().map(AgentPatternSlot::as_expr).collect())
300}
301
302fn key_expr(key: &str, value: Expr) -> (Expr, Expr) {
303    (Expr::Symbol(Symbol::new(key)), value)
304}
305
306use sim_value::access::entry_field as map_field;
307
308fn symbol_from_expr(expr: &Expr) -> Result<Symbol> {
309    match expr {
310        Expr::Symbol(symbol) => Ok(symbol.clone()),
311        Expr::String(text) => Ok(Symbol::new(text.clone())),
312        _ => Err(Error::Eval("expected symbol or string".to_owned())),
313    }
314}
315
316fn stringish_from_expr(expr: &Expr) -> Result<String> {
317    match expr {
318        Expr::String(text) => Ok(text.clone()),
319        Expr::Symbol(symbol) => Ok(symbol.to_string()),
320        _ => Err(Error::Eval("expected string or symbol".to_owned())),
321    }
322}