Skip to main content

sim_lib_view_agent/
mission_control.rs

1//! Mission Control Scene for agent-operated work.
2
3use 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
8/// Mission Control lens id.
9pub const MISSION_CONTROL_LENS: &str = "view:agent-mission-control";
10
11/// Full state rendered by Mission Control.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct MissionControlState {
14    /// Missions shown in the operator view.
15    pub missions: Vec<MissionCard>,
16    /// Lease conflicts with exact targets.
17    pub lease_conflicts: Vec<LeaseConflictCard>,
18    /// Evidence events read from the Dev Cassette.
19    pub evidence: Vec<EvidenceEvent>,
20    /// Command intents exposed to the operator.
21    pub intents: Vec<MissionControlIntent>,
22}
23
24/// One mission card.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct MissionCard {
27    /// Mission id.
28    pub id: Symbol,
29    /// Mission goal.
30    pub goal: String,
31    /// Active agent roles.
32    pub roles: Vec<String>,
33    /// Recipe pattern label.
34    pub recipe_pattern: String,
35    /// Workspace leases.
36    pub leases: Vec<LeaseClaim>,
37    /// Validation status.
38    pub validation: ValidationState,
39    /// Human gates waiting or resolved for this mission.
40    pub human_gates: Vec<HumanGate>,
41    /// F6 explanation and attribution facets.
42    pub facets: Vec<ExplanationFacet>,
43}
44
45/// A lease claim rendered in Mission Control.
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct LeaseClaim {
48    /// Lease target kind: file, crate, or ide-object.
49    pub target_kind: String,
50    /// Exact target string.
51    pub target: String,
52    /// Lease mode.
53    pub mode: String,
54}
55
56/// A visible lease conflict.
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub struct LeaseConflictCard {
59    /// Left mission id.
60    pub left_mission: Symbol,
61    /// Left lease target.
62    pub left: LeaseClaim,
63    /// Right mission id.
64    pub right_mission: Symbol,
65    /// Right lease target.
66    pub right: LeaseClaim,
67}
68
69/// Validation state for a mission.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub enum ValidationState {
72    /// Validation is not started.
73    Pending,
74    /// Validation is running.
75    Running,
76    /// Validation passed.
77    Passed,
78    /// Validation failed.
79    Failed,
80}
81
82impl ValidationState {
83    /// Stable status token.
84    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/// Human gate shown in Mission Control.
95#[derive(Clone, Debug, PartialEq, Eq)]
96pub struct HumanGate {
97    /// Gate id.
98    pub id: String,
99    /// Gate question or action.
100    pub prompt: String,
101    /// Gate status token.
102    pub status: String,
103}
104
105/// F6 explanation or attribution facet.
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct ExplanationFacet {
108    /// Facet label.
109    pub label: String,
110    /// Evidence summary.
111    pub evidence: String,
112    /// Confidence token or percentage.
113    pub confidence: String,
114}
115
116/// One replayable evidence event.
117#[derive(Clone, Debug, PartialEq, Eq)]
118pub struct EvidenceEvent {
119    /// Sequence number in the cassette.
120    pub sequence: u64,
121    /// DevEnvelope kind.
122    pub kind: Symbol,
123    /// Text summary extracted from the payload when available.
124    pub summary: String,
125}
126
127/// Command intent shown in Mission Control.
128#[derive(Clone, Debug, PartialEq, Eq)]
129pub struct MissionControlIntent {
130    /// Intent kind, such as `approve` or `rerun-validation`.
131    pub kind: &'static str,
132    /// Button label.
133    pub label: &'static str,
134}
135
136/// Renders Mission Control into a Scene value.
137pub 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
157/// Builds one replay frame per cassette event plus the initial frame.
158pub 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
168/// Extracts evidence events directly from a Dev Cassette.
169pub 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
189/// The baseline Mission Control command intents.
190pub 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}