1use sim_kernel::{Expr, Symbol};
4use sim_lib_scene::{data_map, node, sym};
5use sim_lib_stream_core::{DevCassette, StreamPacket};
6use sim_value::build::uint;
7
8pub const MISSION_CONTROL_LENS: &str = "view:agent-mission-control";
10
11#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct MissionControlState {
14 pub missions: Vec<MissionCard>,
16 pub lease_conflicts: Vec<LeaseConflictCard>,
18 pub evidence: Vec<EvidenceEvent>,
20 pub intents: Vec<MissionControlIntent>,
22}
23
24#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct MissionCard {
27 pub id: Symbol,
29 pub goal: String,
31 pub roles: Vec<String>,
33 pub recipe_pattern: String,
35 pub leases: Vec<LeaseClaim>,
37 pub validation: ValidationState,
39 pub human_gates: Vec<HumanGate>,
41 pub facets: Vec<ExplanationFacet>,
43}
44
45#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct LeaseClaim {
48 pub target_kind: String,
50 pub target: String,
52 pub mode: String,
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
58pub struct LeaseConflictCard {
59 pub left_mission: Symbol,
61 pub left: LeaseClaim,
63 pub right_mission: Symbol,
65 pub right: LeaseClaim,
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub enum ValidationState {
72 Pending,
74 Running,
76 Passed,
78 Failed,
80}
81
82impl ValidationState {
83 pub fn token(self) -> &'static str {
85 match self {
86 Self::Pending => "pending",
87 Self::Running => "running",
88 Self::Passed => "passed",
89 Self::Failed => "failed",
90 }
91 }
92}
93
94#[derive(Clone, Debug, PartialEq, Eq)]
96pub struct HumanGate {
97 pub id: String,
99 pub prompt: String,
101 pub status: String,
103}
104
105#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct ExplanationFacet {
108 pub label: String,
110 pub evidence: String,
112 pub confidence: String,
114}
115
116#[derive(Clone, Debug, PartialEq, Eq)]
118pub struct EvidenceEvent {
119 pub sequence: u64,
121 pub kind: Symbol,
123 pub summary: String,
125}
126
127#[derive(Clone, Debug, PartialEq, Eq)]
129pub struct MissionControlIntent {
130 pub kind: &'static str,
132 pub label: &'static str,
134}
135
136pub fn mission_control_view(state: &MissionControlState) -> Expr {
138 node(
139 "stack",
140 vec![
141 ("role", sym("mission-control")),
142 ("dir", sym("column")),
143 (
144 "children",
145 Expr::List(vec![
146 mission_summary(state),
147 mission_list(&state.missions),
148 evidence_replay(&state.evidence),
149 lease_conflicts(&state.lease_conflicts),
150 command_bar(&state.intents),
151 ]),
152 ),
153 ],
154 )
155}
156
157pub fn mission_control_replay_frames(state: &MissionControlState) -> Vec<Expr> {
159 (0..=state.evidence.len())
160 .map(|count| {
161 let mut frame = state.clone();
162 frame.evidence.truncate(count);
163 mission_control_view(&frame)
164 })
165 .collect()
166}
167
168pub fn evidence_from_dev_cassette(cassette: &DevCassette) -> Vec<EvidenceEvent> {
170 cassette
171 .cassette()
172 .envelopes()
173 .iter()
174 .enumerate()
175 .filter_map(|(sequence, envelope)| {
176 let StreamPacket::Data(packet) = envelope.packet() else {
177 return None;
178 };
179 Some(EvidenceEvent {
180 sequence: sequence as u64,
181 kind: packet.kind.clone(),
182 summary: payload_summary(&packet.payload)
183 .unwrap_or_else(|| packet.kind.name.to_string()),
184 })
185 })
186 .collect()
187}
188
189pub fn mission_control_intents() -> Vec<MissionControlIntent> {
191 vec![
192 MissionControlIntent {
193 kind: "approve",
194 label: "Approve",
195 },
196 MissionControlIntent {
197 kind: "reject",
198 label: "Reject",
199 },
200 MissionControlIntent {
201 kind: "ask",
202 label: "Ask",
203 },
204 MissionControlIntent {
205 kind: "split-mission",
206 label: "Split",
207 },
208 MissionControlIntent {
209 kind: "pause-agent",
210 label: "Pause",
211 },
212 MissionControlIntent {
213 kind: "rerun-validation",
214 label: "Rerun validation",
215 },
216 MissionControlIntent {
217 kind: "replay-cassette",
218 label: "Replay",
219 },
220 MissionControlIntent {
221 kind: "open-source",
222 label: "Open source",
223 },
224 ]
225}
226
227fn mission_summary(state: &MissionControlState) -> Expr {
228 node(
229 "box",
230 vec![
231 ("role", sym("mission-summary")),
232 (
233 "children",
234 Expr::List(vec![
235 text(format!("missions: {}", state.missions.len())),
236 text(format!("evidence events: {}", state.evidence.len())),
237 text(format!("lease conflicts: {}", state.lease_conflicts.len())),
238 ]),
239 ),
240 ],
241 )
242}
243
244fn mission_list(missions: &[MissionCard]) -> Expr {
245 node(
246 "grid",
247 vec![
248 ("role", sym("missions")),
249 (
250 "children",
251 Expr::List(missions.iter().map(mission_card).collect()),
252 ),
253 ],
254 )
255}
256
257fn mission_card(mission: &MissionCard) -> Expr {
258 node(
259 "box",
260 vec![
261 ("role", sym("mission-card")),
262 ("mission", Expr::Symbol(mission.id.clone())),
263 (
264 "children",
265 Expr::List(vec![
266 text(mission.goal.clone()),
267 data_line("recipe-pattern", &mission.recipe_pattern),
268 badge(mission.validation.token(), mission.validation.token()),
269 list_box("roles", mission.roles.iter().cloned()),
270 list_box("leases", mission.leases.iter().map(LeaseClaim::label)),
271 list_box(
272 "human-gates",
273 mission.human_gates.iter().map(HumanGate::label),
274 ),
275 list_box(
276 "explanation",
277 mission.facets.iter().map(ExplanationFacet::label),
278 ),
279 ]),
280 ),
281 ],
282 )
283}
284
285fn evidence_replay(evidence: &[EvidenceEvent]) -> Expr {
286 let events = evidence
287 .iter()
288 .map(|event| {
289 data_map(vec![
290 ("at", uint(event.sequence)),
291 ("event", Expr::Symbol(event.kind.clone())),
292 ("label", Expr::String(event.summary.clone())),
293 ])
294 })
295 .collect();
296 node(
297 "box",
298 vec![
299 ("role", sym("evidence-replay")),
300 (
301 "children",
302 Expr::List(vec![
303 node(
304 "timeline",
305 vec![
306 ("lane", sym("dev-cassette")),
307 ("events", Expr::List(events)),
308 ],
309 ),
310 node(
311 "slider",
312 vec![
313 ("target", sym("replay-cassette")),
314 ("value", uint(evidence.len() as u64)),
315 ("max", uint(evidence.len() as u64)),
316 ],
317 ),
318 ]),
319 ),
320 ],
321 )
322}
323
324fn lease_conflicts(conflicts: &[LeaseConflictCard]) -> Expr {
325 let rows = conflicts
326 .iter()
327 .map(|conflict| {
328 node(
329 "text",
330 vec![(
331 "text",
332 Expr::String(format!(
333 "{} {} conflicts with {} {}",
334 conflict.left_mission,
335 conflict.left.label(),
336 conflict.right_mission,
337 conflict.right.label()
338 )),
339 )],
340 )
341 })
342 .collect();
343 node(
344 "box",
345 vec![
346 ("role", sym("lease-conflicts")),
347 ("children", Expr::List(rows)),
348 ],
349 )
350}
351
352fn command_bar(intents: &[MissionControlIntent]) -> Expr {
353 node(
354 "stack",
355 vec![
356 ("role", sym("mission-intents")),
357 ("dir", sym("row")),
358 (
359 "children",
360 Expr::List(
361 intents
362 .iter()
363 .map(|intent| {
364 node(
365 "button",
366 vec![
367 (
368 "intent",
369 Expr::Symbol(Symbol::qualified("intent", intent.kind)),
370 ),
371 ("label", Expr::String(intent.label.to_owned())),
372 ],
373 )
374 })
375 .collect(),
376 ),
377 ),
378 ],
379 )
380}
381
382fn payload_summary(expr: &Expr) -> Option<String> {
383 let Expr::Map(entries) = expr else {
384 return None;
385 };
386 entries.iter().find_map(|(key, value)| {
387 let Expr::Symbol(symbol) = key else {
388 return None;
389 };
390 if symbol.namespace.is_none() && symbol.name.as_ref() == "summary" {
391 match value {
392 Expr::String(summary) => Some(summary.clone()),
393 _ => None,
394 }
395 } else {
396 None
397 }
398 })
399}
400
401fn data_line(label: &str, value: &str) -> Expr {
402 text(format!("{label}: {value}"))
403}
404
405fn list_box(role: &str, items: impl Iterator<Item = String>) -> Expr {
406 node(
407 "box",
408 vec![
409 ("role", sym(role)),
410 ("children", Expr::List(items.map(text).collect())),
411 ],
412 )
413}
414
415fn text(content: impl Into<String>) -> Expr {
416 node("text", vec![("text", Expr::String(content.into()))])
417}
418
419fn badge(status: &str, label: &str) -> Expr {
420 node(
421 "badge",
422 vec![
423 ("status", sym(status)),
424 ("label", Expr::String(label.to_owned())),
425 ],
426 )
427}
428
429impl LeaseClaim {
430 fn label(&self) -> String {
431 format!("{}:{} ({})", self.target_kind, self.target, self.mode)
432 }
433}
434
435impl HumanGate {
436 fn label(&self) -> String {
437 format!("{} ({})", self.prompt, self.status)
438 }
439}
440
441impl ExplanationFacet {
442 fn label(&self) -> String {
443 format!("{}: {} ({})", self.label, self.evidence, self.confidence)
444 }
445}