Skip to main content

liminal_cli/network/
flow.rs

1//! `flow` (SDK-017 R4): steps fired in order, each a routed request fed by
2//! the previous reply through `{"$prev": "<JSON pointer>"}` objects; the
3//! first failure stops the flow by index.
4
5use 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
13/// One step as the host wrote it.
14struct 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
43/// `input` with every `{"$prev": pointer}` object replaced from `prev`.
44///
45/// # Errors
46/// Returns the pointer that names nothing in `prev`.
47pub 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
71/// The `flow` tool over `steps`, at most the rail's configured count.
72pub 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;