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