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    /// 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 [`oxicode_ai::StreamOptions::provider_options`] so the
41    /// provider can read provider-specific settings.
42    pub provider_options: Option<oxicode_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 oxicode_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 `oxicode` 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 (`OXICODE_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: oxicode_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<oxicode_ai::dialect::Dialect>,
141
142    /// Optional circuit breaker for provider calls. When `Some`, the agent
143    /// loop's retry path consults the breaker before each provider attempt
144    /// (`breaker.check()`); an open circuit short-circuits the retry loop
145    /// and returns immediately (the breaker's whole purpose is to stop
146    /// hammering a failing upstream). On every successful call the breaker
147    /// records success; on every error it records failure. When `None`, no
148    /// circuit breaking occurs (default — preserves existing behavior).
149    ///
150    /// This is the SDK-owned behavior trait + reference impl; consumers
151    /// implement [`oxicode_ai::circuit_breaker::CircuitBreaker`] for their domain profile
152    /// (A2A, HTTP, etc.) and pass the impl here. See
153    /// `docs/oxicode-sdk-ownership.md` §3.
154    pub circuit_breaker: Option<oxicode_ai::circuit_breaker::SharedBreaker>,
155}
156
157impl Default for AgentLoopConfig {
158    fn default() -> Self {
159        Self {
160            model_id: String::new(),
161            system_prompt: None,
162            temperature: 0.7,
163            max_tokens: 4096,
164            tool_execution: ToolExecutionMode::Parallel,
165            compaction_strategy: oxicode_ai::CompactionStrategy::default(),
166            context_window: 128_000,
167            compaction_instruction: None,
168            session_id: None,
169            transport: None,
170            compact_on_start: false,
171            max_retry_delay_ms: None,
172            auto_retry_enabled: false,
173            auto_retry_max_attempts: 3,
174            auto_retry_base_delay_ms: 2000,
175            workspace_dir: None,
176            max_tool_result_bytes: None,
177            provider_options: None,
178            on_compaction: None,
179            snapshot_store: None,
180            memory: None,
181            url_resolver: None,
182            todo: None,
183            agent_pool: None,
184            lsp: None,
185            ttsr_engine: None,
186            subagent_runner: None,
187            subagent_depth: 0,
188            thinking_loop_detection: true,
189            approval_config: ApprovalConfig::default(),
190            soft_requirements: Vec::new(),
191            harmony_leak_detection: false,
192            dialect: None,
193            circuit_breaker: None,
194            tool_call_loop_guard:
195                oxicode_ai::utils::tool_call_loop::ToolCallLoopGuardOptions::default(),
196        }
197    }
198}
199
200// Re-export ToolExecutionMode from crate::config to avoid duplicate definitions.
201pub use crate::config::ToolExecutionMode;
202
203use crate::AgentToolResult;
204use crate::compaction::CompactedContext;
205use anyhow::{Error, Result};
206use serde_json::Value;
207use std::future::Future;
208use std::pin::Pin;
209use std::sync::Arc;
210
211/// Async hook invoked after context compaction completes.
212///
213/// Receives the [`CompactedContext`] and returns a `Result<()>` future.
214/// The future is awaited within the agent loop, so async operations
215/// (memory storage, logging, etc.) are safe here.
216///
217/// # Example
218///
219/// ```ignore
220/// let config = AgentLoopConfig {
221///     on_compaction: Some(Arc::new(|ctx: CompactedContext| {
222///         let summary = ctx.summary.clone();
223///         Box::pin(async move {
224///             memory_store.save(summary).await
225///         })
226///     })),
227///     ..Default::default()
228/// };
229/// ```
230pub type CompactionHook =
231    Arc<dyn Fn(CompactedContext) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> + Send + Sync>;
232
233/// Hook invoked before each tool call; may return an override result.
234pub type BeforeToolCallHook = Arc<
235    dyn Fn(
236            &str,
237            &Value,
238        ) -> Pin<Box<dyn Future<Output = Result<Option<AgentToolResult>, Error>> + Send>>
239        + Send
240        + Sync,
241>;
242
243/// Hook invoked after each tool call; may return a modified result.
244pub type AfterToolCallHook = Arc<
245    dyn Fn(
246            &str,
247            &AgentToolResult,
248        ) -> Pin<Box<dyn Future<Output = Result<Option<AgentToolResult>, Error>> + Send>>
249        + Send
250        + Sync,
251>;
252
253// ── Approval system types ────────────────────────────────────────────────
254
255/// Decision returned by the approval hook for a tool call.
256#[derive(Debug, Clone)]
257pub enum ApprovalDecision {
258    /// Allow without conditions.
259    Allow,
260    /// Deny with a reason.
261    Deny(String),
262    /// Request human approval with a reason.
263    RequireApproval(String),
264}
265
266/// Async hook invoked before tool execution to check approval.
267///
268/// Receives the tool name and parsed arguments. Returns an `ApprovalDecision`.
269/// When `None` is returned (no hook registered), all tools are allowed.
270pub type ApprovalHook = Arc<
271    dyn Fn(&str, &Value) -> Pin<Box<dyn Future<Output = Result<ApprovalDecision, Error>> + Send>>
272        + Send
273        + Sync,
274>;
275
276/// Configuration for the approval/tier system.
277///
278/// Controls which tool tiers require approval before execution.
279/// When `hook` is `None`, all tools are allowed regardless of tier.
280/// When empty `require_approval_for`, no tiers trigger approval checks.
281///
282/// Default: no tiers require approval (opt-in only).
283#[derive(Clone, Default)]
284pub struct ApprovalConfig {
285    /// Tool tiers that require approval before execution.
286    /// Empty = no approval gating.
287    pub require_approval_for: Vec<crate::tools::ToolTier>,
288    /// Approval hook. When `None`, decisions are permissive.
289    pub hook: Option<ApprovalHook>,
290}
291
292// ── Soft requirement types ──────────────────────────────────────────────
293
294/// State for soft requirement tracking across turns.
295///
296/// Tracks which soft-required tools have been reminded/escalated.
297/// Resets when all soft requirements are satisfied or config changes.
298#[derive(Debug, Clone, Default)]
299pub struct SoftRequirementState {
300    /// Set of tool names that have been reminded (missed once).
301    /// When a tool appears here and is still missing next turn, escalate.
302    pub reminded: std::collections::HashSet<String>,
303}
304
305/// Soft requirement: a tool the agent should ideally call.
306/// First miss → reminder; second miss → escalation.
307#[derive(Debug, Clone)]
308pub struct SoftRequirement {
309    /// Tool name to check for.
310    pub tool_name: String,
311    /// Reason shown to the model.
312    pub reason: String,
313}
314
315// MAX_RETRIES and BACKOFF_BASE_SECS are now defined in crate::stream_retry
316// and re-exported from crate::agent_loop::retry.
317pub use crate::stream_retry::{BACKOFF_BASE_SECS, MAX_RETRIES};