Skip to main content

leviath_cli/config/
limits.rs

1//! `[limits]` and `[webhook]`: the ceilings a run is bounded by.
2//!
3//! Concurrency caps, timeouts, retention, the stall and wedge watchdogs, and the
4//! webhook retry schedule. The `default_*` functions are serde defaults for the
5//! fields below them and are kept beside those fields deliberately: a default
6//! that drifts from the field it fills is invisible until someone reads a
7//! config that omits it.
8
9use serde::{Deserialize, Serialize};
10
11fn default_max_concurrent_inferences() -> Option<usize> {
12    Some(8)
13}
14
15fn default_default_max_iterations() -> Option<usize> {
16    Some(50)
17}
18
19fn default_max_concurrent_tools() -> usize {
20    8
21}
22
23fn default_script_shell_timeout_secs() -> u64 {
24    60
25}
26
27fn default_stall_timeout_secs() -> u64 {
28    leviath_runtime::pipeline::DEFAULT_STALL_TIMEOUT_SECS
29}
30
31fn default_dead_cycles_before_relief() -> u32 {
32    leviath_runtime::host::DEFAULT_DEAD_CYCLES_BEFORE_RELIEF
33}
34
35pub(crate) fn default_mcp_idle_disconnect_secs() -> u64 {
36    crate::daemon::mcp_pool::DEFAULT_MCP_IDLE_DISCONNECT_SECS
37}
38
39fn default_finished_retention_secs() -> u64 {
40    leviath_runtime::host::DEFAULT_FINISHED_RETENTION_SECS
41}
42
43fn default_wedge_timeout_secs() -> u64 {
44    leviath_runtime::pipeline::DEFAULT_WEDGE_TIMEOUT_SECS
45}
46
47fn default_provider_failures_before_open() -> u32 {
48    leviath_runtime::pipeline::DEFAULT_FAILURES_BEFORE_OPEN
49}
50
51fn default_provider_circuit_cooldown_secs() -> u64 {
52    leviath_runtime::pipeline::DEFAULT_CIRCUIT_COOLDOWN_SECS
53}
54
55fn default_interaction_timeout_secs() -> u64 {
56    leviath_runtime::interaction_hub::DEFAULT_INTERACTION_TIMEOUT_SECS
57}
58
59fn default_inference_retry_attempts() -> u32 {
60    leviath_runtime::DEFAULT_RETRY_ATTEMPTS
61}
62
63fn default_inference_retry_base_ms() -> u64 {
64    leviath_runtime::DEFAULT_RETRY_BASE_DELAY_MS
65}
66
67/// Runtime resource limits with safe defaults baked in.
68///
69/// Both fields default to a bounded value so a fresh install can't accidentally
70/// run unbounded inference concurrency or an unbounded agent loop. Set a field
71/// explicitly in `[limits]` to raise or lower it.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct LimitsConfig {
74    /// Global fallback cap on concurrent inference requests for any model
75    /// without its own per-model pool entry. Defaults to `Some(8)`; omit or set
76    /// a large number to effectively unbound it.
77    ///
78    /// One physical bound sits behind this for *script* providers: each of
79    /// their in-flight calls occupies a blocking-pool thread, and the daemon's
80    /// runtime provisions 2048 of those. Pools above 2048 only run that wide
81    /// for HTTP providers, whose calls are fully async.
82    #[serde(default = "default_max_concurrent_inferences")]
83    pub max_concurrent_inferences: Option<usize>,
84
85    /// Size of the shared tool-execution worker pool - the number of agents whose
86    /// tool batches may run concurrently across the whole daemon (the tool-lane
87    /// counterpart of `max_concurrent_inferences`). Defaults to `8`. Clamped to at
88    /// least 1.
89    #[serde(default = "default_max_concurrent_tools")]
90    pub max_concurrent_tools: usize,
91
92    /// Fallback `max_iterations` applied to a stage that does not set its own,
93    /// so an agent can't loop forever with no completion signal. Defaults to
94    /// `Some(50)`. A stage's explicit `max_iterations` always wins.
95    #[serde(default = "default_default_max_iterations")]
96    pub default_max_iterations: Option<usize>,
97
98    /// Opt-in exact pre-inference token budgeting. When `true`, each agent
99    /// inference is preceded by an exact token count of the assembled request
100    /// (via the provider's `count_tokens`, which uses a remote endpoint for
101    /// Anthropic/Gemini and a local heuristic otherwise) and is rejected before
102    /// sending if it would exceed the model's context window. Off by default:
103    /// normal budgeting uses cheap local estimates, and this adds a network
104    /// round-trip per inference for providers with a remote count endpoint.
105    #[serde(default)]
106    pub exact_token_counting: bool,
107
108    /// Wall-clock timeout (seconds) for a Rhai script tool's `shell()` host call,
109    /// mirroring the built-in shell tool's own 60-second cap so a script can't
110    /// hang an agent on a runaway command. Defaults to `60`.
111    #[serde(default = "default_script_shell_timeout_secs")]
112    pub script_shell_timeout_secs: u64,
113
114    /// How long (seconds) a run may sit ready to work but unable to dispatch
115    /// before it is failed instead of left running.
116    ///
117    /// This only ever fires for something the runtime cannot resolve on its own -
118    /// today, a stage whose provider is not configured. Waiting for a busy
119    /// model's inference pool is ordinary backpressure and is never failed, no
120    /// matter how long it takes. Defaults to `60`; `0` disables the watchdog and
121    /// restores the old behaviour of waiting indefinitely.
122    ///
123    /// Read once at daemon start, so a change needs a daemon restart.
124    #[serde(default = "default_stall_timeout_secs")]
125    pub stall_timeout_secs: u64,
126
127    /// How many consecutive safety re-drives may find the tool lane full and no
128    /// run moving before the daemon widens the lane to break the jam.
129    ///
130    /// The daemon re-drives itself every 30 seconds, so the default of `10` is
131    /// five minutes of a full lane going nowhere. Relief only ever *adds*
132    /// capacity, never cancels anything, and is capped at one extra lane's worth
133    /// over the daemon's life, so it cannot run away.
134    ///
135    /// `0` turns relief off. Detection and reporting stay on either way, so
136    /// `lev ps` and the metrics still show the streak.
137    ///
138    /// Read once at daemon start, so a change needs a daemon restart.
139    #[serde(default = "default_dead_cycles_before_relief")]
140    pub dead_cycles_before_relief: u32,
141
142    /// How long (seconds) a run keeps its place in `lev ps` after the daemon
143    /// unloads it from memory.
144    ///
145    /// A terminal run used to leave the listing the moment it was unloaded,
146    /// which made a run that died on its first inference look exactly like a run
147    /// that had never been spawned. A scheduler polling the listing could only
148    /// tell the two apart with a stopwatch, and issue #205 is what that cost:
149    /// forty minutes of spawning work, timing out, and spawning it again.
150    ///
151    /// Defaults to `300`. `0` drops a run as soon as it finishes, which is the
152    /// old behaviour. The record lives in memory, so a restart clears it
153    /// whatever this is set to.
154    ///
155    /// Read once at daemon start, so a change needs a daemon restart.
156    #[serde(default = "default_finished_retention_secs")]
157    pub finished_retention_secs: u64,
158    /// How long (seconds) a per-agent MCP server may sit with zero live runs
159    /// leasing it before the daemon disconnects it (ending a stdio server's
160    /// child process). Long enough that back-to-back runs of a blueprint reuse
161    /// the warm connection; the next run that declares the server reconnects
162    /// lazily. `0` keeps every server connected for the daemon's life, which
163    /// was the old behaviour. Global `[[mcp_servers]]` from config.toml are
164    /// never disconnected regardless.
165    ///
166    /// Read once at daemon start, so a change needs a daemon restart.
167    #[serde(default = "default_mcp_idle_disconnect_secs")]
168    pub mcp_idle_disconnect_secs: u64,
169    /// How long (seconds) a run may sit in a state no part of the engine can
170    /// reach before it is failed instead of left reported as running.
171    ///
172    /// Not a general "this run looks slow" timeout, and never fires on one. An
173    /// agent waiting on the model, on a tool, on its sub-agents, or on a person
174    /// is holding the marker that says so, and is exempt however long it takes.
175    /// This only catches an agent holding *no* marker at all, which the engine's
176    /// own invariants say cannot happen and which nothing will ever look at
177    /// again. Such a run stays `running` in `meta.json` for the life of the
178    /// daemon and keeps whatever capacity an external scheduler assigned it,
179    /// which is issue #202.
180    ///
181    /// Defaults to `0`, which is off: this fails runs, and an upgrade that
182    /// starts killing work nobody asked it to kill is worse than the leak. `300`
183    /// is a reasonable value to set. Turning it on is also a way to find out
184    /// whether it is happening to you, since it says so in the log and in the
185    /// run's error.
186    ///
187    /// Read once at daemon start, so a change needs a daemon restart.
188    #[serde(default = "default_wedge_timeout_secs")]
189    pub wedge_timeout_secs: u64,
190    /// How many consecutive provider-fatal failures (out of credits, rejected
191    /// key) take a provider out of service for every run.
192    ///
193    /// Defaults to `3`. One 402 can just be a request asking for more output
194    /// tokens than the balance covers; three in a row is the account. While a
195    /// provider is out, runs move to their next candidate, and runs with none
196    /// left are failed by the stall watchdog rather than left "running".
197    ///
198    /// `0` disables the breaker, leaving per-run failover on its own.
199    ///
200    /// Read once at daemon start, so a change needs a daemon restart.
201    #[serde(default = "default_provider_failures_before_open")]
202    pub provider_failures_before_open: u32,
203
204    /// How long a provider stays out of service before one request is let
205    /// through to see whether it recovered.
206    ///
207    /// Defaults to `300` (five minutes). That probe either succeeds, which puts
208    /// the provider straight back into service, or fails and restarts the wait,
209    /// so topping up an account brings the factory back with no restart.
210    ///
211    /// Read once at daemon start, so a change needs a daemon restart.
212    #[serde(default = "default_provider_circuit_cooldown_secs")]
213    pub provider_circuit_cooldown_secs: u64,
214    /// How long (seconds) a prompt may go unanswered before the daemon resolves
215    /// it itself and lets the run carry on.
216    ///
217    /// Covers every prompt that waits on a person: an agent's `ask_user_*` /
218    /// `present_for_review` call, a tool-approval prompt, a taint gate, and a
219    /// blueprint interaction point. Before this existed, a run whose operator
220    /// had walked away sat in `WaitingInput` holding its slot until the daemon
221    /// restarted - hours, in the report that prompted it (issue #204).
222    ///
223    /// Expiry resolves the prompt exactly as cancelling it would: a tool
224    /// approval and a taint gate **deny**, an `ask_user_*` call is told nobody
225    /// answered, and an interaction point proceeds with no user text. Nothing is
226    /// approved on the strength of a timeout.
227    ///
228    /// Defaults to `3600` (one hour); `0` waits indefinitely.
229    ///
230    /// Read once at daemon start, so a change needs a daemon restart.
231    #[serde(default = "default_interaction_timeout_secs")]
232    pub interaction_timeout_secs: u64,
233
234    /// How many times an inference is attempted, the first try included,
235    /// before the agent is failed with whatever the provider last said.
236    ///
237    /// Only a transient failure is retried at all - a reset connection, a
238    /// timeout, a 429, a 5xx. An authentication error or an over-long request
239    /// fails on the first answer, since the second would be identical.
240    ///
241    /// Defaults to `4`, which is one try and three retries. `1` turns retrying
242    /// off. The wait between retries is `inference_retry_base_ms`, doubling each
243    /// time, so the default schedule is 1s, 2s, 4s.
244    ///
245    /// Raising it lengthens how long a run rides out a provider **overload**
246    /// (an Anthropic 529, or a 429), which is retried on its own much slower
247    /// schedule of 15s, 30s, then 60s per further attempt - that case is why the
248    /// key exists (issue #417). Whatever this is set to, the retries of one
249    /// request may sleep at most five minutes in total.
250    ///
251    /// Read once at daemon start, so a change needs a daemon restart.
252    #[serde(default = "default_inference_retry_attempts")]
253    pub inference_retry_attempts: u32,
254
255    /// The wait before the first inference retry, in milliseconds, doubling for
256    /// each retry after it. Defaults to `1000`, so the schedule is 1s, 2s, 4s.
257    ///
258    /// This is the *blip* schedule and is meant to stay short: a reset
259    /// connection or a 500 is usually gone by the next attempt. A provider
260    /// overload does not use it - see `inference_retry_attempts` - so raising
261    /// this to wait out an outage is the wrong lever and only delays ordinary
262    /// failures.
263    ///
264    /// Read once at daemon start, so a change needs a daemon restart.
265    #[serde(default = "default_inference_retry_base_ms")]
266    pub inference_retry_base_ms: u64,
267
268    /// Most bytes one tool call may write to disk. Unset is unlimited.
269    ///
270    /// **Unset in code, set by `lev setup`.** How much an agent should write is
271    /// a judgement about what you are doing with it, so nothing is imposed on a
272    /// user who never opened this file - but a fresh install gets a concrete
273    /// number written here, where it is visible and can be deleted outright.
274    ///
275    /// The incident behind it (issue #252) was a single shell call appending in
276    /// a loop until the 60-second timeout: about 14 GB, from one call that
277    /// looked ordinary.
278    ///
279    /// A shell redirect is measured *after* the call, since the bytes go from
280    /// the shell to the file without passing through Leviath. So this stops the
281    /// call after the one that overran, not the one that did. `write_file` is
282    /// measured before, and is stopped outright.
283    ///
284    /// Running out of disk is checked separately and is never configurable: see
285    /// [`leviath_core::write_limits::MIN_FREE_BYTES`].
286    #[serde(default, skip_serializing_if = "Option::is_none")]
287    pub max_tool_call_write_bytes: Option<u64>,
288
289    /// Most bytes a whole run may write to disk. Unset is unlimited.
290    ///
291    /// The companion to `max_tool_call_write_bytes`, and the one that catches
292    /// what a per-call ceiling cannot: three calls of 12-14 GB each are
293    /// individually plausible and collectively a full disk. Same defaulting -
294    /// unset in code, written by `lev setup`.
295    #[serde(default, skip_serializing_if = "Option::is_none")]
296    pub max_run_write_bytes: Option<u64>,
297}
298
299impl LimitsConfig {
300    /// The write ceilings in effect, for the engine.
301    pub fn write_limits(&self) -> leviath_core::write_limits::WriteLimits {
302        leviath_core::write_limits::WriteLimits {
303            per_call: self.max_tool_call_write_bytes,
304            per_run: self.max_run_write_bytes,
305        }
306    }
307}
308
309impl Default for LimitsConfig {
310    fn default() -> Self {
311        Self {
312            max_concurrent_inferences: default_max_concurrent_inferences(),
313            max_concurrent_tools: default_max_concurrent_tools(),
314            default_max_iterations: default_default_max_iterations(),
315            exact_token_counting: false,
316            script_shell_timeout_secs: default_script_shell_timeout_secs(),
317            stall_timeout_secs: default_stall_timeout_secs(),
318            dead_cycles_before_relief: default_dead_cycles_before_relief(),
319            finished_retention_secs: default_finished_retention_secs(),
320            mcp_idle_disconnect_secs: default_mcp_idle_disconnect_secs(),
321            wedge_timeout_secs: default_wedge_timeout_secs(),
322            provider_failures_before_open: default_provider_failures_before_open(),
323            provider_circuit_cooldown_secs: default_provider_circuit_cooldown_secs(),
324            interaction_timeout_secs: default_interaction_timeout_secs(),
325            inference_retry_attempts: default_inference_retry_attempts(),
326            inference_retry_base_ms: default_inference_retry_base_ms(),
327            // Deliberately `None` here and concrete in `lev setup`: the code
328            // imposes no ceiling on a user who never opened the config, and a
329            // fresh install gets a number written where it can be seen and
330            // deleted. See the field docs.
331            max_tool_call_write_bytes: None,
332            max_run_write_bytes: None,
333        }
334    }
335}
336
337fn default_webhook_max_retries() -> u32 {
338    3
339}
340
341fn default_webhook_base_delay_ms() -> u64 {
342    500
343}
344
345fn default_webhook_max_delay_ms() -> u64 {
346    30_000
347}
348
349fn default_webhook_timeout_secs() -> u64 {
350    10
351}
352
353/// Completion-webhook delivery tuning.
354///
355/// A completion webhook is POSTed when a run reaches a terminal status. Delivery
356/// retries on transient failures (network errors, timeouts, 5xx, 429, 408) with
357/// exponential backoff. Each field has a safe default so `[webhook]` can be
358/// omitted entirely.
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct WebhookConfig {
361    /// Number of retries **after** the first attempt (so total sends is
362    /// `max_retries + 1`). Defaults to `3`. Set `0` to disable retries.
363    #[serde(default = "default_webhook_max_retries")]
364    pub max_retries: u32,
365
366    /// Base backoff before the first retry, in milliseconds. Subsequent retries
367    /// double it (capped at `max_delay_ms`). Defaults to `500`.
368    #[serde(default = "default_webhook_base_delay_ms")]
369    pub base_delay_ms: u64,
370
371    /// Upper bound on any single backoff delay, in milliseconds. Defaults to
372    /// `30_000` (30s).
373    #[serde(default = "default_webhook_max_delay_ms")]
374    pub max_delay_ms: u64,
375
376    /// Per-attempt request timeout, in seconds. Defaults to `10`.
377    #[serde(default = "default_webhook_timeout_secs")]
378    pub timeout_secs: u64,
379}
380
381impl Default for WebhookConfig {
382    fn default() -> Self {
383        Self {
384            max_retries: default_webhook_max_retries(),
385            base_delay_ms: default_webhook_base_delay_ms(),
386            max_delay_ms: default_webhook_max_delay_ms(),
387            timeout_secs: default_webhook_timeout_secs(),
388        }
389    }
390}