Skip to main content

monoloop_contracts/
canonical.rs

1//! Provider-neutral canonical semantic units and lifecycle events.
2//!
3//! See `doc/INTERPRETER.md`. No provider-native DTOs.
4
5use crate::id::{ConnectionId, ExternalSessionId};
6use serde::{Deserialize, Serialize};
7
8/// Interpretation instance identity.
9#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct InterpretationId(String);
11
12impl InterpretationId {
13    /// Create from an explicit value.
14    pub fn new(value: impl Into<String>) -> Self {
15        Self(value.into())
16    }
17
18    /// Allocate a random id.
19    pub fn generate() -> Self {
20        Self(uuid::Uuid::new_v4().to_string())
21    }
22
23    /// Borrow the underlying string.
24    pub fn as_str(&self) -> &str {
25        &self.0
26    }
27}
28
29/// Stable unit identity within one interpretation.
30#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
31pub struct UnitId(String);
32
33impl UnitId {
34    /// Create from an explicit value.
35    pub fn new(value: impl Into<String>) -> Self {
36        Self(value.into())
37    }
38
39    /// Borrow the underlying string.
40    pub fn as_str(&self) -> &str {
41        &self.0
42    }
43}
44
45/// Flow identity (one logical dialect exchange/response).
46#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
47pub struct FlowId(String);
48
49impl FlowId {
50    /// Create from an explicit value.
51    pub fn new(value: impl Into<String>) -> Self {
52        Self(value.into())
53    }
54
55    /// Default main flow.
56    pub fn main() -> Self {
57        Self("main".into())
58    }
59
60    /// Borrow the underlying string.
61    pub fn as_str(&self) -> &str {
62        &self.0
63    }
64}
65
66/// Lane identity within a flow.
67#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
68pub struct LaneId(String);
69
70impl LaneId {
71    /// Create from an explicit value.
72    pub fn new(value: impl Into<String>) -> Self {
73        Self(value.into())
74    }
75
76    /// Response text lane.
77    pub fn response() -> Self {
78        Self("response".into())
79    }
80
81    /// Tool lane.
82    pub fn tool() -> Self {
83        Self("tool".into())
84    }
85
86    /// Reasoning summary lane.
87    pub fn reasoning() -> Self {
88        Self("reasoning".into())
89    }
90
91    /// Borrow the underlying string.
92    pub fn as_str(&self) -> &str {
93        &self.0
94    }
95}
96
97/// Tool action identity (dialect-provided or interpretation-scoped).
98#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
99pub struct ToolActionId(String);
100
101impl ToolActionId {
102    /// Create from an explicit value.
103    pub fn new(value: impl Into<String>) -> Self {
104        Self(value.into())
105    }
106
107    /// Borrow the underlying string.
108    pub fn as_str(&self) -> &str {
109        &self.0
110    }
111}
112
113/// Canonical text channel.
114#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
115pub enum TextChannel {
116    /// Public assistant response.
117    PublicResponse,
118    /// Publishable reasoning summary only (never private CoT).
119    PublicReasoningSummary,
120    /// Status / narration.
121    StatusNarration,
122    /// Quoted external content (untrusted).
123    QuotedExternalContent,
124}
125
126impl TextChannel {
127    /// Short label for console rendering.
128    pub fn label(self) -> &'static str {
129        match self {
130            Self::PublicResponse => "assistant",
131            Self::PublicReasoningSummary => "reasoning",
132            Self::StatusNarration => "status",
133            Self::QuotedExternalContent => "quoted",
134        }
135    }
136}
137
138/// Lifecycle state of a canonical unit.
139#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
140pub enum UnitState {
141    /// Fully assembled and immutable (typical for sentences).
142    Complete,
143    /// Lifecycle-bearing unit awaiting more correlated material.
144    Waiting,
145    /// Explicitly incomplete (malformed or terminated mid-assembly).
146    Incomplete,
147    /// Malformed input sealed as such.
148    Malformed,
149}
150
151/// Closed top-level canonical unit vocabulary.
152#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
153pub enum CanonicalUnit {
154    /// Complete sentence atom.
155    Text(TextSentence),
156    /// Complete structural atom (heading, code block, …).
157    Structure(StructuralAtom),
158    /// Paragraph open/close.
159    Paragraph(ParagraphBoundary),
160    /// Tool action lifecycle.
161    Tool(ToolActionEvent),
162    /// Usage observation.
163    Usage(UsageObservation),
164    /// Model/dialect diagnostic.
165    Diagnostic(ModelDiagnostic),
166    /// Semantic boundary observation (not turn completion).
167    Boundary(SemanticBoundary),
168}
169
170impl CanonicalUnit {
171    /// Short kind label.
172    pub fn kind_label(&self) -> &'static str {
173        match self {
174            Self::Text(_) => "text",
175            Self::Structure(_) => "structure",
176            Self::Paragraph(_) => "paragraph",
177            Self::Tool(_) => "tool",
178            Self::Usage(_) => "usage",
179            Self::Diagnostic(_) => "diagnostic",
180            Self::Boundary(_) => "boundary",
181        }
182    }
183}
184
185/// Complete sentence atom (immutable after emission).
186#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
187pub struct TextSentence {
188    /// Sentence identity.
189    pub sentence_id: UnitId,
190    /// Canonical channel.
191    pub channel: TextChannel,
192    /// Optional paragraph membership.
193    pub paragraph_id: Option<UnitId>,
194    /// Ordinal within the lane/paragraph.
195    pub sentence_ordinal: u64,
196    /// Complete sentence content.
197    pub content: String,
198}
199
200/// Non-sentence structural atom.
201#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
202pub struct StructuralAtom {
203    /// Structure identity.
204    pub structure_id: UnitId,
205    /// Kind of structure.
206    pub kind: StructureKind,
207    /// Complete textual payload where applicable.
208    pub content: String,
209}
210
211/// Structural kinds recognized for canonical assembly.
212#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
213pub enum StructureKind {
214    /// Heading.
215    Heading,
216    /// List item boundary.
217    ListItem,
218    /// Fenced code block.
219    CodeBlock,
220    /// Table row.
221    TableRow,
222    /// Block quote boundary.
223    BlockQuote,
224    /// Thematic break.
225    ThematicBreak,
226    /// Declared raw block.
227    RawBlock,
228}
229
230/// Paragraph open/close.
231#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
232pub struct ParagraphBoundary {
233    /// Paragraph identity.
234    pub paragraph_id: UnitId,
235    /// Opened or closed.
236    pub kind: ParagraphKind,
237    /// Channel.
238    pub channel: TextChannel,
239}
240
241/// Paragraph boundary kind.
242#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
243pub enum ParagraphKind {
244    /// Paragraph opened.
245    Opened,
246    /// Paragraph closed.
247    Closed,
248}
249
250/// Tool-action event payload.
251#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
252pub struct ToolActionEvent {
253    /// Stable tool action id.
254    pub tool_action_id: ToolActionId,
255    /// Tool name when known (complete only for ready/resolved).
256    pub tool_name: Option<String>,
257    /// Request state.
258    pub request_state: ToolRequestState,
259    /// Execution state.
260    pub execution_state: ToolExecutionState,
261    /// Result state.
262    pub result_state: ToolResultState,
263    /// Complete request payload JSON when ready (not partial fragments).
264    pub request_payload: Option<String>,
265    /// Complete result payload when resolved.
266    pub result_payload: Option<String>,
267    /// Terminal outcome when known.
268    pub terminal_outcome: Option<ToolTerminalOutcome>,
269    /// What the action is waiting for (when waiting).
270    pub waiting_for: Option<String>,
271}
272
273/// Tool request assembly state.
274#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
275pub enum ToolRequestState {
276    /// Still assembling.
277    Assembling,
278    /// Complete and syntactically valid.
279    Ready,
280    /// Malformed.
281    Malformed,
282    /// Incomplete at termination.
283    Incomplete,
284}
285
286/// Observed execution state from dialect (not host Loop execution).
287#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
288pub enum ToolExecutionState {
289    /// Not observed in dialect stream.
290    NotObserved,
291    /// Waiting for execution/result.
292    Waiting,
293    /// Running (if dialect reports it).
294    Running,
295    /// Terminal observed.
296    Terminal,
297}
298
299/// Result assembly state.
300#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
301pub enum ToolResultState {
302    /// No result yet.
303    Absent,
304    /// Assembling.
305    Assembling,
306    /// Complete.
307    Complete,
308    /// Malformed.
309    Malformed,
310    /// Incomplete.
311    Incomplete,
312}
313
314/// Terminal tool outcome as observed in the dialect (not host tool runtime).
315#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
316pub enum ToolTerminalOutcome {
317    /// Success.
318    Success,
319    /// Failure.
320    Failure,
321    /// Cancelled.
322    Cancelled,
323    /// Lost / unknown.
324    Lost,
325}
326
327/// Usage observation (unavailable is not zero).
328#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
329pub struct UsageObservation {
330    /// Input tokens.
331    pub input_tokens: TokenCount,
332    /// Output tokens.
333    pub output_tokens: TokenCount,
334}
335
336/// Measured or unavailable count.
337#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
338pub enum TokenCount {
339    /// Measured value.
340    Measured(u64),
341    /// Not supplied by dialect.
342    Unavailable,
343}
344
345/// Safe model/dialect diagnostic.
346#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
347pub struct ModelDiagnostic {
348    /// Classification.
349    pub kind: DiagnosticKind,
350    /// Bounded safe message (no secrets/raw bodies).
351    pub message: String,
352}
353
354/// Diagnostic kinds.
355#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
356pub enum DiagnosticKind {
357    /// Dialect warning.
358    DialectWarning,
359    /// Model-reported error (normalized).
360    ModelReportedError,
361    /// Unsupported event.
362    UnsupportedEvent,
363    /// Malformed frame.
364    MalformedFrame,
365    /// Malformed semantic payload.
366    MalformedSemanticPayload,
367    /// Incomplete text at termination.
368    IncompleteText,
369    /// Incomplete structure.
370    IncompleteStructure,
371    /// Incomplete tool.
372    IncompleteToolAction,
373    /// Limit exceeded.
374    LimitExceeded,
375}
376
377/// Semantic boundary (not turn/task completion authority).
378#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
379pub struct SemanticBoundary {
380    /// Boundary kind.
381    pub kind: BoundaryKind,
382}
383
384/// Boundary kinds.
385#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
386pub enum BoundaryKind {
387    /// Response started.
388    ResponseStarted,
389    /// Channel started.
390    ChannelStarted,
391    /// Channel finished.
392    ChannelFinished,
393    /// Response finished (dialect-level).
394    ResponseFinished,
395    /// Usage finalized.
396    UsageFinalized,
397}
398
399/// Dialect-observed source time for a complete (or lifecycle) unit.
400///
401/// Observational only: does **not** establish causality, turn success, or
402/// authority. Lane ordinal / explicit causal parent remain primary. Values are
403/// provider clock milliseconds when the dialect supplies them (e.g. Grok ACP
404/// `params._meta.agentTimestampMs`); absent when the dialect does not.
405#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
406pub struct SourceTimeObservation {
407    /// Earliest dialect-reported source timestamp (ms) among contributing fragments.
408    pub first_ms: u64,
409    /// Latest dialect-reported source timestamp (ms) among contributing fragments.
410    pub last_ms: u64,
411}
412
413impl SourceTimeObservation {
414    /// Build from a single observed timestamp.
415    pub fn point(ms: u64) -> Self {
416        Self {
417            first_ms: ms,
418            last_ms: ms,
419        }
420    }
421
422    /// Merge two observations (min first, max last).
423    pub fn merge(self, other: Self) -> Self {
424        Self {
425            first_ms: self.first_ms.min(other.first_ms),
426            last_ms: self.last_ms.max(other.last_ms),
427        }
428    }
429
430    /// Extend with an optional single timestamp.
431    pub fn include(self, ms: Option<u64>) -> Self {
432        match ms {
433            Some(t) => self.merge(Self::point(t)),
434            None => self,
435        }
436    }
437
438    /// From optional first/last (None if neither known).
439    pub fn from_bounds(first: Option<u64>, last: Option<u64>) -> Option<Self> {
440        match (first, last) {
441            (Some(f), Some(l)) => Some(Self {
442                first_ms: f.min(l),
443                last_ms: f.max(l),
444            }),
445            (Some(t), None) | (None, Some(t)) => Some(Self::point(t)),
446            (None, None) => None,
447        }
448    }
449}
450
451/// Correlation + lifecycle envelope for one unit generation.
452#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
453pub struct CanonicalUnitSnapshot {
454    /// Unit identity (stable across generations).
455    pub unit_id: UnitId,
456    /// Monotonic generation (starts at 1).
457    pub unit_generation: u64,
458    /// Lifecycle state.
459    pub unit_state: UnitState,
460    /// Interpretation identity.
461    pub interpretation_id: InterpretationId,
462    /// Connection identity.
463    pub connection_id: ConnectionId,
464    /// External session when present (e.g. Grok sessionId).
465    pub external_session_id: Option<ExternalSessionId>,
466    /// Flow.
467    pub flow_id: FlowId,
468    /// Lane.
469    pub lane_id: LaneId,
470    /// Strict ordinal within the lane.
471    pub lane_ordinal: u64,
472    /// Optional causal parent unit.
473    pub causal_parent_id: Option<UnitId>,
474    /// Optional dialect source time (observational; not causality).
475    pub source_time: Option<SourceTimeObservation>,
476    /// Optional dialect stream step / sequence id (observational; not causality).
477    ///
478    /// Examples: Antigravity ACP `update._meta.stepIdx`, numeric `messageId`.
479    /// Used by human projection when wall-clock source times are absent.
480    pub source_step: Option<u64>,
481    /// Canonical unit content allowed for this state.
482    pub unit: CanonicalUnit,
483}
484
485/// Unit lifecycle event (closed vocabulary).
486#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
487pub enum CanonicalUnitEvent {
488    /// Unit created (often already complete for sentences).
489    Created(CanonicalUnitSnapshot),
490    /// Lifecycle-bearing unit advanced.
491    Advanced(CanonicalUnitSnapshot),
492    /// Unit completed.
493    Completed(CanonicalUnitSnapshot),
494    /// Unit incomplete at termination or failure.
495    Incomplete(CanonicalUnitSnapshot),
496}
497
498impl CanonicalUnitEvent {
499    /// Borrow the snapshot.
500    pub fn snapshot(&self) -> &CanonicalUnitSnapshot {
501        match self {
502            Self::Created(s) | Self::Advanced(s) | Self::Completed(s) | Self::Incomplete(s) => s,
503        }
504    }
505
506    /// Lifecycle label for console.
507    pub fn lifecycle_label(&self) -> &'static str {
508        match self {
509            Self::Created(_) => "created",
510            Self::Advanced(_) => "advanced",
511            Self::Completed(_) => "completed",
512            Self::Incomplete(_) => "incomplete",
513        }
514    }
515}
516
517/// Exactly one terminal report per interpretation.
518#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
519pub struct InterpretationEnd {
520    /// Interpretation identity.
521    pub interpretation_id: InterpretationId,
522    /// Connection identity.
523    pub connection_id: ConnectionId,
524    /// External session when present.
525    pub external_session_id: Option<ExternalSessionId>,
526    /// Terminal kind.
527    pub kind: InterpretationEndKind,
528    /// Canonical events published.
529    pub canonical_event_count: u64,
530    /// Completed sentences.
531    pub completed_sentence_count: u64,
532    /// Completed structures.
533    pub completed_structure_count: u64,
534    /// Unresolved text bytes at end.
535    pub unresolved_text_bytes: u64,
536    /// Source bytes consumed.
537    pub source_bytes_consumed: u64,
538    /// Bounded safe diagnostics.
539    pub safe_diagnostics: Vec<String>,
540}
541
542/// Interpretation terminal kinds.
543#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
544pub enum InterpretationEndKind {
545    /// Clean complete.
546    Complete,
547    /// Cancelled.
548    Cancelled,
549    /// Terminated.
550    Terminated,
551    /// Transport failed.
552    TransportFailed,
553    /// Dialect failed.
554    DialectFailed,
555    /// Limit exceeded.
556    LimitExceeded,
557    /// Invariant failed.
558    InvariantFailed,
559}
560
561/// Stream events delivered to subscribers (Interpreter output + end).
562///
563/// `Unit` is boxed so the enum stays small (strict Clippy large-variant rule).
564#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
565pub enum InterpreterOutputEvent {
566    /// Canonical unit lifecycle.
567    Unit(Box<CanonicalUnitEvent>),
568    /// Interpretation ended.
569    Ended(InterpretationEnd),
570}
571
572impl InterpreterOutputEvent {
573    /// Wrap a unit lifecycle event.
574    pub fn unit(event: CanonicalUnitEvent) -> Self {
575        Self::Unit(Box::new(event))
576    }
577}