sim_lib_openai_server/plan/
shape.rs1use sim_kernel::{Error, Expr, Result, Symbol};
2
3use crate::plan::combinators::combinator_by_name;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub struct PlanLimits {
8 pub max_depth: usize,
10 pub max_fan_out: usize,
12}
13
14impl Default for PlanLimits {
15 fn default() -> Self {
16 Self {
17 max_depth: 4,
18 max_fan_out: 4,
19 }
20 }
21}
22
23pub fn check_plan(plan: &Expr) -> Result<()> {
25 check_plan_with_limits(plan, PlanLimits::default())
26}
27
28pub fn check_plan_with_limits(plan: &Expr, limits: PlanLimits) -> Result<()> {
30 check_plan_at(plan, &limits, 1)
31}
32
33pub fn explain_plan(plan: &Expr) -> Result<String> {
35 check_plan(plan)?;
36 let mut lines = Vec::new();
37 explain_plan_at(plan, 0, &mut lines)?;
38 Ok(lines.join("\n"))
39}
40
41fn check_plan_at(plan: &Expr, limits: &PlanLimits, depth: usize) -> Result<()> {
42 if depth > limits.max_depth {
43 return Err(Error::Eval(format!(
44 "plan nesting exceeds maximum depth {}",
45 limits.max_depth
46 )));
47 }
48 let (name, args) = plan_parts(plan)?;
49 if name == "atom" {
50 return check_atom(args);
51 }
52
53 let Some(combinator) = combinator_by_name(name) else {
54 return Err(Error::Eval(format!("unknown plan combinator {name}")));
55 };
56 let children = args.iter().filter(|arg| !is_keyword_arg(arg)).count();
57 if children < combinator.min_children || children > combinator.max_children {
58 return Err(Error::Eval(format!(
59 "plan/{name} expects {}..{} children, found {children}",
60 combinator.min_children, combinator.max_children
61 )));
62 }
63 if children > limits.max_fan_out {
64 return Err(Error::Eval(format!(
65 "plan/{name} fan-out {children} exceeds maximum {}",
66 limits.max_fan_out
67 )));
68 }
69 for arg in args {
70 if let Some(keyword) = keyword_name(arg) {
71 if !combinator.keywords.contains(&keyword) {
72 return Err(Error::Eval(format!(
73 "unknown keyword {keyword} for plan/{name}"
74 )));
75 }
76 continue;
77 }
78 check_plan_at(arg, limits, depth + 1)?;
79 }
80 Ok(())
81}
82
83fn check_atom(args: &[Expr]) -> Result<()> {
84 match args {
85 [Expr::String(address)] if !address.trim().is_empty() => Ok(()),
86 [_] => Err(Error::Eval("plan/atom address must be a string".to_owned())),
87 _ => Err(Error::Eval("plan/atom expects one address".to_owned())),
88 }
89}
90
91fn explain_plan_at(plan: &Expr, indent: usize, lines: &mut Vec<String>) -> Result<()> {
92 let (name, args) = plan_parts(plan)?;
93 let prefix = " ".repeat(indent);
94 if name == "atom" {
95 let [Expr::String(address)] = args else {
96 return Err(Error::Eval("plan/atom expects one address".to_owned()));
97 };
98 lines.push(format!("{prefix}plan/atom {address}"));
99 return Ok(());
100 }
101 lines.push(format!("{prefix}plan/{name}"));
102 for arg in args {
103 if let Some(keyword) = keyword_name(arg) {
104 lines.push(format!("{prefix} keyword {keyword}"));
105 } else {
106 explain_plan_at(arg, indent + 1, lines)?;
107 }
108 }
109 Ok(())
110}
111
112pub(crate) fn plan_parts(plan: &Expr) -> Result<(&str, &[Expr])> {
113 let Expr::List(items) = plan else {
114 return Err(Error::Eval("plan must be a list expression".to_owned()));
115 };
116 let Some((Expr::Symbol(symbol), args)) = items.split_first() else {
117 return Err(Error::Eval("plan must have a symbolic head".to_owned()));
118 };
119 plan_name(symbol).map(|name| (name, args))
120}
121
122fn plan_name(symbol: &Symbol) -> Result<&str> {
123 let name = symbol.name.as_ref();
124 name.strip_prefix("plan/")
125 .ok_or_else(|| Error::Eval(format!("plan head must use plan/*, found {name}")))
126}
127
128fn is_keyword_arg(expr: &Expr) -> bool {
129 keyword_name(expr).is_some()
130}
131
132fn keyword_name(expr: &Expr) -> Option<&str> {
133 let Expr::Map(entries) = expr else {
134 return None;
135 };
136 entries.iter().find_map(|(key, value)| match (key, value) {
137 (Expr::Symbol(key), Expr::Symbol(name))
138 if key.namespace.is_none() && key.name.as_ref() == "keyword" =>
139 {
140 Some(name.name.as_ref())
141 }
142 _ => None,
143 })
144}