use std::path::Path;
use serde_json::Value;
use crate::budgets::{BudgetSpend, spend_from_bytes};
use crate::git_tree::{AgentState, Framing, segment_count, settled};
use crate::login::auth::{AuthFailure, row_of_model};
mod detail;
mod orphan;
pub(crate) mod records;
pub(crate) mod wire;
mod wound;
pub use detail::{Doc, StepDetail, ToolIo, UNPARSED, detail};
pub use orphan::{ORPHANED_MAIL, ORPHANED_WINDOW, Orphan, Tail};
pub use wound::{NO_RESPONSE, OUTPUT_LIMIT, Wound, latest_wound};
const STEPS_DIR: &str = "steps";
const META_FILE: &str = "meta.json";
const REQUEST_FILE: &str = "request.json";
const RESPONSE_FILE: &str = "response.json";
const STAGING_FILE: &str = "staging.json";
const TOOLS_SUBDIR: &str = "tools";
const INPUT_FILE: &str = "input.json";
const OUTPUT_FILE: &str = "output.json";
const STEP_SEQ_WIDTH: usize = 3;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StepSummary {
pub seq: String,
pub framing: Framing,
pub attempts: usize,
pub tokens: BudgetSpend,
pub commit: Option<String>,
pub started_at: Option<String>,
pub ended_at: Option<String>,
pub auth_failed: AuthFailure,
pub wound: Wound,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct StepsView {
pub steps: Vec<StepSummary>,
pub orphan: Orphan,
}
pub fn build(workspace: &Path, agent_id: &str, state: AgentState) -> StepsView {
let mut steps: Vec<StepSummary> = step_seqs(workspace, agent_id)
.into_iter()
.map(|seq| summarize(workspace, agent_id, &seq))
.collect();
if wound::driven(state)
&& let Some(newest) = steps.last_mut()
{
newest.wound = Wound::None;
}
route_auth(workspace, agent_id, &mut steps);
StepsView {
steps,
orphan: orphan::read(workspace, agent_id, state),
}
}
fn route_auth(workspace: &Path, agent_id: &str, steps: &mut [StepSummary]) {
if !steps.iter().any(|step| step.auth_failed.offered()) {
return;
}
let roles = crate::fork::roles_at(workspace, &format!("agents/{agent_id}"));
for step in steps.iter_mut().filter(|s| s.auth_failed.offered()) {
if let Some(row) = step_model(workspace, agent_id, &step.seq)
.and_then(|model| row_of_model(&model, &roles))
{
step.auth_failed = AuthFailure::Row(row);
}
}
}
fn step_model(workspace: &Path, agent_id: &str, seq: &str) -> Option<String> {
let step = workspace.join(STEPS_DIR).join(agent_id).join(seq);
let bytes = std::fs::read(step.join(REQUEST_FILE)).ok()?;
let request: Value = serde_json::from_slice(&bytes).ok()?;
request.get("model")?.as_str().map(str::to_string)
}
fn step_seqs(workspace: &Path, agent_id: &str) -> Vec<String> {
let dir = workspace.join(STEPS_DIR).join(agent_id);
let Ok(entries) = std::fs::read_dir(&dir) else {
return Vec::new();
};
let mut seqs: Vec<String> = entries
.flatten()
.filter_map(|entry| {
let name = entry.file_name().to_str()?.to_string();
let is_seq = name.len() == STEP_SEQ_WIDTH
&& name.bytes().all(|b| b.is_ascii_digit())
&& entry.path().is_dir();
is_seq.then_some(name)
})
.collect();
seqs.sort();
seqs
}
fn summarize(workspace: &Path, agent_id: &str, seq: &str) -> StepSummary {
let step = workspace.join(STEPS_DIR).join(agent_id).join(seq);
let response = std::fs::read(step.join(RESPONSE_FILE)).unwrap_or_default();
let meta_bytes = std::fs::read(step.join(META_FILE)).ok();
let meta = meta_bytes
.as_ref()
.and_then(|bytes| serde_json::from_slice::<Value>(bytes).ok());
let settled = settled(&response);
StepSummary {
seq: seq.to_string(),
framing: settled.framing,
attempts: segment_count(&response),
tokens: spend_from_bytes(&response),
commit: meta_field(meta.as_ref(), "commit"),
started_at: meta_field(meta.as_ref(), "started_at"),
ended_at: meta_field(meta.as_ref(), "ended_at"),
auth_failed: crate::login::auth::classify(&response),
wound: wound::read(&step, &response, meta_bytes.is_some(), settled.ending),
}
}
fn meta_field(meta: Option<&Value>, key: &str) -> Option<String> {
meta?.get(key)?.as_str().map(str::to_string)
}
#[cfg(test)]
mod tests;