liminal_cli/network/
flow.rs1use serde_json::{Map, Value, json};
6
7use super::Rail;
8use super::call::{Outcome, request};
9use crate::mcp::tools::ToolResult;
10
11const PREV: &str = "$prev";
12
13struct Step<'a> {
15 subject: &'a str,
16 input: &'a Value,
17 reply: bool,
18 pass_for: Option<&'a str>,
19}
20
21fn step(value: &Value, index: usize) -> Result<Step<'_>, String> {
22 let subject = value
23 .get("subject")
24 .and_then(Value::as_str)
25 .ok_or_else(|| format!("step {index} is missing subject"))?;
26 let Some(input @ Value::Object(_)) = value.get("input") else {
27 return Err(format!("step {index}'s input is missing or not an object"));
28 };
29 let reply = value.get("reply").and_then(Value::as_bool).unwrap_or(true);
30 let pass_for = match value.get("pass_for") {
31 None => None,
32 Some(Value::String(audience)) => Some(audience.as_str()),
33 Some(_) => return Err(format!("step {index}'s pass_for is not a string")),
34 };
35 Ok(Step {
36 subject,
37 input,
38 reply,
39 pass_for,
40 })
41}
42
43pub fn fill(input: &Value, prev: Option<&Value>) -> Result<Value, String> {
48 match input {
49 Value::Object(object) => {
50 if let (1, Some(Value::String(pointer))) = (object.len(), object.get(PREV)) {
51 let prev = prev.ok_or_else(|| format!("{PREV} {pointer:?} in the first step"))?;
52 return prev.pointer(pointer).cloned().ok_or_else(|| {
53 format!("{PREV} {pointer:?} names nothing in the previous reply")
54 });
55 }
56 let mut filled = Map::with_capacity(object.len());
57 for (key, value) in object {
58 filled.insert(key.clone(), fill(value, prev)?);
59 }
60 Ok(Value::Object(filled))
61 }
62 Value::Array(items) => items
63 .iter()
64 .map(|item| fill(item, prev))
65 .collect::<Result<_, _>>()
66 .map(Value::Array),
67 other => Ok(other.clone()),
68 }
69}
70
71pub fn flow<R: Rail>(rail: &R, steps: &[Value]) -> ToolResult {
73 let limit = rail.flow_steps();
74 if steps.is_empty() {
75 return ToolResult::failed("the flow has no steps", json!({ "steps": [] }));
76 }
77 if steps.len() > limit {
78 return ToolResult::failed(
79 &format!(
80 "the flow has {} steps, over the limit of {limit}",
81 steps.len()
82 ),
83 json!({ "steps": [] }),
84 );
85 }
86 let mut answered: Vec<Value> = Vec::with_capacity(steps.len());
87 let mut prev: Option<Value> = None;
88 for (index, value) in steps.iter().enumerate() {
89 let failed = |words: String, answered: &[Value]| {
90 ToolResult::failed(
91 &format!("step {index} stopped the flow: {words}"),
92 json!({ "failed_step": index, "steps": answered }),
93 )
94 };
95 let step = match step(value, index) {
96 Ok(step) => step,
97 Err(words) => return failed(words, &answered),
98 };
99 let input = match fill(step.input, prev.as_ref()) {
100 Ok(input) => input,
101 Err(words) => return failed(words, &answered),
102 };
103 match request(rail, step.subject, &input, step.reply, step.pass_for) {
104 Outcome::Reply(reply) => {
105 answered.push(json!({ "subject": step.subject, "reply": reply }));
106 prev = Some(reply);
107 }
108 Outcome::Sent => {
109 answered.push(json!({ "subject": step.subject, "sent": true }));
110 prev = Some(Value::Null);
111 }
112 Outcome::Refused(words) => {
113 return failed(format!("{}: {words}", step.subject), &answered);
114 }
115 }
116 }
117 ToolResult::ok(json!({ "steps": answered }))
118}
119
120#[cfg(test)]
121#[path = "flow_tests.rs"]
122mod tests;