Skip to main content

sim_lib_agent/atelier/
mission_control.rs

1//! Deterministic Mission Control fixtures for agent-operated Atelier runs.
2
3use super::{
4    AgentMission, GuardCapability, HumanDecisionPoint, MissionRun, WorkspaceLease,
5    WorkspaceLeaseConflict, WorkspaceLeaseMode, detect_workspace_lease_conflicts,
6};
7use sim_kernel::{Expr, Result, Symbol};
8use sim_lib_stream_core::{DevCassette, DevEvent, LatencyClass};
9
10/// Fake Mission Control data used by UI and replay tests.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct MissionControlFixture {
13    /// Mission rendered by Mission Control.
14    pub mission: AgentMission,
15    /// Replayable evidence stream for the mission.
16    pub cassette: DevCassette,
17    /// Lease conflicts surfaced to the operator.
18    pub conflicts: Vec<WorkspaceLeaseConflict>,
19    /// Structured mission snapshot as SIM expression data.
20    pub snapshot: Expr,
21}
22
23/// Builds a deterministic fake Mission Control fixture with no live model.
24pub fn fake_mission_control_fixture() -> Result<MissionControlFixture> {
25    let mission = mission();
26    let conflicting = AgentMission::new(
27        Symbol::qualified("agent/mission", "docs-refresh"),
28        "sim-agent-net",
29    )
30    .with_goal("Refresh generated docs")
31    .with_lease(WorkspaceLease::crate_name("sim-agent-net", "sim-lib-agent"));
32    let conflicts = detect_workspace_lease_conflicts(&[mission.clone(), conflicting]);
33    let cassette = evidence_cassette(mission.evidence_stream().clone())?;
34    let snapshot = mission_snapshot(&mission, &conflicts);
35    Ok(MissionControlFixture {
36        mission,
37        cassette,
38        conflicts,
39        snapshot,
40    })
41}
42
43fn mission() -> AgentMission {
44    AgentMission::new(
45        Symbol::qualified("agent/mission", "mission-control-fixture"),
46        "sim-agent-net",
47    )
48    .with_goal("Render Mission Control")
49    .with_scope_summary("agent mission state and deterministic evidence")
50    .with_capability(GuardCapability::EditRepo("sim-agent-net".to_owned()))
51    .with_capability(GuardCapability::RunValidation("sim-agent-net".to_owned()))
52    .with_capability(GuardCapability::RegenDocs("sim-agent-net".to_owned()))
53    .with_lease(WorkspaceLease::crate_name("sim-agent-net", "sim-lib-agent"))
54    .with_lease(
55        WorkspaceLease::file(
56            "sim-agent-net",
57            "crates/sim-lib-agent/src/atelier/mission_control.rs",
58        )
59        .for_read(),
60    )
61    .with_lease(WorkspaceLease::ide_object(Symbol::qualified(
62        "ide/object",
63        "agent-mission-control",
64    )))
65    .with_validation(MissionRun::new(
66        "agent-view-tests",
67        "cargo test -p sim-lib-view-agent mission_control",
68    ))
69    .with_docs_run(MissionRun::new(
70        "simdoc",
71        "cargo run -p xtask -- simdoc --check",
72    ))
73    .with_decision_point(HumanDecisionPoint::new(
74        "approve",
75        "Approve the Mission Control change",
76    ))
77    .with_recipe_pattern(Symbol::new("a30-009-agentic-workflow"))
78}
79
80fn evidence_cassette(stream_id: Symbol) -> Result<DevCassette> {
81    let node = Symbol::qualified("atelier/agent", "mission-control");
82    let events = vec![
83        DevEvent::new(
84            "retrieval",
85            node.clone(),
86            LatencyClass::OfflineRender,
87            payload("Cartographer ranked Mission Control context"),
88        )?,
89        DevEvent::new(
90            "guard",
91            node.clone(),
92            LatencyClass::Interactive,
93            payload("Guard accepted sim-agent-net lease"),
94        )?,
95        DevEvent::validate(
96            node.clone(),
97            payload("cargo test -p sim-lib-view-agent mission_control"),
98        )?,
99        DevEvent::new(
100            "human-gate",
101            node.clone(),
102            LatencyClass::Interactive,
103            payload("approval pending"),
104        )?,
105        DevEvent::new(
106            "reflect",
107            node,
108            LatencyClass::OfflineRender,
109            payload("F6 attribution cites cassette evidence"),
110        )?,
111    ];
112    DevCassette::from_events(stream_id, events)
113}
114
115fn mission_snapshot(mission: &AgentMission, conflicts: &[WorkspaceLeaseConflict]) -> Expr {
116    Expr::Map(vec![
117        key("id", Expr::Symbol(mission.id().clone())),
118        key("goal", Expr::String(mission.goal().to_owned())),
119        key(
120            "recipe-pattern",
121            Expr::Symbol(mission.recipe_pattern().clone()),
122        ),
123        key(
124            "roles",
125            Expr::List(
126                mission
127                    .roles()
128                    .iter()
129                    .map(|role| Expr::Symbol(role.as_symbol()))
130                    .collect(),
131            ),
132        ),
133        key(
134            "validations",
135            Expr::List(
136                mission
137                    .validations()
138                    .iter()
139                    .map(|run| Expr::String(run.label.clone()))
140                    .collect(),
141            ),
142        ),
143        key(
144            "human-gates",
145            Expr::List(
146                mission
147                    .decision_points()
148                    .iter()
149                    .map(|point| Expr::String(point.id.clone()))
150                    .collect(),
151            ),
152        ),
153        key(
154            "conflicts",
155            Expr::List(conflicts.iter().map(conflict_expr).collect()),
156        ),
157    ])
158}
159
160fn conflict_expr(conflict: &WorkspaceLeaseConflict) -> Expr {
161    Expr::Map(vec![
162        key("left-mission", Expr::Symbol(conflict.left_mission.clone())),
163        key(
164            "right-mission",
165            Expr::Symbol(conflict.right_mission.clone()),
166        ),
167        key("left-target", Expr::String(conflict.left.target.clone())),
168        key("right-target", Expr::String(conflict.right.target.clone())),
169        key(
170            "mode",
171            Expr::Symbol(Symbol::new(match conflict.left.mode {
172                WorkspaceLeaseMode::SharedRead => "shared-read",
173                WorkspaceLeaseMode::ExclusiveWrite => "exclusive-write",
174            })),
175        ),
176    ])
177}
178
179fn payload(summary: &str) -> Expr {
180    Expr::Map(vec![key("summary", Expr::String(summary.to_owned()))])
181}
182
183fn key(name: &str, value: Expr) -> (Expr, Expr) {
184    (Expr::Symbol(Symbol::new(name)), value)
185}