Skip to main content

ai_agents_runtime/optimization/
config.rs

1use serde::{Deserialize, Serialize};
2
3use ai_agents_core::{AgentError, Result};
4use ai_agents_tools::ToolSchemaPromptMode;
5
6/// Runtime-level configuration for latency optimization and tool prompt behavior.
7#[derive(Debug, Clone, Serialize, Deserialize, Default)]
8#[serde(default)]
9pub struct RuntimeConfig {
10    /// Policies that reduce latency without changing behavior by default.
11    pub optimization: RuntimeOptimizationConfig,
12    /// Tool schema rendering mode for system prompt generation. Defaults to full.
13    pub tool_schema_prompt_mode: ToolSchemaPromptMode,
14}
15
16/// Controls safe pre-response routing, maintenance concurrency, and runtime task limits.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(default)]
19pub struct RuntimeOptimizationConfig {
20    /// Enables runtime optimization behavior. Disabled agents keep serial behavior.
21    pub enabled: bool,
22    /// Hard cap for additional speculative LLM calls in one turn.
23    pub max_speculative_llm_calls_per_turn: u32,
24    /// Runs explicitly marked guard and resolved-intent transitions before old-state response generation.
25    pub pre_response_deterministic_transitions: bool,
26    /// Runs current-state extractors before pre-response transition selection when requested.
27    pub pre_response_extractors: bool,
28    /// Enables response-independent transition branches beside a draft response.
29    pub speculative_state_transitions: bool,
30    /// Enables pure skill routing beside a draft response.
31    pub speculative_skill_routing: bool,
32    /// Enables auto reasoning decisions beside a plain draft response.
33    pub speculative_reasoning_auto: bool,
34    /// Allows facts and relationship maintenance to run concurrently when configured.
35    pub parallel_post_turn_memory: bool,
36    /// Allows orchestration vote extraction to run concurrently while preserving order.
37    pub parallel_orchestration_vote_extraction: bool,
38    /// Reserved for snapshot-based observability export outside the response path.
39    pub background_observability_export: bool,
40    /// Streaming safety policy used when optimization is enabled.
41    pub streaming_policy: StreamingOptimizationPolicy,
42    /// Maximum internal runtime tasks scheduled at once.
43    pub max_parallel_runtime_tasks: usize,
44    /// Post-turn maintenance policy for future-turn work.
45    pub post_turn: PostTurnOptimizationConfig,
46}
47
48impl Default for RuntimeOptimizationConfig {
49    fn default() -> Self {
50        Self {
51            enabled: false,
52            max_speculative_llm_calls_per_turn: 0,
53            pre_response_deterministic_transitions: false,
54            pre_response_extractors: false,
55            speculative_state_transitions: false,
56            speculative_skill_routing: false,
57            speculative_reasoning_auto: false,
58            parallel_post_turn_memory: false,
59            parallel_orchestration_vote_extraction: false,
60            background_observability_export: false,
61            streaming_policy: StreamingOptimizationPolicy::PreflightOnly,
62            max_parallel_runtime_tasks: 4,
63            post_turn: PostTurnOptimizationConfig::default(),
64        }
65    }
66}
67
68impl RuntimeOptimizationConfig {
69    /// Validates optimization settings before the runtime is built.
70    pub fn validate(&self) -> Result<()> {
71        if self.max_parallel_runtime_tasks == 0 {
72            return Err(AgentError::InvalidSpec(
73                "runtime.optimization.max_parallel_runtime_tasks must be greater than 0".into(),
74            ));
75        }
76        if self.post_turn.max_background_tasks == 0 && self.post_turn.any_background_tasks_enabled()
77        {
78            return Err(AgentError::InvalidSpec(
79                "runtime.optimization.post_turn.max_background_tasks must be greater than 0 when background maintenance is enabled".into(),
80            ));
81        }
82        if self.background_observability_export {
83            return Err(AgentError::InvalidSpec(
84                "runtime.optimization.background_observability_export requires snapshot export support and is not enabled yet".into(),
85            ));
86        }
87        let any_speculative = self.speculative_state_transitions
88            || self.speculative_skill_routing
89            || self.speculative_reasoning_auto;
90        if any_speculative {
91            if !self.enabled {
92                return Err(AgentError::InvalidSpec(
93                    "runtime.optimization.enabled must be true when speculative branch settings are enabled".into(),
94                ));
95            }
96            if self.max_speculative_llm_calls_per_turn == 0 {
97                return Err(AgentError::InvalidSpec(
98                    "runtime.optimization.max_speculative_llm_calls_per_turn must be greater than 0 when speculative branch settings are enabled".into(),
99                ));
100            }
101        }
102        if self.max_speculative_llm_calls_per_turn > self.max_parallel_runtime_tasks as u32 {
103            return Err(AgentError::InvalidSpec(
104                "runtime.optimization.max_speculative_llm_calls_per_turn must be less than or equal to max_parallel_runtime_tasks".into(),
105            ));
106        }
107        if self.post_turn.sessions != MaintenanceTaskPolicy::default() {
108            return Err(AgentError::InvalidSpec(
109                "runtime.optimization.post_turn.sessions is reserved until session maintenance scheduling is enabled".into(),
110            ));
111        }
112        if self.post_turn.memory_compression != MaintenanceTaskPolicy::default() {
113            return Err(AgentError::InvalidSpec(
114                "runtime.optimization.post_turn.memory_compression is reserved until compression scheduling is enabled".into(),
115            ));
116        }
117        Ok(())
118    }
119}
120
121/// Post-turn task policies for work that affects later turns.
122#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(default)]
124pub struct PostTurnOptimizationConfig {
125    /// Fact extraction policy.
126    pub facts: MaintenanceTaskPolicy,
127    /// Relationship update policy.
128    pub relationships: MaintenanceTaskPolicy,
129    /// Session metadata policy.
130    pub sessions: MaintenanceTaskPolicy,
131    /// Memory compression policy.
132    pub memory_compression: MaintenanceTaskPolicy,
133    /// Maximum number of queued background tasks.
134    pub max_background_tasks: usize,
135    /// Behavior when the background queue is full.
136    pub on_background_overflow: BackgroundOverflowPolicy,
137}
138
139impl Default for PostTurnOptimizationConfig {
140    fn default() -> Self {
141        Self {
142            facts: MaintenanceTaskPolicy {
143                mode: MaintenanceMode::InlineSerial,
144                await_before_next_turn: AwaitBeforeNextTurn::Always,
145            },
146            relationships: MaintenanceTaskPolicy {
147                mode: MaintenanceMode::InlineSerial,
148                await_before_next_turn: AwaitBeforeNextTurn::Always,
149            },
150            sessions: MaintenanceTaskPolicy::default(),
151            memory_compression: MaintenanceTaskPolicy::default(),
152            max_background_tasks: 16,
153            on_background_overflow: BackgroundOverflowPolicy::RunInline,
154        }
155    }
156}
157
158impl PostTurnOptimizationConfig {
159    /// Returns true when any maintenance task may run outside the response path.
160    pub fn any_background_tasks_enabled(&self) -> bool {
161        matches!(self.facts.mode, MaintenanceMode::Background)
162            || matches!(self.relationships.mode, MaintenanceMode::Background)
163            || matches!(self.sessions.mode, MaintenanceMode::Background)
164            || matches!(self.memory_compression.mode, MaintenanceMode::Background)
165    }
166}
167
168/// Policy for one post-turn maintenance task.
169#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
170#[serde(default)]
171pub struct MaintenanceTaskPolicy {
172    /// Whether the task runs serially, concurrently, or in the background.
173    pub mode: MaintenanceMode,
174    /// Whether a later turn waits for pending background work.
175    pub await_before_next_turn: AwaitBeforeNextTurn,
176}
177
178impl Default for MaintenanceTaskPolicy {
179    fn default() -> Self {
180        Self {
181            mode: MaintenanceMode::InlineSerial,
182            await_before_next_turn: AwaitBeforeNextTurn::Always,
183        }
184    }
185}
186
187/// Execution mode for post-turn maintenance.
188#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
189#[serde(rename_all = "snake_case")]
190pub enum MaintenanceMode {
191    /// Run in the existing serial response path.
192    #[default]
193    InlineSerial,
194    /// Run with other independent maintenance tasks and await completion.
195    InlineParallel,
196    /// Queue work after the response and apply freshness policy later.
197    Background,
198}
199
200/// Freshness policy for pending background maintenance.
201#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
202#[serde(rename_all = "snake_case")]
203pub enum AwaitBeforeNextTurn {
204    /// Never wait for this task before a new turn.
205    Never,
206    /// Wait before a turn from the same actor.
207    SameActor,
208    /// Wait before every new turn.
209    #[default]
210    Always,
211}
212
213/// Behavior when a background queue cannot accept more tasks.
214#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
215#[serde(rename_all = "snake_case")]
216pub enum BackgroundOverflowPolicy {
217    /// Run the task inline instead of dropping it.
218    #[default]
219    RunInline,
220    /// Drop the task and record skipped maintenance.
221    Drop,
222    /// Return an error to the caller.
223    Error,
224}
225
226/// Streaming behavior when optimized routing may run before output begins.
227#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
228#[serde(rename_all = "snake_case")]
229pub enum StreamingOptimizationPolicy {
230    /// Run safe preflight routing before opening the stream.
231    #[default]
232    PreflightOnly,
233    /// Buffer unresolved stream output until routing decisions finish.
234    BufferUntilRoutingDone,
235    /// Disable optimized streaming behavior.
236    Disabled,
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn accepts_buffered_streaming_policy() {
245        let config = RuntimeOptimizationConfig {
246            enabled: true,
247            streaming_policy: StreamingOptimizationPolicy::BufferUntilRoutingDone,
248            ..Default::default()
249        };
250        assert!(config.validate().is_ok());
251    }
252
253    #[test]
254    fn rejects_reserved_session_maintenance_policy() {
255        let mut config = RuntimeOptimizationConfig {
256            enabled: true,
257            ..Default::default()
258        };
259        config.post_turn.sessions.mode = MaintenanceMode::Background;
260        assert!(config.validate().is_err());
261    }
262
263    #[test]
264    fn rejects_reserved_compression_maintenance_policy() {
265        let mut config = RuntimeOptimizationConfig {
266            enabled: true,
267            ..Default::default()
268        };
269        config.post_turn.memory_compression.mode = MaintenanceMode::Background;
270        assert!(config.validate().is_err());
271    }
272
273    #[test]
274    fn speculative_flags_require_positive_cap() {
275        let config = RuntimeOptimizationConfig {
276            enabled: true,
277            speculative_skill_routing: true,
278            max_speculative_llm_calls_per_turn: 0,
279            ..Default::default()
280        };
281        assert!(config.validate().is_err());
282    }
283
284    #[test]
285    fn speculative_cap_must_fit_parallel_limit() {
286        let config = RuntimeOptimizationConfig {
287            enabled: true,
288            speculative_skill_routing: true,
289            max_speculative_llm_calls_per_turn: 5,
290            max_parallel_runtime_tasks: 4,
291            ..Default::default()
292        };
293        assert!(config.validate().is_err());
294    }
295}