Skip to main content

sim_lib_agent/atelier/
mission.rs

1//! Agent missions and recipe-pattern descriptors.
2
3use super::{
4    guard::GuardCapability,
5    mission_lease::{WorkspaceLease, leases_expr},
6};
7use crate::{AgentPattern, AgentPatternSlot};
8use sim_kernel::{Expr, Symbol};
9
10/// Structured unit of Atelier agent work.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct AgentMission {
13    id: Symbol,
14    goal: String,
15    scope: MissionScope,
16    allowed_repos: Vec<String>,
17    denied_paths: Vec<String>,
18    code_free_repos: Vec<String>,
19    capabilities: Vec<GuardCapability>,
20    leases: Vec<WorkspaceLease>,
21    validations: Vec<MissionRun>,
22    docs_runs: Vec<MissionRun>,
23    decision_points: Vec<HumanDecisionPoint>,
24    recipe_pattern: Symbol,
25    roles: Vec<AtelierAgentRole>,
26    evidence_stream: Symbol,
27}
28
29impl AgentMission {
30    /// Builds a mission scoped to one leased repository.
31    pub fn new(id: Symbol, leased_repo: impl Into<String>) -> Self {
32        let leased_repo = leased_repo.into();
33        Self {
34            id: id.clone(),
35            goal: "guarded Atelier mission".to_owned(),
36            scope: MissionScope::repo(leased_repo.clone()),
37            allowed_repos: vec![leased_repo.clone()],
38            denied_paths: vec![".meta-workspace/".to_owned()],
39            code_free_repos: Vec::new(),
40            capabilities: Vec::new(),
41            leases: Vec::new(),
42            validations: Vec::new(),
43            docs_runs: Vec::new(),
44            decision_points: Vec::new(),
45            recipe_pattern: Symbol::new("a30-013-code-generation"),
46            roles: default_roles(),
47            evidence_stream: Symbol::qualified("atelier/dev", id.name.as_ref()),
48        }
49    }
50
51    /// Adds a granted guard capability.
52    pub fn with_capability(mut self, capability: GuardCapability) -> Self {
53        push_unique(&mut self.capabilities, capability);
54        self
55    }
56
57    /// Sets the human-readable mission goal.
58    pub fn with_goal(mut self, goal: impl Into<String>) -> Self {
59        self.goal = goal.into();
60        self
61    }
62
63    /// Sets a concise scope summary while preserving the primary repository.
64    pub fn with_scope_summary(mut self, summary: impl Into<String>) -> Self {
65        self.scope.summary = summary.into();
66        self
67    }
68
69    /// Adds an allowed repository to the mission scope.
70    pub fn with_allowed_repo(mut self, repo: impl Into<String>) -> Self {
71        push_unique(&mut self.allowed_repos, repo.into());
72        self
73    }
74
75    /// Adds a denied path prefix such as `.meta-workspace/`.
76    pub fn with_denied_path(mut self, path: impl Into<String>) -> Self {
77        push_unique(&mut self.denied_paths, path.into());
78        self
79    }
80
81    /// Adds a repository that must stay Rust-code-free.
82    pub fn with_code_free_repo(mut self, repo: impl Into<String>) -> Self {
83        push_unique(&mut self.code_free_repos, repo.into());
84        self
85    }
86
87    /// Adds a workspace lease.
88    pub fn with_lease(mut self, lease: WorkspaceLease) -> Self {
89        self.leases.push(lease);
90        self
91    }
92
93    /// Adds a required validation command.
94    pub fn with_validation(mut self, run: MissionRun) -> Self {
95        self.validations.push(run);
96        self
97    }
98
99    /// Adds a required docs command.
100    pub fn with_docs_run(mut self, run: MissionRun) -> Self {
101        self.docs_runs.push(run);
102        self
103    }
104
105    /// Adds a human decision point.
106    pub fn with_decision_point(mut self, point: HumanDecisionPoint) -> Self {
107        self.decision_points.push(point);
108        self
109    }
110
111    /// Selects the 30-agent recipe pattern that frames this mission.
112    pub fn with_recipe_pattern(mut self, recipe_pattern: Symbol) -> Self {
113        self.recipe_pattern = recipe_pattern;
114        self
115    }
116
117    /// Sets the deterministic Dev Cassette stream id.
118    pub fn with_evidence_stream(mut self, evidence_stream: Symbol) -> Self {
119        self.evidence_stream = evidence_stream;
120        self
121    }
122
123    /// Returns the mission id.
124    pub fn id(&self) -> &Symbol {
125        &self.id
126    }
127
128    /// Returns the human-readable mission goal.
129    pub fn goal(&self) -> &str {
130        &self.goal
131    }
132
133    /// Returns the mission scope.
134    pub fn scope(&self) -> &MissionScope {
135        &self.scope
136    }
137
138    /// Returns the primary leased repository.
139    pub fn leased_repo(&self) -> &str {
140        &self.scope.primary_repo
141    }
142
143    /// Returns the repository allow-list.
144    pub fn allowed_repos(&self) -> &[String] {
145        &self.allowed_repos
146    }
147
148    /// Returns the denied path prefixes.
149    pub fn denied_paths(&self) -> &[String] {
150        &self.denied_paths
151    }
152
153    /// Returns the repositories that must stay Rust-code-free.
154    pub fn code_free_repos(&self) -> &[String] {
155        &self.code_free_repos
156    }
157
158    /// Returns the granted guard capabilities.
159    pub fn capabilities(&self) -> &[GuardCapability] {
160        &self.capabilities
161    }
162
163    /// Returns the workspace leases.
164    pub fn leases(&self) -> &[WorkspaceLease] {
165        &self.leases
166    }
167
168    /// Returns the required validation commands.
169    pub fn validations(&self) -> &[MissionRun] {
170        &self.validations
171    }
172
173    /// Returns the required docs commands.
174    pub fn docs_runs(&self) -> &[MissionRun] {
175        &self.docs_runs
176    }
177
178    /// Returns the declared human decision points.
179    pub fn decision_points(&self) -> &[HumanDecisionPoint] {
180        &self.decision_points
181    }
182
183    /// Returns the recipe pattern that frames this mission.
184    pub fn recipe_pattern(&self) -> &Symbol {
185        &self.recipe_pattern
186    }
187
188    /// Returns the agent roles assigned to this mission.
189    pub fn roles(&self) -> &[AtelierAgentRole] {
190        &self.roles
191    }
192
193    /// Returns the deterministic Dev Cassette evidence stream id.
194    pub fn evidence_stream(&self) -> &Symbol {
195        &self.evidence_stream
196    }
197
198    /// Builds the SUP.56 agent-pattern descriptor for this mission.
199    pub fn descriptor(&self) -> AgentPattern {
200        AgentPattern {
201            id: self.id.clone(),
202            description: Some(format!(
203                "Atelier mission using recipe pattern {}",
204                self.recipe_pattern
205            )),
206            sense: slots([
207                ("goal", self.goal.as_str()),
208                ("scope", self.scope.summary.as_str()),
209            ]),
210            model: slots([("fake-cassette-runner", "deterministic runner surface")]),
211            plan: slots([("f3-decompose", "split the mission into leased sub-tasks")]),
212            act: role_slots(&self.roles),
213            memory: slots([("f2-radar-retrieve", "rank query over the Atelier index")]),
214            tools: slots(
215                self.validations
216                    .iter()
217                    .chain(self.docs_runs.iter())
218                    .map(|run| {
219                        (
220                            run.label.as_str(),
221                            "required mission command recorded in evidence",
222                        )
223                    }),
224            ),
225            policy: slots([
226                ("allowed-repos", "repository allow-list"),
227                ("denied-paths", "path deny-list"),
228                ("guard-capabilities", "granted Guideline Firewall tokens"),
229            ]),
230            evaluation: slots([
231                (
232                    "f3-reflect",
233                    "reflect over edits against retrieved evidence",
234                ),
235                ("f4-confidence", "statistics-scored confidence"),
236            ]),
237            guardrail: slots([("guideline-firewall", "capability-gated action checks")]),
238            trace: slots([
239                (
240                    "dev-cassette-ledger",
241                    "read, edit, guard, validation, and refusal evidence",
242                ),
243                (
244                    "f6-attribution",
245                    "attribution card from the evidence ledger",
246                ),
247            ]),
248            extra: vec![
249                (
250                    Symbol::new("recipe-pattern"),
251                    Expr::Symbol(self.recipe_pattern.clone()),
252                ),
253                (
254                    Symbol::new("allowed-repos"),
255                    string_list_expr(&self.allowed_repos),
256                ),
257                (Symbol::new("leases"), leases_expr(&self.leases)),
258            ],
259        }
260    }
261
262    /// Encodes the mission as ordinary SIM expression data.
263    pub fn as_expr(&self) -> Expr {
264        Expr::Map(vec![
265            key("kind", Expr::Symbol(Symbol::new("agent-mission"))),
266            key("id", Expr::Symbol(self.id.clone())),
267            key("goal", Expr::String(self.goal.clone())),
268            key("scope", self.scope.as_expr()),
269            key("allowed-repos", string_list_expr(&self.allowed_repos)),
270            key("denied-paths", string_list_expr(&self.denied_paths)),
271            key("code-free-repos", string_list_expr(&self.code_free_repos)),
272            key("leases", leases_expr(&self.leases)),
273            key("validations", runs_expr(&self.validations)),
274            key("docs-runs", runs_expr(&self.docs_runs)),
275            key("decision-points", decisions_expr(&self.decision_points)),
276            key("recipe-pattern", Expr::Symbol(self.recipe_pattern.clone())),
277            key("descriptor", self.descriptor().as_expr()),
278        ])
279    }
280}
281
282/// Mission scope with a primary repository and concise summary.
283#[derive(Clone, Debug, PartialEq, Eq)]
284pub struct MissionScope {
285    /// Primary repository the mission operates in.
286    pub primary_repo: String,
287    /// Concise human-readable summary of the scope.
288    pub summary: String,
289}
290
291impl MissionScope {
292    /// Builds a scope for a single repository with a default summary.
293    pub fn repo(repo: impl Into<String>) -> Self {
294        let repo = repo.into();
295        Self {
296            primary_repo: repo.clone(),
297            summary: format!("changes in {repo}"),
298        }
299    }
300
301    fn as_expr(&self) -> Expr {
302        Expr::Map(vec![
303            key("primary-repo", Expr::String(self.primary_repo.clone())),
304            key("summary", Expr::String(self.summary.clone())),
305        ])
306    }
307}
308
309/// Required validation or docs command for a mission.
310#[derive(Clone, Debug, PartialEq, Eq)]
311pub struct MissionRun {
312    /// Short label identifying the command.
313    pub label: String,
314    /// Shell command to run.
315    pub command: String,
316}
317
318impl MissionRun {
319    /// Builds a mission run from a label and command.
320    pub fn new(label: impl Into<String>, command: impl Into<String>) -> Self {
321        Self {
322            label: label.into(),
323            command: command.into(),
324        }
325    }
326
327    pub(crate) fn as_expr(&self) -> Expr {
328        Expr::Map(vec![
329            key("label", Expr::String(self.label.clone())),
330            key("command", Expr::String(self.command.clone())),
331        ])
332    }
333}
334
335/// Human decision point declared by a mission.
336#[derive(Clone, Debug, PartialEq, Eq)]
337pub struct HumanDecisionPoint {
338    /// Identifier for the decision point.
339    pub id: String,
340    /// Question posed to the human.
341    pub question: String,
342}
343
344impl HumanDecisionPoint {
345    /// Builds a human decision point from an id and question.
346    pub fn new(id: impl Into<String>, question: impl Into<String>) -> Self {
347        Self {
348            id: id.into(),
349            question: question.into(),
350        }
351    }
352
353    pub(crate) fn as_expr(&self) -> Expr {
354        Expr::Map(vec![
355            key("id", Expr::String(self.id.clone())),
356            key("question", Expr::String(self.question.clone())),
357        ])
358    }
359}
360
361/// Agent roles used by Atelier missions.
362#[derive(Clone, Debug, PartialEq, Eq)]
363pub enum AtelierAgentRole {
364    /// Maps the repository and plans the mission.
365    Cartographer,
366    /// Applies source edits.
367    Editor,
368    /// Handles codec-specific work.
369    CodecSpecialist,
370    /// Enforces Guideline Firewall capability checks.
371    Guard,
372    /// Runs required validation commands.
373    Validator,
374    /// Runs documentation commands.
375    DocsAgent,
376    /// Updates pinned commits.
377    PinAgent,
378    /// Reviews the produced changes.
379    Reviewer,
380    /// Hands off to a human decision point.
381    HumanGate,
382}
383
384impl AtelierAgentRole {
385    /// Returns the role as a qualified `atelier/agent` symbol.
386    pub fn as_symbol(&self) -> Symbol {
387        Symbol::qualified("atelier/agent", self.label())
388    }
389
390    /// Returns the stable kebab-case label for this role.
391    pub fn label(&self) -> &'static str {
392        match self {
393            Self::Cartographer => "cartographer",
394            Self::Editor => "editor",
395            Self::CodecSpecialist => "codec-specialist",
396            Self::Guard => "guard",
397            Self::Validator => "validator",
398            Self::DocsAgent => "docs-agent",
399            Self::PinAgent => "pin-agent",
400            Self::Reviewer => "reviewer",
401            Self::HumanGate => "human-gate",
402        }
403    }
404}
405
406fn default_roles() -> Vec<AtelierAgentRole> {
407    vec![
408        AtelierAgentRole::Cartographer,
409        AtelierAgentRole::Editor,
410        AtelierAgentRole::CodecSpecialist,
411        AtelierAgentRole::Guard,
412        AtelierAgentRole::Validator,
413        AtelierAgentRole::DocsAgent,
414        AtelierAgentRole::PinAgent,
415        AtelierAgentRole::Reviewer,
416        AtelierAgentRole::HumanGate,
417    ]
418}
419
420fn role_slots(roles: &[AtelierAgentRole]) -> Vec<AgentPatternSlot> {
421    roles
422        .iter()
423        .map(|role| AgentPatternSlot {
424            name: role.as_symbol(),
425            description: Some("Atelier mission handoff role".to_owned()),
426            extra: Vec::new(),
427        })
428        .collect()
429}
430
431fn slots(
432    slots: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
433) -> Vec<AgentPatternSlot> {
434    slots
435        .into_iter()
436        .map(|(name, description)| AgentPatternSlot {
437            name: Symbol::new(name.into()),
438            description: Some(description.into()),
439            extra: Vec::new(),
440        })
441        .collect()
442}
443
444fn runs_expr(runs: &[MissionRun]) -> Expr {
445    Expr::List(runs.iter().map(MissionRun::as_expr).collect())
446}
447
448fn decisions_expr(decisions: &[HumanDecisionPoint]) -> Expr {
449    Expr::List(decisions.iter().map(HumanDecisionPoint::as_expr).collect())
450}
451
452fn string_list_expr(values: &[String]) -> Expr {
453    Expr::List(values.iter().cloned().map(Expr::String).collect())
454}
455
456pub(crate) fn key(name: &str, value: Expr) -> (Expr, Expr) {
457    (Expr::Symbol(Symbol::new(name)), value)
458}
459
460fn push_unique<T: PartialEq>(values: &mut Vec<T>, value: T) {
461    if !values.contains(&value) {
462        values.push(value);
463    }
464}