use serde_json::{Map, Value};
use super::fields;
use super::files::{self, Preview};
pub(crate) const KIND: &str = "step";
pub const UNPARSED: &str = "unparseable JSON — bytes verbatim below";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Step {
pub seq: String,
pub meta: Doc,
pub request: Doc,
pub staging: Doc,
pub response: Vec<Doc>,
pub tools: Vec<ToolCall>,
pub stderr: Option<Preview>,
pub driver: Option<Preview>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolCall {
pub tool_id: String,
pub is_error: bool,
pub input: Doc,
pub output: Doc,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Doc {
Json { raw: String },
Absent,
Unparsed { note: String, raw: String },
Unknown(String),
}
pub(crate) fn step(obj: &Map<String, Value>) -> Result<Step, String> {
Ok(Step {
seq: fields::text(obj, "seq")?,
meta: record(obj, "meta")?,
request: record(obj, "request")?,
staging: record(obj, "staging")?,
response: fields::list(obj, "response", doc)?,
tools: fields::list(obj, "tools", tool)?,
stderr: capture(obj, "stderr")?,
driver: capture(obj, "driver")?,
})
}
fn record(obj: &Map<String, Value>, key: &str) -> Result<Doc, String> {
obj.get(key)
.ok_or_else(|| format!("missing field {key:?}"))
.and_then(doc)
}
fn doc(value: &Value) -> Result<Doc, String> {
let obj: &Map<String, Value> = value.as_object().ok_or("record: not an object")?;
Ok(match fields::text(obj, "kind")?.as_str() {
"json" => Doc::Json {
raw: fields::text(obj, "raw")?,
},
"absent" => Doc::Absent,
"unparsed" => Doc::Unparsed {
note: fields::text(obj, "note")?,
raw: fields::text(obj, "raw")?,
},
other => Doc::Unknown(other.to_owned()),
})
}
fn tool(value: &Value) -> Result<ToolCall, String> {
let obj: &Map<String, Value> = value.as_object().ok_or("tool call: not an object")?;
Ok(ToolCall {
tool_id: fields::text(obj, "tool_id")?,
is_error: fields::flag(obj, "is_error")?,
input: record(obj, "input")?,
output: record(obj, "output")?,
})
}
fn capture(obj: &Map<String, Value>, key: &str) -> Result<Option<Preview>, String> {
match obj.get(key) {
None | Some(Value::Null) => Ok(None),
Some(value) => files::preview(value).map(Some),
}
}
#[cfg(test)]
mod tests;