1use 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#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct AgentPattern {
24 pub id: Symbol,
26 pub description: Option<String>,
28 pub sense: Vec<AgentPatternSlot>,
30 pub model: Vec<AgentPatternSlot>,
32 pub plan: Vec<AgentPatternSlot>,
34 pub act: Vec<AgentPatternSlot>,
36 pub memory: Vec<AgentPatternSlot>,
38 pub tools: Vec<AgentPatternSlot>,
40 pub policy: Vec<AgentPatternSlot>,
42 pub evaluation: Vec<AgentPatternSlot>,
44 pub guardrail: Vec<AgentPatternSlot>,
46 pub trace: Vec<AgentPatternSlot>,
48 pub extra: Vec<(Symbol, Expr)>,
50}
51
52#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct AgentPatternSlot {
55 pub name: Symbol,
57 pub description: Option<String>,
59 pub extra: Vec<(Symbol, Expr)>,
61}
62
63impl AgentPattern {
64 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}