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();
}
}
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)
}