Skip to main content

monoloop_contracts/
limits.rs

1//! Bounded transport, interpretation, input, tool, and transaction limits.
2
3use serde::{Deserialize, Serialize};
4use std::time::Duration;
5use thiserror::Error;
6
7/// Input/output buffer bounds for one connection.
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct TransportBufferLimits {
10    /// Maximum queued input bytes awaiting send.
11    pub max_queued_input_bytes: usize,
12    /// Maximum queued output bytes awaiting receive.
13    pub max_queued_output_bytes: usize,
14    /// Maximum individual chunk accepted from the caller.
15    pub max_chunk_bytes: usize,
16}
17
18impl Default for TransportBufferLimits {
19    fn default() -> Self {
20        Self {
21            max_queued_input_bytes: 1024 * 1024,
22            max_queued_output_bytes: 1024 * 1024,
23            max_chunk_bytes: 256 * 1024,
24        }
25    }
26}
27
28/// Connector-level limits applied at open.
29#[derive(Clone, Debug)]
30pub struct ConnectorLimits {
31    /// Connect / open deadline.
32    pub connect_deadline: Duration,
33    /// Buffer bounds.
34    pub buffers: TransportBufferLimits,
35    /// Cancellation grace before forced terminate (caller policy may override).
36    pub cancel_grace: Duration,
37    /// Cleanup deadline after terminal selection.
38    pub cleanup_deadline: Duration,
39}
40
41impl Default for ConnectorLimits {
42    fn default() -> Self {
43        Self {
44            connect_deadline: Duration::from_secs(30),
45            buffers: TransportBufferLimits::default(),
46            cancel_grace: Duration::from_secs(5),
47            cleanup_deadline: Duration::from_secs(10),
48        }
49    }
50}
51
52/// Interpretation assembly and output bounds.
53#[derive(Clone, Debug)]
54pub struct InterpretationLimits {
55    /// Maximum undecoded/raw buffer bytes.
56    pub max_undecoded_bytes: usize,
57    /// Maximum dialect frame bytes.
58    pub max_frame_bytes: usize,
59    /// Maximum sentence assembly buffer.
60    pub max_sentence_assembly_bytes: usize,
61    /// Maximum structural atom bytes.
62    pub max_structural_atom_bytes: usize,
63    /// Maximum pending tool actions.
64    pub max_pending_tool_actions: usize,
65    /// Maximum bytes per pending tool action.
66    pub max_bytes_per_tool_action: usize,
67    /// Maximum canonical output queue items.
68    pub max_output_queue_items: usize,
69    /// Maximum safe diagnostics retained.
70    pub max_safe_diagnostics: usize,
71}
72
73impl Default for InterpretationLimits {
74    fn default() -> Self {
75        Self {
76            max_undecoded_bytes: 4 * 1024 * 1024,
77            max_frame_bytes: 4 * 1024 * 1024,
78            max_sentence_assembly_bytes: 256 * 1024,
79            max_structural_atom_bytes: 512 * 1024,
80            max_pending_tool_actions: 256,
81            max_bytes_per_tool_action: 256 * 1024,
82            max_output_queue_items: 4096,
83            max_safe_diagnostics: 64,
84        }
85    }
86}
87
88/// Bounds for validating [`crate::input::CanonicalInput`].
89#[derive(Clone, Debug, PartialEq, Eq)]
90pub struct InputLimits {
91    /// Maximum messages in one input.
92    pub max_messages: usize,
93    /// Maximum text parts per message.
94    pub max_content_parts: usize,
95    /// Maximum bytes of one text part.
96    pub max_text_part_bytes: usize,
97    /// Maximum aggregate UTF-8 bytes of all text parts.
98    pub max_aggregate_text_bytes: usize,
99    /// Maximum assistant tool calls in one input.
100    pub max_tool_calls: usize,
101    /// Maximum JSON argument bytes per tool call.
102    pub max_tool_argument_bytes: usize,
103    /// Maximum JSON nesting depth for tool arguments.
104    pub max_json_depth: u32,
105    /// Maximum optional `name` field bytes.
106    pub max_name_bytes: usize,
107    /// Maximum `tool_call_id` bytes.
108    pub max_tool_call_id_bytes: usize,
109}
110
111impl Default for InputLimits {
112    fn default() -> Self {
113        Self {
114            max_messages: 256,
115            max_content_parts: 64,
116            max_text_part_bytes: 256 * 1024,
117            max_aggregate_text_bytes: 2 * 1024 * 1024,
118            max_tool_calls: 64,
119            max_tool_argument_bytes: 256 * 1024,
120            max_json_depth: 16,
121            max_name_bytes: 128,
122            max_tool_call_id_bytes: 128,
123        }
124    }
125}
126
127/// Extension map bounds (invocation and session configuration).
128#[derive(Clone, Debug, PartialEq, Eq)]
129pub struct ExtensionLimits {
130    /// Maximum number of extension keys.
131    pub max_keys: usize,
132    /// Maximum key string bytes.
133    pub max_key_bytes: usize,
134    /// Maximum nesting depth of extension values.
135    pub max_value_depth: u32,
136    /// Maximum total serialized JSON bytes of all extensions.
137    pub max_serialized_bytes: usize,
138}
139
140impl Default for ExtensionLimits {
141    fn default() -> Self {
142        Self {
143            max_keys: 32,
144            max_key_bytes: 64,
145            max_value_depth: 8,
146            max_serialized_bytes: 16 * 1024,
147        }
148    }
149}
150
151/// Per-tool execution bounds.
152#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
153pub struct ToolLimits {
154    /// Maximum concurrent executions of this tool.
155    pub max_concurrent: usize,
156    /// Maximum input payload bytes.
157    pub max_input_bytes: usize,
158    /// Maximum output payload bytes.
159    pub max_output_bytes: usize,
160    /// Maximum execution wall time.
161    #[serde(with = "duration_secs")]
162    pub execution_deadline: Duration,
163}
164
165impl Default for ToolLimits {
166    fn default() -> Self {
167        Self {
168            max_concurrent: 8,
169            max_input_bytes: 256 * 1024,
170            max_output_bytes: 256 * 1024,
171            execution_deadline: Duration::from_secs(60),
172        }
173    }
174}
175
176/// Per-Channel capacity and encoding bounds.
177#[derive(Clone, Debug, PartialEq, Eq)]
178pub struct ChannelLimits {
179    /// Maximum concurrent active transactions on this Channel.
180    pub max_active_transactions: usize,
181    /// Maximum concurrent distinct sessions.
182    pub max_distinct_sessions: usize,
183    /// Maximum encoded exchange bytes.
184    pub max_encoded_exchange_bytes: usize,
185}
186
187impl Default for ChannelLimits {
188    fn default() -> Self {
189        Self {
190            max_active_transactions: 64,
191            max_distinct_sessions: 64,
192            max_encoded_exchange_bytes: 4 * 1024 * 1024,
193        }
194    }
195}
196
197/// Runtime transaction bounds (see implementation ยง12).
198#[derive(Clone, Debug, PartialEq, Eq)]
199pub struct TransactionLimits {
200    /// Global active transaction cap.
201    pub max_active_transactions: usize,
202    /// Per-Channel active cap (enforced with Channel limits).
203    pub max_active_per_channel: usize,
204    /// Actor command queue items.
205    pub max_actor_commands: usize,
206    /// Actor command queue bytes.
207    pub max_actor_command_bytes: usize,
208    /// Event delivery queue items.
209    pub max_event_queue: usize,
210    /// Event delivery queue bytes.
211    pub max_event_queue_bytes: usize,
212    /// Maximum input aggregate bytes (admission).
213    pub max_input_bytes: usize,
214    /// Maximum messages.
215    pub max_messages: usize,
216    /// Maximum content parts per message.
217    pub max_content_parts: usize,
218    /// Maximum tools selected on one transaction.
219    pub max_tools_per_transaction: usize,
220    /// Maximum tool schema bytes.
221    pub max_tool_schema_bytes: usize,
222    /// Maximum tool payload bytes.
223    pub max_tool_payload_bytes: usize,
224    /// Maximum tool output bytes.
225    pub max_tool_output_bytes: usize,
226    /// Concurrent tools per transaction.
227    pub max_concurrent_tools_per_transaction: usize,
228    /// Queued tool starts per transaction.
229    pub max_queued_tools_per_transaction: usize,
230    /// Maximum inline continuations.
231    pub max_continuations: usize,
232    /// Maximum provider exchanges.
233    pub max_provider_exchanges: usize,
234    /// Maximum continuation-context encoded bytes.
235    pub max_continuation_context_bytes: usize,
236    /// Maximum total provider input bytes across exchanges.
237    pub max_total_provider_input_bytes: usize,
238    /// Maximum total provider output bytes across exchanges.
239    pub max_total_provider_output_bytes: usize,
240    /// Maximum retained diagnostics.
241    pub max_diagnostic_count: usize,
242    /// Maximum bytes per diagnostic message.
243    pub max_diagnostic_bytes: usize,
244    /// Default transaction deadline.
245    pub transaction_deadline: Duration,
246    /// Cleanup budget after terminal selection.
247    pub cleanup_deadline: Duration,
248    /// Terminal `Ended` delivery budget.
249    pub terminal_event_delivery_deadline: Duration,
250    /// Completion callback deadline.
251    pub callback_deadline: Duration,
252}
253
254impl Default for TransactionLimits {
255    fn default() -> Self {
256        Self {
257            max_active_transactions: 256,
258            max_active_per_channel: 64,
259            max_actor_commands: 256,
260            max_actor_command_bytes: 1024 * 1024,
261            max_event_queue: 1024,
262            max_event_queue_bytes: 4 * 1024 * 1024,
263            max_input_bytes: 2 * 1024 * 1024,
264            max_messages: 256,
265            max_content_parts: 64,
266            max_tools_per_transaction: 64,
267            max_tool_schema_bytes: 64 * 1024,
268            max_tool_payload_bytes: 256 * 1024,
269            max_tool_output_bytes: 256 * 1024,
270            max_concurrent_tools_per_transaction: 16,
271            max_queued_tools_per_transaction: 64,
272            max_continuations: 32,
273            max_provider_exchanges: 64,
274            max_continuation_context_bytes: 2 * 1024 * 1024,
275            max_total_provider_input_bytes: 16 * 1024 * 1024,
276            max_total_provider_output_bytes: 16 * 1024 * 1024,
277            max_diagnostic_count: 64,
278            max_diagnostic_bytes: 1024,
279            transaction_deadline: Duration::from_secs(600),
280            cleanup_deadline: Duration::from_secs(30),
281            terminal_event_delivery_deadline: Duration::from_secs(10),
282            callback_deadline: Duration::from_secs(5),
283        }
284    }
285}
286
287/// Limit configuration rejected before runtime start.
288#[derive(Clone, Debug, Error, PartialEq, Eq)]
289pub enum LimitsError {
290    /// A capacity is zero where non-zero is required.
291    #[error("limit must be non-zero: {0}")]
292    ZeroCapacity(&'static str),
293    /// Related limits are inconsistent.
294    #[error("inconsistent limits: {0}")]
295    Inconsistent(&'static str),
296}
297
298mod duration_secs {
299    use serde::{Deserialize, Deserializer, Serialize, Serializer};
300    use std::time::Duration;
301
302    pub fn serialize<S>(d: &Duration, serializer: S) -> Result<S::Ok, S::Error>
303    where
304        S: Serializer,
305    {
306        d.as_secs().serialize(serializer)
307    }
308
309    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
310    where
311        D: Deserializer<'de>,
312    {
313        let secs = u64::deserialize(deserializer)?;
314        Ok(Duration::from_secs(secs))
315    }
316}
317
318impl TransactionLimits {
319    /// Validate non-zero capacities and basic consistency (D-015).
320    pub fn validate(&self) -> Result<(), LimitsError> {
321        for (name, v) in [
322            ("max_active_transactions", self.max_active_transactions),
323            ("max_active_per_channel", self.max_active_per_channel),
324            ("max_actor_commands", self.max_actor_commands),
325            ("max_actor_command_bytes", self.max_actor_command_bytes),
326            ("max_event_queue", self.max_event_queue),
327            ("max_event_queue_bytes", self.max_event_queue_bytes),
328            ("max_input_bytes", self.max_input_bytes),
329            ("max_messages", self.max_messages),
330            ("max_content_parts", self.max_content_parts),
331            ("max_tools_per_transaction", self.max_tools_per_transaction),
332            ("max_tool_schema_bytes", self.max_tool_schema_bytes),
333            ("max_tool_payload_bytes", self.max_tool_payload_bytes),
334            ("max_tool_output_bytes", self.max_tool_output_bytes),
335            (
336                "max_concurrent_tools_per_transaction",
337                self.max_concurrent_tools_per_transaction,
338            ),
339            (
340                "max_queued_tools_per_transaction",
341                self.max_queued_tools_per_transaction,
342            ),
343            ("max_continuations", self.max_continuations.max(1)), // 0 continuations allowed
344            ("max_provider_exchanges", self.max_provider_exchanges),
345            (
346                "max_continuation_context_bytes",
347                self.max_continuation_context_bytes,
348            ),
349            (
350                "max_total_provider_input_bytes",
351                self.max_total_provider_input_bytes,
352            ),
353            (
354                "max_total_provider_output_bytes",
355                self.max_total_provider_output_bytes,
356            ),
357            ("max_diagnostic_count", self.max_diagnostic_count),
358            ("max_diagnostic_bytes", self.max_diagnostic_bytes),
359        ] {
360            // Continuations may be zero (CallerControlled-only channels).
361            if name == "max_continuations" {
362                continue;
363            }
364            if v == 0 {
365                return Err(LimitsError::ZeroCapacity(name));
366            }
367        }
368        if self.callback_deadline.is_zero() {
369            return Err(LimitsError::ZeroCapacity("callback_deadline"));
370        }
371        if self.cleanup_deadline.is_zero() {
372            return Err(LimitsError::ZeroCapacity("cleanup_deadline"));
373        }
374        if self.terminal_event_delivery_deadline.is_zero() {
375            return Err(LimitsError::ZeroCapacity(
376                "terminal_event_delivery_deadline",
377            ));
378        }
379        if self.transaction_deadline.is_zero() {
380            return Err(LimitsError::ZeroCapacity("transaction_deadline"));
381        }
382        if self.max_active_per_channel > self.max_active_transactions {
383            return Err(LimitsError::Inconsistent(
384                "max_active_per_channel exceeds max_active_transactions",
385            ));
386        }
387        if self.max_event_queue_bytes < self.max_event_queue {
388            return Err(LimitsError::Inconsistent(
389                "max_event_queue_bytes smaller than max_event_queue items",
390            ));
391        }
392        Ok(())
393    }
394}