Skip to main content

sim_lib_agent/atelier/
mission_handoff.rs

1//! Deterministic fake/cassette mission handoff runner.
2
3use super::{
4    mission::{AgentMission, AtelierAgentRole, key},
5    radar::{RadarIndex, RadarQuery, RadarReport, retrieve_radar_hints},
6};
7use crate::{Decomposition, PlanningTask, Reflection, decompose_and_run, reflect};
8use sim_kernel::{Cx, Error, Expr, Result, Symbol};
9use sim_lib_agent_runner_core::ModelRunner;
10use sim_lib_numbers_stats::bayesian_update_binary;
11use sim_lib_stream_core::{DevCassette, DevEvent, LatencyClass};
12
13/// Deterministic report from one fake/cassette mission handoff.
14#[derive(Clone, Debug, PartialEq)]
15pub struct MissionHandoffReport {
16    /// Identifier of the mission this report covers.
17    pub mission_id: Symbol,
18    /// Ranked memory hints retrieved for the mission.
19    pub radar: RadarReport,
20    /// Planned subtasks and their produced outputs.
21    pub decomposition: Decomposition,
22    /// Reviewer reflection over the final output.
23    pub reflection: Reflection,
24    /// Bayesian-updated confidence in the handoff, in `[0, 1]`.
25    pub confidence: f64,
26    /// Recorded cassette of mission evidence events.
27    pub evidence: DevCassette,
28}
29
30/// Runs a deterministic multi-agent handoff with ranked memory and cassette evidence.
31pub fn run_mission_handoff(
32    cx: &mut Cx,
33    mission: &AgentMission,
34    radar_index: &RadarIndex,
35    runner: &dyn ModelRunner,
36    max_steps: u32,
37) -> Result<MissionHandoffReport> {
38    let mut query = RadarQuery::new(mission.goal());
39    query.repo = mission.allowed_repos().first().cloned();
40    query.agent_role = Some(AtelierAgentRole::Cartographer.label().to_owned());
41    let radar = retrieve_radar_hints(radar_index, &query)
42        .map_err(|err| Error::Eval(format!("mission memory retrieval failed: {err}")))?;
43
44    let goal = PlanningTask::new(mission.id().to_string(), mission.goal());
45    let decomposition = decompose_and_run(cx, &goal, runner, max_steps)?;
46    let output = decomposition
47        .outputs
48        .last()
49        .ok_or_else(|| Error::Eval("mission produced no handoff output".to_owned()))?;
50    let reflection = reflect(cx, output, runner, 0)?;
51    let confidence = mission_confidence(&radar, &reflection)?;
52    let evidence = DevCassette::from_events(
53        mission.evidence_stream().clone(),
54        evidence_events(mission, &radar, &decomposition, &reflection, confidence)?,
55    )?;
56
57    Ok(MissionHandoffReport {
58        mission_id: mission.id().clone(),
59        radar,
60        decomposition,
61        reflection,
62        confidence,
63        evidence,
64    })
65}
66
67fn mission_confidence(radar: &RadarReport, reflection: &Reflection) -> Result<f64> {
68    let prior = if radar.hints.is_empty() {
69        0.5
70    } else {
71        radar.hints.iter().map(|hint| hint.confidence).sum::<f64>() / radar.hints.len() as f64
72    };
73    let (true_positive, false_positive) = if reflection.accept {
74        (0.9, 0.2)
75    } else {
76        (0.45, 0.55)
77    };
78    bayesian_update_binary(prior, true_positive, false_positive)
79        .map_err(|err| Error::Eval(format!("mission confidence failed: {err}")))
80}
81
82fn evidence_events(
83    mission: &AgentMission,
84    radar: &RadarReport,
85    decomposition: &Decomposition,
86    reflection: &Reflection,
87    confidence: f64,
88) -> Result<Vec<DevEvent>> {
89    let mut events = vec![
90        mission_event(
91            "retrieve",
92            AtelierAgentRole::Cartographer,
93            Expr::List(
94                radar
95                    .hints
96                    .iter()
97                    .map(|hint| Expr::String(hint.chunk_id.clone()))
98                    .collect(),
99            ),
100        )?,
101        mission_event(
102            "plan",
103            AtelierAgentRole::Editor,
104            Expr::List(
105                decomposition
106                    .subtasks
107                    .iter()
108                    .map(|task| Expr::String(task.id.clone()))
109                    .collect(),
110            ),
111        )?,
112        mission_event(
113            "guard-result",
114            AtelierAgentRole::Guard,
115            Expr::List(
116                mission
117                    .capabilities()
118                    .iter()
119                    .map(|capability| Expr::String(format!("{capability:?}")))
120                    .collect(),
121            ),
122        )?,
123    ];
124
125    let handoff_roles = [
126        AtelierAgentRole::Editor,
127        AtelierAgentRole::CodecSpecialist,
128        AtelierAgentRole::Validator,
129        AtelierAgentRole::DocsAgent,
130        AtelierAgentRole::PinAgent,
131        AtelierAgentRole::Reviewer,
132    ];
133    for (index, output) in decomposition.outputs.iter().enumerate() {
134        events.push(mission_event(
135            "handoff",
136            handoff_roles[index % handoff_roles.len()].clone(),
137            Expr::Map(vec![
138                key("task", Expr::String(output.task.id.clone())),
139                key("content", Expr::String(output.content.clone())),
140            ]),
141        )?);
142    }
143    for run in mission.validations() {
144        events.push(mission_event(
145            "validate",
146            AtelierAgentRole::Validator,
147            run.as_expr(),
148        )?);
149    }
150    for run in mission.docs_runs() {
151        events.push(mission_event(
152            "docs-run",
153            AtelierAgentRole::DocsAgent,
154            run.as_expr(),
155        )?);
156    }
157    for point in mission.decision_points() {
158        events.push(mission_event(
159            "human-gate",
160            AtelierAgentRole::HumanGate,
161            point.as_expr(),
162        )?);
163    }
164    events.push(mission_event(
165        "reflect",
166        AtelierAgentRole::Reviewer,
167        Expr::Map(vec![
168            key("accept", Expr::Bool(reflection.accept)),
169            key("critique", Expr::String(reflection.critique.clone())),
170            key("confidence", Expr::String(format!("{confidence:.6}"))),
171        ]),
172    )?);
173    Ok(events)
174}
175
176fn mission_event(kind: &str, role: AtelierAgentRole, payload: Expr) -> Result<DevEvent> {
177    let latency = match role {
178        AtelierAgentRole::Validator | AtelierAgentRole::DocsAgent => LatencyClass::OfflineRender,
179        _ => LatencyClass::Interactive,
180    };
181    DevEvent::new(kind, role.as_symbol(), latency, payload)
182}