Skip to main content

agent_doc_element/
lib.rs

1//! Pure element descriptors for agent-doc document components.
2//!
3//! Element crates describe component semantics only: marker names, aliases,
4//! ownership, realtime policy, and scheduling role. They do not read or write
5//! files, schedule turns, open IPC, invoke tmux, mutate snapshots, or load
6//! plugins. Runtime crates consume these descriptors.
7//!
8//! Architecture: every supported element may own a small pure realtime model
9//! for its local state and reconciliation rules. The document model composes
10//! those element models and owns cross-element invariants such as backlog to
11//! queue projection, active-head marker placement, and signals that observe
12//! queue/turn/supervisor state.
13
14pub mod element;
15pub mod id;
16
17pub use element::Component;
18
19/// Where an element descriptor comes from.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ElementSource {
22    BuiltIn,
23    Plugin,
24}
25
26/// Marker shape used by the document.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ElementShape {
29    /// Bounded region: `<!-- agent:name -->...<!-- /agent:name -->`.
30    Component,
31    /// Single marker inside another component, such as an exchange boundary.
32    InlineMarker,
33}
34
35/// Who owns conflict authority for this element.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ElementAuthority {
38    /// Shared user/agent surface; realtime merge must preserve operator text.
39    SharedOperatorAuthoritative,
40    /// User-visible tracked work mutated only through granular operations.
41    GranularTrackedWork,
42    /// Derived projection of runtime state; source state lives elsewhere.
43    DerivedProjection,
44    /// Durable completed-work archive.
45    Archive,
46    /// Realtime signals surface with user-definable signal declarations.
47    Signals,
48}
49
50/// How writes to this element are allowed to happen.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum ElementWritePolicy {
53    /// Merge into the operator-visible buffer; never full-replace from stale state.
54    MergeOnly,
55    /// Use granular add/edit/done/reorder/clear-style operations.
56    GranularOnly,
57    /// Rewrite from current runtime state as a projection, preserving unrelated text.
58    ProjectionOnly,
59    /// Append/archive only; not a runnable work source.
60    ArchiveOnly,
61    /// Update signal definitions/readings through signal-specific reconciliation.
62    SignalReconcile,
63}
64
65/// Scheduling relationship with the realtime queue.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum ElementSchedulingRole {
68    None,
69    ActiveQueue,
70    RunnableWorkSource,
71    ParkedWorkSource,
72    ReviewGate,
73    CompletionArchive,
74    Status,
75    Signals,
76}
77
78/// Local realtime model owned by an element crate.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum ElementRealtimeModel {
81    /// No local realtime state beyond parsed component text.
82    None,
83    /// Conversation/prompt state inside `agent:exchange`.
84    Exchange,
85    /// Tracked work list state shared by backlog/review/icebox variants.
86    TrackedItems,
87    /// Active queue state: heads, pins, auto-DAG ordering, and projections.
88    Queue,
89    /// Runtime status projection.
90    Status,
91    /// Completed-work archive projection.
92    Archive,
93    /// Realtime signal definitions and readings.
94    Signals,
95    /// Inline response boundary marker inside an exchange.
96    Boundary,
97    /// Unknown or pluginless component content.
98    Unknown,
99}
100
101/// Whether this element participates in document-level composition.
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum ElementCompositionRole {
104    /// Independent element; local reconciliation is enough.
105    LocalOnly,
106    /// Feeds another element, e.g. backlog feeds queue.
107    Producer,
108    /// Projects or consumes state from another element, e.g. queue from backlog.
109    Consumer,
110    /// Observes multiple models, e.g. signals.
111    Observer,
112    /// Archive target for completed tracked work.
113    ArchiveTarget,
114}
115
116/// Built-in descriptor shape. Plugin loaders can convert this to an owned
117/// descriptor later without changing element crate semantics.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub struct ElementDescriptor {
120    pub name: &'static str,
121    pub aliases: &'static [&'static str],
122    pub source: ElementSource,
123    pub shape: ElementShape,
124    pub authority: ElementAuthority,
125    pub write_policy: ElementWritePolicy,
126    pub scheduling_role: ElementSchedulingRole,
127    pub realtime_model: ElementRealtimeModel,
128    pub composition_role: ElementCompositionRole,
129    /// True when realtime processing may update this element outside turn
130    /// closeout. Commits remain owned by the turn lifecycle.
131    pub realtime: bool,
132}
133
134impl ElementDescriptor {
135    pub fn matches_name(self, name: &str) -> bool {
136        self.name == name || self.aliases.contains(&name)
137    }
138
139    pub fn canonical_name(self, name: &str) -> Option<&'static str> {
140        self.matches_name(name).then_some(self.name)
141    }
142
143    pub fn as_registration(self) -> ElementRegistration {
144        ElementRegistration {
145            name: self.name.to_string(),
146            aliases: self
147                .aliases
148                .iter()
149                .map(|alias| (*alias).to_string())
150                .collect(),
151            source: self.source,
152            shape: self.shape,
153            authority: self.authority,
154            write_policy: self.write_policy,
155            scheduling_role: self.scheduling_role,
156            realtime_model: self.realtime_model,
157            composition_role: self.composition_role,
158            realtime: self.realtime,
159        }
160    }
161}
162
163/// Owned descriptor shape suitable for future plugin registration.
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct ElementRegistration {
166    pub name: String,
167    pub aliases: Vec<String>,
168    pub source: ElementSource,
169    pub shape: ElementShape,
170    pub authority: ElementAuthority,
171    pub write_policy: ElementWritePolicy,
172    pub scheduling_role: ElementSchedulingRole,
173    pub realtime_model: ElementRealtimeModel,
174    pub composition_role: ElementCompositionRole,
175    pub realtime: bool,
176}
177
178impl ElementRegistration {
179    pub fn matches_name(&self, name: &str) -> bool {
180        self.name == name || self.aliases.iter().any(|alias| alias == name)
181    }
182}
183
184/// Future plugin boundary: loaders register descriptors, while element crates
185/// remain pure and effect-free.
186pub trait ElementPlugin {
187    fn element_descriptors(&self) -> Vec<ElementRegistration>;
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    const BACKLOG: ElementDescriptor = ElementDescriptor {
195        name: "backlog",
196        aliases: &["pending"],
197        source: ElementSource::BuiltIn,
198        shape: ElementShape::Component,
199        authority: ElementAuthority::GranularTrackedWork,
200        write_policy: ElementWritePolicy::GranularOnly,
201        scheduling_role: ElementSchedulingRole::RunnableWorkSource,
202        realtime_model: ElementRealtimeModel::TrackedItems,
203        composition_role: ElementCompositionRole::Producer,
204        realtime: true,
205    };
206
207    #[test]
208    fn descriptor_matches_aliases_and_canonicalizes() {
209        assert!(BACKLOG.matches_name("backlog"));
210        assert!(BACKLOG.matches_name("pending"));
211        assert_eq!(BACKLOG.canonical_name("pending"), Some("backlog"));
212        assert_eq!(BACKLOG.canonical_name("queue"), None);
213    }
214
215    #[test]
216    fn built_in_descriptor_converts_to_plugin_registration_shape() {
217        let registration = BACKLOG.as_registration();
218        assert_eq!(registration.name, "backlog");
219        assert_eq!(registration.aliases, vec!["pending"]);
220        assert!(registration.matches_name("pending"));
221    }
222
223    #[test]
224    fn manifest_stays_pure() {
225        let manifest = include_str!("../Cargo.toml");
226        for forbidden in [
227            "agent-doc-orchestration",
228            "interprocess",
229            "notify",
230            "rusqlite",
231            "tmux-router",
232        ] {
233            assert!(
234                !manifest.contains(forbidden),
235                "agent-doc-element must stay pure; found forbidden dependency {forbidden}"
236            );
237        }
238    }
239}