use serde_json::{Map, Value, json};
use crate::boundary::answer::agent::AgentView;
use crate::boundary::codec::fields::{
bool_of, list_of, opt, opt_val, pick, str_of, strings_of, u64_of,
};
use crate::context::Fullness;
use crate::control::hold::Held;
use crate::git_tree::AgentMark;
use crate::nav::convs::{Doing, FlightStrip, Seat};
use super::board::decode::figure;
use super::board::figure_value;
use super::rows::decode::{FLIGHTS, state_of};
use super::rows::{flight_token, state_token};
const MARKS: [(&str, AgentMark); 5] = [
("notified", AgentMark::Notified),
("budget-exhausted", AgentMark::BudgetExhausted),
("conflicted", AgentMark::Conflicted),
("held", AgentMark::Held),
("abandoned", AgentMark::Abandoned),
];
const DOINGS: [(&str, Doing); 5] = [
("waiting", Doing::Waiting),
("thinking", Doing::Thinking),
("inference", Doing::Inference),
("tools", Doing::Tools),
("idle", Doing::Idle),
];
pub(super) fn reply(view: &AgentView) -> Value {
let mut map = Map::new();
map.insert("ok".to_owned(), json!(true));
map.insert("kind".to_owned(), json!("agent"));
map.insert("agent".to_owned(), json!(view.agent_id));
map.insert("root".to_owned(), json!(view.root));
if !view.ancestors.is_empty() {
map.insert("ancestors".to_owned(), json!(view.ancestors));
}
map.insert("display".to_owned(), json!(view.name));
map.insert("display_only".to_owned(), json!(view.display_only));
map.insert("tip".to_owned(), json!(view.tip));
map.insert("state".to_owned(), json!(state_token(view.state)));
if !view.marks.is_empty() {
let marks: Vec<&str> = view.marks.iter().copied().map(mark_token).collect();
map.insert("marks".to_owned(), json!(marks));
}
if let Some(flight) = view.flight {
map.insert("flight".to_owned(), json!(flight_token(flight)));
}
if let Some(held) = &view.held {
map.insert(
"held".to_owned(),
json!({
"tool_use_id": held.tool_use_id,
"tool": held.tool,
"reason": held.reason,
}),
);
}
map.insert("present".to_owned(), json!(view.present));
map.insert("nudgeable".to_owned(), json!(view.nudgeable));
map.insert("stoppable".to_owned(), json!(view.stoppable));
map.insert("stop_children".to_owned(), json!(view.stop_children));
if !view.seats.is_empty() {
let seats: Vec<Value> = view
.seats
.iter()
.map(|seat| json!({ "name": seat.name, "doing": doing_token(seat.doing) }))
.collect();
map.insert("seats".to_owned(), json!(seats));
}
if let Some(strip) = &view.strip {
map.insert(
"strip".to_owned(),
json!({ "class": flight_token(strip.class), "facts": strip.facts }),
);
}
map.insert("spend".to_owned(), figure_value(&view.spend));
if let Some(full) = &view.context {
map.insert(
"context".to_owned(),
json!({
"model": full.model,
"prompt_tokens": full.prompt_tokens,
"window": full.window,
"percent": full.percent(),
}),
);
}
Value::Object(map)
}
pub(super) fn view_of(o: &Map<String, Value>) -> Result<AgentView, String> {
let marks: Vec<AgentMark> = opt(o, "marks", strings_of)?
.unwrap_or_default()
.iter()
.map(|word| mark_of(word))
.collect::<Result<_, String>>()?;
Ok(AgentView {
agent_id: str_of(o, "agent")?,
root: str_of(o, "root")?,
ancestors: opt(o, "ancestors", strings_of)?.unwrap_or_default(),
name: str_of(o, "display")?,
display_only: bool_of(o, "display_only")?,
tip: str_of(o, "tip")?,
state: state_of(o)?,
marks,
held: opt_val(o, "held", held_of)?,
flight: opt(o, "flight", |o, k| pick(o, k, &FLIGHTS))?,
present: bool_of(o, "present")?,
nudgeable: bool_of(o, "nudgeable")?,
stoppable: bool_of(o, "stoppable")?,
stop_children: bool_of(o, "stop_children")?,
seats: opt(o, "seats", |o, k| list_of(o, k, seat_of))?.unwrap_or_default(),
strip: opt_val(o, "strip", strip_of)?,
spend: figure(o.get("spend").ok_or("agent: missing spend")?)?,
context: opt_val(o, "context", context_of)?,
})
}
fn context_of(v: &Value) -> Result<Fullness, String> {
let o = v.as_object().ok_or("context: not an object")?;
Ok(Fullness {
model: str_of(o, "model")?,
prompt_tokens: u64_of(o, "prompt_tokens")?,
window: u64_of(o, "window")?,
})
}
fn seat_of(v: &Value) -> Result<Seat, String> {
let o = v.as_object().ok_or("seat: not an object")?;
Ok(Seat {
name: str_of(o, "name")?,
doing: pick(o, "doing", &DOINGS)?,
})
}
fn strip_of(v: &Value) -> Result<FlightStrip, String> {
let o = v.as_object().ok_or("strip: not an object")?;
Ok(FlightStrip {
class: pick(o, "class", &FLIGHTS)?,
facts: str_of(o, "facts")?,
})
}
fn doing_token(doing: Doing) -> &'static str {
match doing {
Doing::Waiting => "waiting",
Doing::Thinking => "thinking",
Doing::Inference => "inference",
Doing::Tools => "tools",
Doing::Idle => "idle",
}
}
fn held_of(v: &Value) -> Result<Held, String> {
let o = v.as_object().ok_or("held: not an object")?;
Ok(Held {
tool_use_id: str_of(o, "tool_use_id")?,
tool: str_of(o, "tool")?,
reason: str_of(o, "reason")?,
})
}
fn mark_token(mark: AgentMark) -> &'static str {
match mark {
AgentMark::Notified => "notified",
AgentMark::BudgetExhausted => "budget-exhausted",
AgentMark::Conflicted => "conflicted",
AgentMark::Held => "held",
AgentMark::Abandoned => "abandoned",
}
}
fn mark_of(word: &str) -> Result<AgentMark, String> {
MARKS
.iter()
.find(|(token, _)| *token == word)
.map(|(_, mark)| *mark)
.ok_or_else(|| format!("agent: unknown mark {word:?}"))
}