Skip to main content

monoloop_contracts/
loop_types.rs

1//! Loop identities, limits, and provider-neutral outbound tool results.
2//!
3//! See `doc/THE_LOOP.md`. No concrete tools or dialect encoding.
4
5use crate::canonical::{InterpretationId, ToolActionId, UnitId};
6use crate::id::{ConnectionId, ExternalSessionId, MonoloopRunId};
7use serde::{Deserialize, Serialize};
8
9/// Loop instance identity.
10#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct LoopId(String);
12
13impl LoopId {
14    /// Create from an explicit value.
15    pub fn new(value: impl Into<String>) -> Self {
16        Self(value.into())
17    }
18
19    /// Allocate a random id.
20    pub fn generate() -> Self {
21        Self(uuid::Uuid::new_v4().to_string())
22    }
23
24    /// Borrow the underlying string.
25    pub fn as_str(&self) -> &str {
26        &self.0
27    }
28}
29
30/// Stable tool execution identity within one Loop incarnation.
31#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
32pub struct ToolExecutionId(String);
33
34impl ToolExecutionId {
35    /// Create from an explicit value.
36    pub fn new(value: impl Into<String>) -> Self {
37        Self(value.into())
38    }
39
40    /// Allocate a random id.
41    pub fn generate() -> Self {
42        Self(uuid::Uuid::new_v4().to_string())
43    }
44
45    /// Borrow the underlying string.
46    pub fn as_str(&self) -> &str {
47        &self.0
48    }
49}
50
51/// Aggregate Loop bounds.
52#[derive(Clone, Debug)]
53pub struct LoopLimits {
54    /// Maximum tracked tool actions.
55    pub max_tool_actions: usize,
56    /// Maximum concurrent tool executions.
57    pub max_concurrent_executions: usize,
58    /// Maximum queued ready requests.
59    pub max_queued_ready: usize,
60    /// Maximum output queue items.
61    pub max_output_queue: usize,
62    /// Maximum deduplication table entries.
63    pub max_dedup_entries: usize,
64}
65
66impl Default for LoopLimits {
67    fn default() -> Self {
68        Self {
69            max_tool_actions: 1024,
70            max_concurrent_executions: 32,
71            max_queued_ready: 256,
72            max_output_queue: 4096,
73            max_dedup_entries: 4096,
74        }
75    }
76}
77
78/// Explicit Loop admission scope (no ambient expansion).
79#[derive(Clone, Debug)]
80pub struct LoopScope {
81    /// Owning run.
82    pub monoloop_run_id: MonoloopRunId,
83    /// Loop identity.
84    pub loop_id: LoopId,
85    /// Admitted interpretation ids (empty = accept any for initial test convenience
86    /// only when `accept_all_interpretations` is true).
87    pub accepted_interpretation_ids: Vec<InterpretationId>,
88    /// Admitted connection ids.
89    pub accepted_connection_ids: Vec<ConnectionId>,
90    /// Admitted external session ids.
91    pub accepted_external_session_ids: Vec<ExternalSessionId>,
92    /// When true, skip interpretation membership checks (tests / single-source runs).
93    pub accept_all_in_run: bool,
94}
95
96impl Default for LoopScope {
97    fn default() -> Self {
98        Self {
99            monoloop_run_id: MonoloopRunId::generate(),
100            loop_id: LoopId::generate(),
101            accepted_interpretation_ids: Vec::new(),
102            accepted_connection_ids: Vec::new(),
103            accepted_external_session_ids: Vec::new(),
104            accept_all_in_run: true,
105        }
106    }
107}
108
109impl LoopScope {
110    /// Scope for a single interpretation/connection pair.
111    pub fn single(
112        run_id: MonoloopRunId,
113        loop_id: LoopId,
114        interpretation_id: InterpretationId,
115        connection_id: ConnectionId,
116        external_session_id: Option<ExternalSessionId>,
117    ) -> Self {
118        let mut sessions = Vec::new();
119        if let Some(s) = external_session_id {
120            sessions.push(s);
121        }
122        Self {
123            monoloop_run_id: run_id,
124            loop_id,
125            accepted_interpretation_ids: vec![interpretation_id],
126            accepted_connection_ids: vec![connection_id],
127            accepted_external_session_ids: sessions,
128            accept_all_in_run: false,
129        }
130    }
131}
132
133/// Why a tool was unavailable.
134#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
135pub enum ToolUnavailableReason {
136    /// Empty registry / no registered tool.
137    NoRegisteredTool,
138    /// Named tool not found.
139    NotFound,
140    /// Policy denied (future).
141    Denied,
142}
143
144/// Terminal outcome of a loop-owned tool action (provider-neutral).
145#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
146pub enum OutboundToolOutcome {
147    /// Tool succeeded (future runtime).
148    Success,
149    /// Tool unavailable at registry.
150    ToolUnavailable,
151    /// Dispatch rejected (limits/validation).
152    DispatchRejected,
153    /// Execution failed.
154    ExecutionFailed,
155    /// Cancelled.
156    Cancelled,
157    /// Execution lost.
158    ExecutionLost,
159}
160
161/// Provider-neutral outbound tool result (Loop product; not dialect-encoded).
162#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
163pub struct OutboundToolResult {
164    /// Result identity.
165    pub outbound_result_id: String,
166    /// Owning run.
167    pub monoloop_run_id: MonoloopRunId,
168    /// Loop identity.
169    pub loop_id: LoopId,
170    /// Source interpretation.
171    pub source_interpretation_id: InterpretationId,
172    /// Source connection.
173    pub source_connection_id: ConnectionId,
174    /// External session when present.
175    pub external_session_id: Option<ExternalSessionId>,
176    /// Tool action id.
177    pub tool_action_id: ToolActionId,
178    /// Request generation that triggered dispatch.
179    pub request_generation: u64,
180    /// Execution id when started.
181    pub tool_execution_id: Option<ToolExecutionId>,
182    /// Terminal outcome.
183    pub outcome: OutboundToolOutcome,
184    /// Complete result payload or safe error (bounded).
185    pub payload: String,
186    /// Canonical unit id for correlation.
187    pub source_unit_id: UnitId,
188}
189
190/// Closed Loop output event vocabulary (initial).
191#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
192pub enum LoopOutputEvent {
193    /// Registry resolution requested for a ready tool.
194    ToolDispatchRequested {
195        /// Tool action.
196        tool_action_id: ToolActionId,
197        /// Generation.
198        request_generation: u64,
199    },
200    /// Tool unavailable (empty registry path).
201    ToolUnavailable {
202        /// Tool action.
203        tool_action_id: ToolActionId,
204        /// Reason.
205        reason: ToolUnavailableReason,
206    },
207    /// Provider-neutral outbound result.
208    OutboundToolResult(OutboundToolResult),
209    /// Safe loop diagnostic.
210    Diagnostic {
211        /// Bounded message.
212        message: String,
213    },
214    /// Loop ended.
215    LoopEnded(LoopEnd),
216}
217
218/// Exactly one Loop terminal report.
219#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
220pub struct LoopEnd {
221    /// Owning run.
222    pub monoloop_run_id: MonoloopRunId,
223    /// Loop identity.
224    pub loop_id: LoopId,
225    /// Terminal kind.
226    pub kind: LoopEndKind,
227    /// Delivery events received.
228    pub delivery_events_received: u64,
229    /// Duplicate events ignored.
230    pub duplicate_events: u64,
231    /// Tool actions by terminal unavailable count.
232    pub tools_unavailable: u64,
233    /// Outbound results emitted.
234    pub outbound_results_emitted: u64,
235    /// Safe diagnostics.
236    pub safe_diagnostics: Vec<String>,
237}
238
239/// Loop terminal kinds.
240#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
241pub enum LoopEndKind {
242    /// Source drained cleanly.
243    Drained,
244    /// Cancelled.
245    Cancelled,
246    /// Subscription gap/loss.
247    SubscriptionLost,
248    /// Output failed.
249    OutputFailed,
250    /// Invariant failed.
251    InvariantFailed,
252    /// Configuration failed.
253    ConfigurationFailed,
254}
255
256/// Loop error classification.
257#[derive(Clone, Copy, Debug, PartialEq, Eq)]
258pub enum LoopErrorKind {
259    /// Event out of scope.
260    EventOutOfScope,
261    /// Delivery sequence gap.
262    DeliverySequenceGap,
263    /// Unit identity conflict.
264    UnitIdentityConflict,
265    /// Tool request incomplete.
266    ToolRequestIncomplete,
267    /// Concurrency/queue limit.
268    LimitExceeded,
269    /// Cancelled.
270    Cancelled,
271    /// Invariant violation.
272    InvariantViolation,
273    /// Configuration invalid.
274    ConfigurationInvalid,
275    /// Output backpressure.
276    OutputBackpressure,
277}
278
279/// Loop error with safe diagnostics.
280#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
281#[error("{kind:?}: {message}")]
282pub struct LoopError {
283    /// Closed family.
284    pub kind: LoopErrorKind,
285    /// Bounded message.
286    pub message: String,
287}
288
289impl LoopError {
290    /// Construct.
291    pub fn new(kind: LoopErrorKind, message: impl Into<String>) -> Self {
292        Self {
293            kind,
294            message: message.into(),
295        }
296    }
297
298    /// Cancelled.
299    pub fn cancelled() -> Self {
300        Self::new(LoopErrorKind::Cancelled, "loop cancelled")
301    }
302
303    /// Gap.
304    pub fn gap() -> Self {
305        Self::new(
306            LoopErrorKind::DeliverySequenceGap,
307            "subscription gap detected",
308        )
309    }
310
311    /// Limit.
312    pub fn limit(message: impl Into<String>) -> Self {
313        Self::new(LoopErrorKind::LimitExceeded, message)
314    }
315}