1use 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#[derive(Clone, Debug, PartialEq)]
15pub struct MissionHandoffReport {
16 pub mission_id: Symbol,
18 pub radar: RadarReport,
20 pub decomposition: Decomposition,
22 pub reflection: Reflection,
24 pub confidence: f64,
26 pub evidence: DevCassette,
28}
29
30pub 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}