use super::{
mission::{AgentMission, AtelierAgentRole, key},
radar::{RadarIndex, RadarQuery, RadarReport, retrieve_radar_hints},
};
use crate::{Decomposition, PlanningTask, Reflection, decompose_and_run, reflect};
use sim_kernel::{Cx, Error, Expr, Result, Symbol};
use sim_lib_agent_runner_core::ModelRunner;
use sim_lib_numbers_stats::bayesian_update_binary;
use sim_lib_stream_core::{DevCassette, DevEvent, LatencyClass};
#[derive(Clone, Debug, PartialEq)]
pub struct MissionHandoffReport {
pub mission_id: Symbol,
pub radar: RadarReport,
pub decomposition: Decomposition,
pub reflection: Reflection,
pub confidence: f64,
pub evidence: DevCassette,
}
pub fn run_mission_handoff(
cx: &mut Cx,
mission: &AgentMission,
radar_index: &RadarIndex,
runner: &dyn ModelRunner,
max_steps: u32,
) -> Result<MissionHandoffReport> {
let mut query = RadarQuery::new(mission.goal());
query.repo = mission.allowed_repos().first().cloned();
query.agent_role = Some(AtelierAgentRole::Cartographer.label().to_owned());
let radar = retrieve_radar_hints(radar_index, &query)
.map_err(|err| Error::Eval(format!("mission memory retrieval failed: {err}")))?;
let goal = PlanningTask::new(mission.id().to_string(), mission.goal());
let decomposition = decompose_and_run(cx, &goal, runner, max_steps)?;
let output = decomposition
.outputs
.last()
.ok_or_else(|| Error::Eval("mission produced no handoff output".to_owned()))?;
let reflection = reflect(cx, output, runner, 0)?;
let confidence = mission_confidence(&radar, &reflection)?;
let evidence = DevCassette::from_events(
mission.evidence_stream().clone(),
evidence_events(mission, &radar, &decomposition, &reflection, confidence)?,
)?;
Ok(MissionHandoffReport {
mission_id: mission.id().clone(),
radar,
decomposition,
reflection,
confidence,
evidence,
})
}
fn mission_confidence(radar: &RadarReport, reflection: &Reflection) -> Result<f64> {
let prior = if radar.hints.is_empty() {
0.5
} else {
radar.hints.iter().map(|hint| hint.confidence).sum::<f64>() / radar.hints.len() as f64
};
let (true_positive, false_positive) = if reflection.accept {
(0.9, 0.2)
} else {
(0.45, 0.55)
};
bayesian_update_binary(prior, true_positive, false_positive)
.map_err(|err| Error::Eval(format!("mission confidence failed: {err}")))
}
fn evidence_events(
mission: &AgentMission,
radar: &RadarReport,
decomposition: &Decomposition,
reflection: &Reflection,
confidence: f64,
) -> Result<Vec<DevEvent>> {
let mut events = vec![
mission_event(
"retrieve",
AtelierAgentRole::Cartographer,
Expr::List(
radar
.hints
.iter()
.map(|hint| Expr::String(hint.chunk_id.clone()))
.collect(),
),
)?,
mission_event(
"plan",
AtelierAgentRole::Editor,
Expr::List(
decomposition
.subtasks
.iter()
.map(|task| Expr::String(task.id.clone()))
.collect(),
),
)?,
mission_event(
"guard-result",
AtelierAgentRole::Guard,
Expr::List(
mission
.capabilities()
.iter()
.map(|capability| Expr::String(format!("{capability:?}")))
.collect(),
),
)?,
];
let handoff_roles = [
AtelierAgentRole::Editor,
AtelierAgentRole::CodecSpecialist,
AtelierAgentRole::Validator,
AtelierAgentRole::DocsAgent,
AtelierAgentRole::PinAgent,
AtelierAgentRole::Reviewer,
];
for (index, output) in decomposition.outputs.iter().enumerate() {
events.push(mission_event(
"handoff",
handoff_roles[index % handoff_roles.len()].clone(),
Expr::Map(vec![
key("task", Expr::String(output.task.id.clone())),
key("content", Expr::String(output.content.clone())),
]),
)?);
}
for run in mission.validations() {
events.push(mission_event(
"validate",
AtelierAgentRole::Validator,
run.as_expr(),
)?);
}
for run in mission.docs_runs() {
events.push(mission_event(
"docs-run",
AtelierAgentRole::DocsAgent,
run.as_expr(),
)?);
}
for point in mission.decision_points() {
events.push(mission_event(
"human-gate",
AtelierAgentRole::HumanGate,
point.as_expr(),
)?);
}
events.push(mission_event(
"reflect",
AtelierAgentRole::Reviewer,
Expr::Map(vec![
key("accept", Expr::Bool(reflection.accept)),
key("critique", Expr::String(reflection.critique.clone())),
key("confidence", Expr::String(format!("{confidence:.6}"))),
]),
)?);
Ok(events)
}
fn mission_event(kind: &str, role: AtelierAgentRole, payload: Expr) -> Result<DevEvent> {
let latency = match role {
AtelierAgentRole::Validator | AtelierAgentRole::DocsAgent => LatencyClass::OfflineRender,
_ => LatencyClass::Interactive,
};
DevEvent::new(kind, role.as_symbol(), latency, payload)
}