liminal-cli 0.3.0

The liminal command: serve the network, maintain its stores, or serve its discovery MCP over stdio
Documentation
//! Projection names the text fields permitted on the unretained stream.

use super::handshake::Version;
use agent_client_protocol_schema::{v1, v2};
use serde_json::{Value, json};

pub(super) fn project(
    version: Version,
    params: Value,
    session: &str,
    bound: usize,
) -> anyhow::Result<Option<Value>> {
    let (id, update) = match version {
        Version::One => {
            let note: v1::SessionNotification = serde_json::from_value(params)
                .map_err(|_| anyhow::anyhow!("ACP 1 update params refused"))?;
            (
                note.session_id.to_string(),
                super::handshake::encoded(note.update)?,
            )
        }
        Version::Two => {
            let note: v2::UpdateSessionNotification = serde_json::from_value(params)
                .map_err(|_| anyhow::anyhow!("ACP 2 update params refused"))?;
            (
                note.session_id.to_string(),
                super::handshake::encoded(note.update)?,
            )
        }
    };
    anyhow::ensure!(id == session, "stream update names a foreign session");
    let kind = update["sessionUpdate"]
        .as_str()
        .ok_or_else(|| anyhow::anyhow!("update has no sessionUpdate"))?;
    let mut item = match kind {
        "agent_message_chunk" | "user_message_chunk" if update["content"]["type"] == "text" => {
            let mut item = json!({"type":kind,"text":update["content"]["text"]});
            if let Some(id) = update.get("messageId") {
                item["message_id"] = id.clone();
            }
            item
        }
        "agent_thought_chunk" if update["content"]["type"] == "text" => {
            json!({"type":"thought","text":update["content"]["text"]})
        }
        "available_commands_update" => {
            let commands: Vec<Value> = update["availableCommands"]
                .as_array()
                .map(|listed| {
                    listed
                        .iter()
                        .filter(|command| command["name"].is_string())
                        .map(|command| json!({"name":command["name"],"description":command["description"]}))
                        .collect()
                })
                .unwrap_or_default();
            json!({"type":"available_commands","commands":commands})
        }
        "tool_call" | "tool_call_update" => {
            let mut item = json!({"type":kind,"tool_call_id":update["toolCallId"]});
            for field in ["title", "status", "kind"] {
                if let Some(value) = update.get(field) {
                    item[field] = value.clone();
                }
            }
            // What the tool was given and what it answered, for the person to read; dropped
            // whole, never cut, when it would not fit one stream item.
            for (field, name) in [("rawInput", "input"), ("rawOutput", "output")] {
                if let Some(value) = update.get(field) {
                    item[name] = value.clone();
                }
            }
            item
        }
        "plan" | "plan_update" => {
            let mut entries = Vec::new();
            if let Some(plan) = (if kind == "plan" {
                update.get("entries")
            } else {
                update.pointer("/plan/entries")
            })
            .and_then(Value::as_array)
            {
                for entry in plan {
                    entries.push(json!({"content":entry["content"],"status":entry["status"],"priority":entry["priority"]}));
                }
            }
            json!({"type":kind,"entries":entries})
        }
        "state_update" => {
            json!({"type":kind,"state":update["state"],"stop_reason":update["stopReason"]})
        }
        "agent_message" | "user_message" => {
            let mut text = String::new();
            if let Some(content) = update.get("content").and_then(Value::as_array) {
                for block in content.iter().filter(|block| block["type"] == "text") {
                    if let Some(chunk) = block["text"].as_str() {
                        text.push_str(chunk);
                    }
                }
            }
            json!({"type":kind,"message_id":update["messageId"],"text":text})
        }
        "config_option_update" => {
            json!({"type":"config_options","options":update["configOptions"]})
        }
        _ => json!({"type":"omitted_update","reason":"content outside the text stream projection"}),
    };
    item["truncated"] = json!(false);
    fit(&mut item, bound)?;
    Ok(Some(item))
}

fn fit(item: &mut Value, bound: usize) -> anyhow::Result<()> {
    if serde_json::to_vec(item)?.len() <= bound {
        return Ok(());
    }
    item["truncated"] = json!(true);
    if let Some(fields) = item.as_object_mut() {
        fields.remove("input");
        fields.remove("output");
        fields.remove("commands");
    }
    if serde_json::to_vec(item)?.len() <= bound {
        return Ok(());
    }
    if let Some(entries) = item.get_mut("entries").and_then(Value::as_array_mut) {
        for entry in entries.iter_mut() {
            entry["content"] = json!("");
        }
        while serde_json::to_vec(item)?.len() > bound {
            let entries = item["entries"]
                .as_array_mut()
                .ok_or_else(|| anyhow::anyhow!("stream plan lost its entries"))?;
            anyhow::ensure!(
                !entries.is_empty(),
                "stream envelope exceeds content_bytes {bound}"
            );
            entries.pop();
        }
        return Ok(());
    }
    let field = if item.get("text").is_some() {
        "text"
    } else {
        "title"
    };
    let text = item
        .get(field)
        .and_then(Value::as_str)
        .unwrap_or_default()
        .to_owned();
    item[field] = json!("");
    anyhow::ensure!(
        serde_json::to_vec(item)?.len() <= bound,
        "stream envelope exceeds content_bytes {bound}"
    );
    let mut low = 0;
    let mut high = text.chars().count();
    while low < high {
        let middle = low + (high - low).div_ceil(2);
        item[field] = json!(text.chars().take(middle).collect::<String>());
        if serde_json::to_vec(item)?.len() <= bound {
            low = middle;
        } else {
            high = middle - 1;
        }
    }
    item[field] = json!(text.chars().take(low).collect::<String>());
    Ok(())
}

pub(super) fn replayed(mut item: Value, bound: usize) -> anyhow::Result<Value> {
    item["replayed"] = json!(true);
    fit(&mut item, bound)?;
    Ok(item)
}