Skip to main content

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