oxi_agent/agent_loop/config.rs
1//! Agent loop configuration types
2
3/// Configuration for an [`crate::AgentLoop`] instance.
4#[derive(Clone)]
5pub struct AgentLoopConfig {
6 /// Model identifier in `provider/model` format.
7 pub model_id: String,
8 /// Optional system prompt prepended to every request.
9 pub system_prompt: Option<String>,
10 /// Sampling temperature (0.0 – 2.0).
11 pub temperature: f32,
12 /// Maximum tokens the model may generate per request.
13 pub max_tokens: u32,
14 /// Whether tool calls run in parallel or sequentially.
15 pub tool_execution: ToolExecutionMode,
16 /// Compaction strategy for managing context window usage.
17 pub compaction_strategy: oxi_ai::CompactionStrategy,
18 /// Approximate context window size in tokens.
19 pub context_window: usize,
20 /// Optional instruction injected into the compaction prompt.
21 pub compaction_instruction: Option<String>,
22 /// Optional session identifier for logging and tracing.
23 pub session_id: Option<String>,
24 /// Optional transport override (e.g. "sse", "stdio").
25 pub transport: Option<String>,
26 /// Whether to trigger compaction before the first turn.
27 pub compact_on_start: bool,
28 /// Optional cap on retry back-off delay (milliseconds).
29 pub max_retry_delay_ms: Option<u64>,
30 /// Enable automatic retry on retryable assistant errors.
31 pub auto_retry_enabled: bool,
32 /// Maximum number of auto-retry attempts.
33 pub auto_retry_max_attempts: usize,
34 /// Base delay in milliseconds for auto-retry exponential back-off.
35 pub auto_retry_base_delay_ms: u64,
36 /// Working directory for file tools. Defaults to current directory if None.
37 pub workspace_dir: Option<std::path::PathBuf>,
38 /// Per-provider options for fine-grained control.
39 ///
40 /// Passed through to [`oxi_ai::StreamOptions::provider_options`] so the
41 /// provider can read provider-specific settings.
42 pub provider_options: Option<oxi_ai::ProviderOptions>,
43
44 /// Async hook invoked after context compaction completes.
45 ///
46 /// Unlike the `Compaction` event in the `Fn` callback, this hook is
47 /// async and its future is awaited within the agent loop. Errors are
48 /// logged at WARN level but don't fail the loop.
49 ///
50 /// Use this for side effects that require async I/O (e.g., persisting
51 /// compaction summaries to a memory store) without resorting to
52 /// `tokio::spawn` fire-and-forget.
53 pub on_compaction: Option<CompactionHook>,
54 /// Snapshot store for hashline edit mode.
55 pub snapshot_store: Option<Arc<dyn oxi_hashline::SnapshotStore>>,
56 /// Memory backend for memory tools.
57 pub memory: Option<Arc<dyn crate::tools::MemoryBackend>>,
58 /// URL resolver for internal protocol schemes.
59 pub url_resolver: Option<Arc<dyn crate::tools::UrlResolver>>,
60 /// Todo state provider for the `todo` tool.
61 pub todo: Option<Arc<dyn crate::tools::TodoStateProvider>>,
62 /// Agent pool for Hub display.
63 pub agent_pool: Option<Arc<dyn crate::tools::AgentPoolProvider>>,
64 /// LSP provider for the `lsp` tool.
65 pub lsp: Option<Arc<dyn crate::tools::LspProvider>>,
66 /// TTSR engine for stream rule checking.
67 pub ttsr_engine: Option<Arc<crate::agent_loop::ttsr::TtsrEngine>>,
68 /// In-process sub-agent runner (issue #28 gap 3).
69 /// When `Some`, the `subagent` tool prefers an in-process isolated
70 /// run over shelling out to the CLI. Library consumers set this so
71 /// delegation works without an `oxi` subprocess.
72 pub subagent_runner: Option<Arc<dyn crate::tools::SubagentRunner>>,
73 /// Current sub-agent nesting depth (issue #28 gap 3).
74 ///
75 /// The CLI backend uses env vars (`OXI_SUBAGENT_DEPTH`) for this,
76 /// which is safe because each subprocess has its own env. The
77 /// in-process backend **cannot** use env vars (concurrent
78 /// `set_var` is UB; state leaks between forks), so it reads this
79 /// field instead. Default 0 (top-level). The `subagent` tool
80 /// increments this when creating a forked `AgentLoopConfig`, and
81 /// the fork checks it against the agent definition's
82 /// `max_subagent_depth` to cap recursion.
83 pub subagent_depth: u8,
84 /// Maximum size (in bytes) of a single tool result's text content
85 /// before it is truncated (issue #28 gap 1).
86 ///
87 /// When set, tool results exceeding this limit are truncated to
88 /// the limit and a marker is appended:
89 /// `"... [truncated: N bytes omitted]"`. This prevents a single
90 /// large tool output (e.g. reading a huge file, verbose bash
91 /// output) from consuming the entire context window.
92 ///
93 /// `None` (default) = no limit. Opt-in — existing behavior is
94 /// preserved.
95 pub max_tool_result_bytes: Option<usize>,
96 /// Enable thinking-loop detection in the streaming layer. When true,
97 /// each `ThinkingDelta` is fed to a detector that recognises verbatim
98 /// tail repetition, near-duplicate paragraph clusters, and
99 /// progress-lexicon stalls. On detection the stream is aborted with
100 /// a transient error so the retry layer resamples.
101 ///
102 /// Default: `true`. Set to `false` to disable (e.g. for tests that
103 /// exercise specific failure modes).
104 pub thinking_loop_detection: bool,
105 /// Settings for the cross-turn tool-call loop guard. When the same
106 /// single-tool call repeats past the threshold, the agent emits a
107 /// steering message to break the loop. Default: threshold 5, with
108 /// `read`/`ls`/`grep` exempt.
109 pub tool_call_loop_guard: oxi_ai::utils::tool_call_loop::ToolCallLoopGuardOptions,
110 /// Approval/tier configuration for gating tool execution.
111 ///
112 /// When configured, tool calls at tiers in `require_approval_for` are
113 /// checked against the approval hook before execution. Default: no
114 /// approval gating (all tools allowed without check).
115 pub approval_config: ApprovalConfig,
116
117 /// Soft tool requirements: tools the agent should call.
118 ///
119 /// On the first turn where a soft-required tool is missing, the loop
120 /// injects a reminder steering message. On the second consecutive miss,
121 /// it escalates. Default: empty (no soft requirements).
122 pub soft_requirements: Vec<SoftRequirement>,
123 /// Enable GPT-5 Harmony protocol leak detection.
124 ///
125 /// When `true`, each text delta is scanned for Harmony markers
126 /// (`to=functions.xxx`, `<|start|>`, etc.). On detection, the stream
127 /// is aborted, a `HarmonyLeakDetected` event is emitted, and the
128 /// turn is restarted. Default: `false`.
129 pub harmony_leak_detection: bool,
130
131 /// Owned (in-band) tool-calling dialect.
132 ///
133 /// When `Some`, the loop targets models **without native tool support**:
134 /// it sends no native `tools`, injects the tool catalog into the system
135 /// prompt, re-encodes prior tool calls/results as text in the history, and
136 /// parses the model's text output back into canonical tool calls. Mirrors
137 /// omp's `AgentLoopConfig.dialect` / `PI_DIALECT`.
138 ///
139 /// `None` (default) keeps provider-native tool calling.
140 pub dialect: Option<oxi_ai::dialect::Dialect>,
141}
142
143impl Default for AgentLoopConfig {
144 fn default() -> Self {
145 Self {
146 model_id: String::new(),
147 system_prompt: None,
148 temperature: 0.7,
149 max_tokens: 4096,
150 tool_execution: ToolExecutionMode::Parallel,
151 compaction_strategy: oxi_ai::CompactionStrategy::default(),
152 context_window: 128_000,
153 compaction_instruction: None,
154 session_id: None,
155 transport: None,
156 compact_on_start: false,
157 max_retry_delay_ms: None,
158 auto_retry_enabled: false,
159 auto_retry_max_attempts: 3,
160 auto_retry_base_delay_ms: 2000,
161 workspace_dir: None,
162 max_tool_result_bytes: None,
163 provider_options: None,
164 on_compaction: None,
165 snapshot_store: None,
166 memory: None,
167 url_resolver: None,
168 todo: None,
169 agent_pool: None,
170 lsp: None,
171 ttsr_engine: None,
172 subagent_runner: None,
173 subagent_depth: 0,
174 thinking_loop_detection: true,
175 approval_config: ApprovalConfig::default(),
176 soft_requirements: Vec::new(),
177 harmony_leak_detection: false,
178 dialect: None,
179 tool_call_loop_guard: oxi_ai::utils::tool_call_loop::ToolCallLoopGuardOptions::default(
180 ),
181 }
182 }
183}
184
185// Re-export ToolExecutionMode from crate::config to avoid duplicate definitions.
186pub use crate::config::ToolExecutionMode;
187
188use crate::AgentToolResult;
189use crate::compaction::CompactedContext;
190use anyhow::{Error, Result};
191use serde_json::Value;
192use std::future::Future;
193use std::pin::Pin;
194use std::sync::Arc;
195
196/// Async hook invoked after context compaction completes.
197///
198/// Receives the [`CompactedContext`] and returns a `Result<()>` future.
199/// The future is awaited within the agent loop, so async operations
200/// (memory storage, logging, etc.) are safe here.
201///
202/// # Example
203///
204/// ```ignore
205/// let config = AgentLoopConfig {
206/// on_compaction: Some(Arc::new(|ctx: CompactedContext| {
207/// let summary = ctx.summary.clone();
208/// Box::pin(async move {
209/// memory_store.save(summary).await
210/// })
211/// })),
212/// ..Default::default()
213/// };
214/// ```
215pub type CompactionHook =
216 Arc<dyn Fn(CompactedContext) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> + Send + Sync>;
217
218/// Hook invoked before each tool call; may return an override result.
219pub type BeforeToolCallHook = Arc<
220 dyn Fn(
221 &str,
222 &Value,
223 ) -> Pin<Box<dyn Future<Output = Result<Option<AgentToolResult>, Error>> + Send>>
224 + Send
225 + Sync,
226>;
227
228/// Hook invoked after each tool call; may return a modified result.
229pub type AfterToolCallHook = Arc<
230 dyn Fn(
231 &str,
232 &AgentToolResult,
233 ) -> Pin<Box<dyn Future<Output = Result<Option<AgentToolResult>, Error>> + Send>>
234 + Send
235 + Sync,
236>;
237
238// ── Approval system types ────────────────────────────────────────────────
239
240/// Decision returned by the approval hook for a tool call.
241#[derive(Debug, Clone)]
242pub enum ApprovalDecision {
243 /// Allow without conditions.
244 Allow,
245 /// Deny with a reason.
246 Deny(String),
247 /// Request human approval with a reason.
248 RequireApproval(String),
249}
250
251/// Async hook invoked before tool execution to check approval.
252///
253/// Receives the tool name and parsed arguments. Returns an `ApprovalDecision`.
254/// When `None` is returned (no hook registered), all tools are allowed.
255pub type ApprovalHook = Arc<
256 dyn Fn(&str, &Value) -> Pin<Box<dyn Future<Output = Result<ApprovalDecision, Error>> + Send>>
257 + Send
258 + Sync,
259>;
260
261/// Configuration for the approval/tier system.
262///
263/// Controls which tool tiers require approval before execution.
264/// When `hook` is `None`, all tools are allowed regardless of tier.
265/// When empty `require_approval_for`, no tiers trigger approval checks.
266///
267/// Default: no tiers require approval (opt-in only).
268#[derive(Clone, Default)]
269pub struct ApprovalConfig {
270 /// Tool tiers that require approval before execution.
271 /// Empty = no approval gating.
272 pub require_approval_for: Vec<crate::tools::ToolTier>,
273 /// Approval hook. When `None`, decisions are permissive.
274 pub hook: Option<ApprovalHook>,
275}
276
277// ── Soft requirement types ──────────────────────────────────────────────
278
279/// State for soft requirement tracking across turns.
280///
281/// Tracks which soft-required tools have been reminded/escalated.
282/// Resets when all soft requirements are satisfied or config changes.
283#[derive(Debug, Clone, Default)]
284pub struct SoftRequirementState {
285 /// Set of tool names that have been reminded (missed once).
286 /// When a tool appears here and is still missing next turn, escalate.
287 pub reminded: std::collections::HashSet<String>,
288}
289
290/// Soft requirement: a tool the agent should ideally call.
291/// First miss → reminder; second miss → escalation.
292#[derive(Debug, Clone)]
293pub struct SoftRequirement {
294 /// Tool name to check for.
295 pub tool_name: String,
296 /// Reason shown to the model.
297 pub reason: String,
298}
299
300// MAX_RETRIES and BACKOFF_BASE_SECS are now defined in crate::stream_retry
301// and re-exported from crate::agent_loop::retry.
302pub use crate::stream_retry::{BACKOFF_BASE_SECS, MAX_RETRIES};