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