kcode-k1-chat-codex-codec 0.1.0

Concrete Codex wrapper validator and K1 chat-box JSON projector
Documentation
use kcode_k1_chat_chatend::{BoxContent, ChatBox};

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Call {
    pub name: String,
    pub arguments: String,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BoxValue {
    History(String),
    Call(Result<Call, String>),
}

#[derive(Default)]
pub struct Codec;

impl kcode_k1_codex_adapter::BoxCodec for Codec {
    type Box = BoxValue;

    fn tool_call_box(&mut self, call: &kcode_k1_codex_adapter::ToolCall) -> Self::Box {
        if call.name != "call_ktool" {
            return BoxValue::Call(Err("tool name must be call_ktool".into()));
        }
        let serde_json::Value::Object(values) = &call.arguments else {
            return BoxValue::Call(Err("tool arguments must be an object".into()));
        };
        if values.len() != 2 || !values.contains_key("name") || !values.contains_key("arguments") {
            return BoxValue::Call(Err("tool arguments must have name and arguments".into()));
        }
        let Some(serde_json::Value::String(name)) = values.get("name") else {
            return BoxValue::Call(Err("tool arguments name must be a string".into()));
        };
        let Some(arguments) = values.get("arguments") else {
            return BoxValue::Call(Err("tool arguments require arguments".into()));
        };
        let arguments = match serde_json::to_string(arguments) {
            Ok(arguments) => arguments,
            Err(_) => return BoxValue::Call(Err("tool arguments cannot be encoded".into())),
        };
        BoxValue::Call(Ok(Call {
            name: name.clone(),
            arguments,
        }))
    }

    fn box_text<'a>(&self, box_: &'a Self::Box) -> &'a str {
        match box_ {
            BoxValue::History(text) => text,
            BoxValue::Call(_) => "",
        }
    }
}

pub fn project(box_: &ChatBox) -> BoxValue {
    let box_id = box_.id().get();
    let text = match box_.content() {
        BoxContent::System(text) => format!(
            "{{\"box_id\":{box_id},\"kind\":\"system\",\"text\":{}}}",
            json(text)
        ),
        BoxContent::User(text) => format!(
            "{{\"box_id\":{box_id},\"kind\":\"user\",\"text\":{}}}",
            json(text)
        ),
        BoxContent::Kennedy { text, complete } => format!(
            "{{\"box_id\":{box_id},\"kind\":\"kennedy\",\"text\":{},\"complete\":{complete}}}",
            json(text)
        ),
        BoxContent::Attachment => format!("{{\"box_id\":{box_id},\"kind\":\"attachment\"}}"),
        BoxContent::KtoolCall {
            action_id,
            name,
            arguments,
        } => format!(
            "{{\"box_id\":{box_id},\"kind\":\"ktool_call\",\"action_id\":{},\"name\":{},\"arguments\":{}}}",
            action(action_id),
            json(name),
            json(arguments)
        ),
        BoxContent::KtoolReturn {
            action_id,
            originating_call,
            result,
        } => format!(
            "{{\"box_id\":{box_id},\"kind\":\"ktool_return\",\"action_id\":{},\"originating_call_box_id\":{},\"result\":{}}}",
            action(action_id),
            originating_call.get(),
            result_json(result)
        ),
    };
    BoxValue::History(text)
}

fn json(text: &str) -> String {
    serde_json::to_string(text).expect("strings serialize")
}

fn action(action_id: &kcode_k1_chat_chatend::ActionId) -> String {
    let session = action_id
        .session()
        .iter()
        .map(|byte| format!("{byte:02x}"))
        .collect::<String>();
    format!(
        "{{\"session\":{session:?},\"sequence\":{}}}",
        action_id.sequence()
    )
}

fn result_json(result: &Result<String, String>) -> String {
    match result {
        Ok(value) => format!("{{\"status\":\"ok\",\"value\":{}}}", json(value)),
        Err(value) => format!("{{\"status\":\"error\",\"value\":{}}}", json(value)),
    }
}