Skip to main content

mlua_swarm/core/
config.rs

1//! EngineCfg + LongHoldConfig.
2
3use std::time::Duration;
4
5fn random_token_secret() -> Vec<u8> {
6    let mut buf = vec![0u8; 32];
7    getrandom::fill(&mut buf).expect("OS RNG unavailable");
8    buf
9}
10
11/// Lock acquisition + max-hold guard configuration.
12#[derive(Debug, Clone)]
13pub struct EngineCfg {
14    /// When `true`, `Engine::with_state` never retries on a busy lock — it
15    /// fails fast with `EngineError::LockBusy` on the very first
16    /// `try_lock` miss instead of backing off and retrying.
17    pub try_only: bool,
18    /// Max number of `try_lock` retries before giving up with
19    /// `EngineError::LockBusyAfterRetry` (ignored when `try_only = true`).
20    pub max_retry: u32,
21    /// Linear backoff step (ms) between retries; the sleep duration for
22    /// attempt `n` is `backoff_ms_step * (n + 1)`.
23    pub backoff_ms_step: u64,
24    /// R4 guard threshold: if a single `with_state` closure holds the lock
25    /// longer than this (ms), `with_state` panics — a signal that a long
26    /// operation leaked inside the lock in violation of the R3 discipline.
27    pub max_hold_ms: u128,
28    /// HMAC secret used by `TokenSigner` to sign/verify `CapToken`s.
29    ///
30    /// `Default` generates this fresh (32 random bytes from the OS RNG) on
31    /// every call when the caller does not supply one — set it explicitly
32    /// for tokens to stay valid across restarts, or when multiple
33    /// independently-constructed engines/signers must accept each other's
34    /// tokens.
35    pub token_secret: Vec<u8>,
36    /// Long-hold session tuning (idle keepalive, heartbeat cadence).
37    pub long_hold: LongHoldConfig,
38    /// Worker recursive spawn depth ceiling (guards against unbounded spawn).
39    ///
40    /// When `Ctx.meta.runtime["spawn_depth"]` has already reached this value
41    /// and a Worker token tries to call `start_task`, the engine raises
42    /// `EngineError::SpawnDepthExceeded`. `0` = root (a task launched
43    /// directly by an Operator); `4` = default, which allows four levels of
44    /// nested sub-tasks.
45    pub max_spawn_depth: u32,
46    /// GH #31: threshold/mode/storage tuning for delivering an oversized
47    /// baked `system_prompt` by reference (`WorkerPayload.system_ref`)
48    /// instead of inline (`WorkerPayload.system`). See
49    /// [`SystemRefConfig`].
50    pub system_ref: SystemRefConfig,
51}
52
53impl EngineCfg {
54    /// Strict variant: `try_only = true` (no retry/backoff) and a tight
55    /// `max_hold_ms = 10`. Useful for tests that want lock contention to
56    /// fail fast rather than silently wait.
57    pub fn strict() -> Self {
58        Self {
59            try_only: true,
60            max_retry: 0,
61            backoff_ms_step: 0,
62            max_hold_ms: 10,
63            ..Self::default()
64        }
65    }
66
67    /// Relaxed variant: higher `max_retry` / `backoff_ms_step` /
68    /// `max_hold_ms` than the default, for environments where lock
69    /// contention is expected to be more frequent or operations slower.
70    pub fn relaxed() -> Self {
71        Self {
72            try_only: false,
73            max_retry: 10,
74            backoff_ms_step: 50,
75            max_hold_ms: 200,
76            ..Self::default()
77        }
78    }
79}
80
81impl Default for EngineCfg {
82    /// Baseline configuration: bounded retry with backoff, a generous
83    /// (but non-zero) `max_hold_ms`, and `max_spawn_depth = 4`.
84    /// `token_secret` is generated fresh per call — see the field doc.
85    fn default() -> Self {
86        Self {
87            try_only: false,
88            max_retry: 3,
89            backoff_ms_step: 10,
90            max_hold_ms: 50,
91            token_secret: random_token_secret(),
92            long_hold: LongHoldConfig::default(),
93            max_spawn_depth: 4,
94            system_ref: SystemRefConfig::default(),
95        }
96    }
97}
98
99/// GH #31: server-side config for how a baked `system_prompt` too large
100/// to inline is delivered instead — see `Engine::fetch_worker_payload`'s
101/// threshold branch (`crate::core::engine`) for where this is consumed.
102/// Single server-side setting, not per-request: every fetch of a given
103/// `(task_id, attempt)` sees the same `mode` and `threshold_bytes`.
104#[derive(Debug, Clone)]
105pub struct SystemRefConfig {
106    /// Byte length of the baked `system` string above which
107    /// `Engine::fetch_worker_payload{,_trusted}` switches from inlining
108    /// the value (`WorkerPayload.system`) to a reference
109    /// (`WorkerPayload.system_ref`). Default (`25 * 1024`, 25 KiB)
110    /// matches `bp_doctor`'s existing WARN threshold
111    /// (`AGENT_MD_DEFAULT_WARN_BYTES` in
112    /// `crates/mlua-swarm-cli/src/mcp.rs`) — the same SubAgent
113    /// context-window headroom rationale that threshold documents
114    /// applies here to inline-vs-reference delivery.
115    pub threshold_bytes: usize,
116    /// Which [`crate::types::SystemRefMode`] an over-threshold response
117    /// uses to deliver its content.
118    pub mode: crate::types::SystemRefMode,
119    /// Directory `SystemRefMode::File` writes rendered `system` bodies
120    /// into (`<store_dir>/<task_id>-<attempt>.md`). No eviction policy —
121    /// files accumulate for the process lifetime (explicitly out of
122    /// scope; see the risk note on `Engine::fetch_worker_payload`).
123    pub store_dir: std::path::PathBuf,
124}
125
126impl Default for SystemRefConfig {
127    /// `threshold_bytes = 25 * 1024`, `mode = SystemRefMode::File`,
128    /// `store_dir = std::env::temp_dir().join("mse-system-ref")`.
129    fn default() -> Self {
130        Self {
131            threshold_bytes: 25 * 1024,
132            mode: crate::types::SystemRefMode::File,
133            store_dir: std::env::temp_dir().join("mse-system-ref"),
134        }
135    }
136}
137
138/// Tuning for long-running (suspend/resume-capable) sessions and tasks —
139/// how long a poll/suspend may hold, how often heartbeats are expected,
140/// and whether idle tasks are kept alive across a detach.
141#[derive(Debug, Clone)]
142pub struct LongHoldConfig {
143    /// Default wait duration used by long-poll style waits when the
144    /// caller does not specify one explicitly.
145    pub default_hold: Duration,
146    /// Upper bound on how long a single suspend/poll wait may block.
147    pub max_hold: Duration,
148    /// Expected cadence of `Engine::heartbeat` calls from an attached
149    /// session; consumed by `Engine::start_detach_loop`.
150    pub heartbeat_interval: Duration,
151    /// Number of missed heartbeat intervals tolerated before the detach
152    /// loop flips a session's `attached` flag to `false`.
153    pub heartbeat_miss_threshold: u32,
154    /// When `true`, a task survives (its state is retained) across a
155    /// session detach, so a later reattach can resume it in place.
156    pub keepalive_on_idle: bool,
157}
158
159impl Default for LongHoldConfig {
160    fn default() -> Self {
161        Self {
162            default_hold: Duration::from_secs(3600),
163            max_hold: Duration::from_secs(48 * 3600),
164            heartbeat_interval: Duration::from_secs(300),
165            heartbeat_miss_threshold: 3,
166            keepalive_on_idle: true,
167        }
168    }
169}