phi_core/agent_loop/config.rs
1use crate::context::{ContextConfig, ExecutionLimits};
2use crate::provider::context_translation::ContextTranslationStrategy;
3use crate::provider::{ModelConfig, ResponseFormat, StreamProvider};
4use crate::types::*;
5use std::sync::Arc;
6
7// ── Context transformation callbacks ────────────────────────────────────────
8/// All hook types use `Arc` (shared ownership) so they can be cloned into closures
9/// and stored without lifetime complications. `Box<dyn Fn>` would suffice for single-owner
10/// cases but `Arc` makes it trivially cheap to share across async tasks.
11/// Converts `AgentMessage[]` → `Message[]` before each LLM call.
12pub type ConvertToLlmFn = Arc<dyn Fn(&[AgentMessage]) -> Vec<Message> + Send + Sync>;
13/// Transforms the full context before `convert_to_llm` (for pruning, reordering, injection).
14pub type TransformContextFn = Arc<dyn Fn(Vec<AgentMessage>) -> Vec<AgentMessage> + Send + Sync>;
15/// Returns pending messages (steering interrupts or follow-up work) when polled.
16pub type GetMessagesFn = Box<dyn Fn() -> Vec<AgentMessage> + Send + Sync>;
17
18// ── Loop hooks ───────────────────────────────────────────────────────────────
19/// Called once before the entire agent loop begins (before `AgentStart` is emitted).
20///
21/// Arguments: `(messages, loop_index)` — `messages` is the full context at the time of the call;
22/// `loop_index` is always `0` (reserved for future multi-loop scenarios).
23/// Return `false` to abort: `AgentEnd` is emitted immediately with an empty message list.
24pub type BeforeLoopFn = Arc<dyn Fn(&[AgentMessage], usize) -> bool + Send + Sync>;
25/// Called once after the entire agent loop ends (after `AgentEnd` is emitted).
26///
27/// Arguments: `(new_messages, accumulated_usage)` — `new_messages` are the messages produced
28/// by this loop call; `accumulated_usage` sums input/output tokens across all turns.
29pub type AfterLoopFn = Arc<dyn Fn(&[AgentMessage], &Usage) + Send + Sync>;
30
31// ── Turn hooks ───────────────────────────────────────────────────────────────
32/// Called before each LLM turn (before `TurnStart` is emitted).
33///
34/// Arguments: `(messages, turn_index)` — `messages` is the full context (steering messages
35/// queued for *this* turn are not yet visible); `turn_index` is 0-based.
36/// Return `false` to abort the turn: no `TurnStart`/`TurnEnd` events are emitted,
37/// but `AgentEnd` still fires normally.
38pub type BeforeTurnFn = Arc<dyn Fn(&[AgentMessage], usize) -> bool + Send + Sync>;
39/// Called after each LLM turn (after `TurnEnd` is emitted).
40///
41/// Arguments: `(messages, turn_usage)` — `turn_usage` covers only this turn's tokens.
42/// Fires on both the normal path and the error/abort path.
43pub type AfterTurnFn = Arc<dyn Fn(&[AgentMessage], &Usage) + Send + Sync>;
44
45// ── Tool execution hooks ─────────────────────────────────────────────────────
46/// Called before each tool call (before `ToolExecutionStart` is emitted).
47///
48/// Arguments: `(tool_name, tool_call_id, args)`.
49/// Return `false` to skip the call: an error `ToolResult` is synthesised so the LLM still
50/// receives a response, but `ToolExecutionStart`/`End` are **not** emitted.
51pub type BeforeToolExecutionFn = Arc<dyn Fn(&str, &str, &serde_json::Value) -> bool + Send + Sync>;
52/// Called after each tool call (after `ToolExecutionEnd` is emitted).
53///
54/// Arguments: `(tool_name, tool_call_id, is_error)`.
55pub type AfterToolExecutionFn = Arc<dyn Fn(&str, &str, bool) + Send + Sync>;
56/// Called before each incremental tool update (before `ToolExecutionUpdate` is emitted).
57///
58/// Fires every time a tool calls `ctx.on_update(partial)` — potentially many times per call
59/// (e.g. each line of bash output). Arguments: `(tool_name, tool_call_id, text_content)`.
60/// Return `false` to suppress the streaming event; the tool keeps running and its final
61/// `ToolResult` (what the LLM sees) is **unaffected**.
62pub type BeforeToolExecutionUpdateFn = Arc<dyn Fn(&str, &str, &str) -> bool + Send + Sync>;
63/// Called after each incremental tool update (after `ToolExecutionUpdate` is emitted).
64///
65/// Only fires when the update was *not* suppressed by `BeforeToolExecutionUpdateFn`.
66/// Arguments: `(tool_name, tool_call_id, text_content)`.
67pub type AfterToolExecutionUpdateFn = Arc<dyn Fn(&str, &str, &str) + Send + Sync>;
68
69/// Called when the LLM returns `StopReason::Error`. Argument: the error message string.
70pub type OnErrorFn = Arc<dyn Fn(&str) + Send + Sync>;
71
72// ── Compaction hooks (G1) ───────────────────────────────────────────────────
73/// Called before compaction starts.
74///
75/// Arguments: `(estimated_tokens, message_count)`.
76/// Return `false` to skip compaction for this cycle.
77pub type BeforeCompactionStartFn = Arc<dyn Fn(usize, usize) -> bool + Send + Sync>;
78/// Called after compaction completes.
79///
80/// Arguments: `(messages_before, messages_after, tokens_before, tokens_after)`.
81pub type AfterCompactionEndFn = Arc<dyn Fn(usize, usize, usize, usize) + Send + Sync>;
82
83/// All static settings for a single [`agent_loop`] / [`agent_loop_continue`] call.
84///
85/// Build with the public fields directly or via [`crate::agent::Agent`]'s builder methods.
86/// The config is borrowed (`&AgentLoopConfig`) throughout the loop — it is never mutated.
87///
88/// ## Lifecycle hooks
89///
90/// All hook fields are `Option<Arc<dyn Fn(...)>>`. `None` means "no hook" (zero overhead).
91/// See the module-level doc for the guaranteed ordering relative to [`AgentEvent`]s.
92pub struct AgentLoopConfig {
93 /// Complete provider identity: model id, api_key, base_url, protocol, compat flags, cost rates.
94 /// The agent loop resolves the concrete `StreamProvider` from `model_config.api` via
95 /// `ProviderRegistry`. Set `provider_override` to bypass the registry for custom providers.
96 pub model_config: ModelConfig,
97
98 /// Custom provider override. When `Some`, bypasses `ProviderRegistry` dispatch and uses
99 /// this provider directly. Useful for testing (`MockProvider`) or custom implementations.
100 /// When `None` (the default), the provider is resolved from `model_config.api`.
101 pub provider_override: Option<Arc<dyn StreamProvider>>,
102
103 pub thinking_level: ThinkingLevel,
104 pub max_tokens: Option<u32>,
105 pub temperature: Option<f32>,
106
107 /// Convert AgentMessage[] → Message[] before each LLM call.
108 /// Default: keep only LLM-compatible messages.
109 pub convert_to_llm: Option<ConvertToLlmFn>,
110
111 /// Transform context before convert_to_llm (for pruning, compaction).
112 pub transform_context: Option<TransformContextFn>,
113
114 /// Get steering messages (user interruptions mid-run).
115 pub get_steering_messages: Option<GetMessagesFn>,
116
117 /// Get follow-up messages (queued work after agent finishes).
118 pub get_follow_up_messages: Option<GetMessagesFn>,
119
120 /// Context window configuration (auto-compaction).
121 /// Compaction strategies are now part of `ContextConfig.compaction` (G5 consolidation).
122 pub context_config: Option<ContextConfig>,
123
124 /// Execution limits (max turns, tokens, duration, cost).
125 /// Cost is tracked automatically using `model_config.cost` rates after each turn.
126 /// `ExecutionLimits.max_cost` enforcement is active whenever rates are non-zero.
127 pub execution_limits: Option<ExecutionLimits>,
128
129 /// Prompt caching configuration.
130 pub cache_config: CacheConfig, //from types.rs
131
132 /// Tool execution strategy (sequential, parallel, or batched).
133 pub tool_execution: ToolExecutionStrategy, // from types.rs
134
135 /// Per-tool execution timeout.
136 ///
137 /// When `Some(d)`, each individual `AgentTool::execute()` call is bounded by `d`.
138 /// On expiry, the tool's child cancel token is signalled (cooperative cleanup) and a
139 /// `ToolError::Timeout` is synthesised as the tool result — the LLM sees the failure
140 /// and the agent loop continues. A per-tool override via `AgentTool::timeout()` takes
141 /// precedence over this field. `None` (the default) means no per-tool timeout.
142 pub tool_timeout: Option<std::time::Duration>,
143
144 /// Retry configuration for transient provider errors.
145 pub retry_config: crate::provider::retry::RetryConfig,
146
147 //******* Callbacks Turn *******
148 /// Called before each LLM turn. Return `false` to abort the turn.
149 pub before_turn: Option<BeforeTurnFn>,
150 /// Called after each LLM turn with the current messages and the turn's usage.
151 pub after_turn: Option<AfterTurnFn>,
152
153 //******* Callbacks Loop *******
154 /// Called before each Agent loop. Return `false` to abort the loop.
155 pub before_loop: Option<BeforeLoopFn>,
156 /// Called after each Agent loop with the current messages and the loop's usage.
157 pub after_loop: Option<AfterLoopFn>,
158
159 //******* Callbacks Tool Execution *******
160 /// Called before each tool execution. Return `false` to skip the tool call.
161 pub before_tool_execution: Option<BeforeToolExecutionFn>,
162 /// Called after each tool execution.
163 pub after_tool_execution: Option<AfterToolExecutionFn>,
164 /// Called before each ToolExecutionUpdate event. Return `false` to suppress the event.
165 pub before_tool_execution_update: Option<BeforeToolExecutionUpdateFn>,
166 /// Called after each ToolExecutionUpdate event.
167 pub after_tool_execution_update: Option<AfterToolExecutionUpdateFn>,
168
169 /// Called when the LLM returns a `StopReason::Error`.
170 pub on_error: Option<OnErrorFn>,
171
172 //******* Callbacks Compaction (G1) *******
173 /// Called before compaction starts. Return `false` to skip compaction.
174 pub before_compaction_start: Option<BeforeCompactionStartFn>,
175 /// Called after compaction completes.
176 pub after_compaction_end: Option<AfterCompactionEndFn>,
177
178 /// Input filters applied to user messages before the LLM call.
179 /// Filters run in order; first `Reject` wins and discards any accumulated
180 /// warnings. `Warn` messages accumulate and are appended to the user message.
181 pub input_filters: Vec<Arc<dyn InputFilter>>, // from types.rs
182
183 /// The trigger type for the first TurnStart event in this run.
184 /// Defaults to `TurnTrigger::User`; set to `SubAgent` by sub-agent callers.
185 pub first_turn_trigger: TurnTrigger,
186
187 /// Stable identity for this config, used as the middle segment of `loop_id`:
188 /// `loop_id = "{session_id}.{config_id}.{N}"`
189 ///
190 /// When `None` and the `Agent` wrapper is used, the identity is auto-derived by
191 /// `Agent::next_loop_id()` from the provider, model, and thinking level:
192 /// `"{provider_id}.{model_slug}[.thinking]"`
193 ///
194 /// For direct callers of `agent_loop`, set `context.loop_id` explicitly — this field
195 /// is only read by `Agent::next_loop_id()` and has no effect inside `agent_loop` itself.
196 ///
197 /// Set explicitly for human-readable or deterministic loop IDs, e.g.:
198 /// `config.config_id = Some("experiment-A".to_string());`
199 /// → loop IDs: `ses_xyz.experiment-A.1`, `ses_xyz.experiment-A.2`, …
200 pub config_id: Option<String>,
201
202 /// G8 — Optional context translation strategy for cross-provider compatibility.
203 ///
204 /// When set, messages are translated through this strategy before being sent to
205 /// the LLM provider. This allows content types from one provider (e.g.,
206 /// `Content::Thinking` from Anthropic) to be translated or removed when targeting
207 /// a different provider. The translation is read-only — originals are never modified.
208 pub context_translation: Option<Arc<dyn ContextTranslationStrategy>>,
209
210 /// Shared state for PrunTool to communicate pruning requests to the loop.
211 pub prun_pending: Option<Arc<std::sync::Mutex<Vec<crate::tools::prun::PrunRequest>>>>,
212
213 /// Shared state for [`RevertTool`](crate::tools::RevertTool) to communicate
214 /// revert requests to the loop. `Some` iff the agent was constructed via
215 /// [`BasicAgent::with_revert_tool`](crate::agents::BasicAgent::with_revert_tool);
216 /// `apply_revert` (Phase 3) is gated on this being `Some`, so the LLM has
217 /// no path to invoke the tool when the builder did not opt in.
218 pub revert_pending: Option<Arc<std::sync::Mutex<Vec<crate::tools::revert::RevertRequest>>>>,
219
220 /// Desired LLM output shape. Default `Text` preserves the historical free-form
221 /// behaviour; `JsonObject` / `JsonSchema` request constrained structured output
222 /// from providers that support it. See `provider::ResponseFormat` and the
223 /// capability matrix in `docs/specs/developer/provider.md`.
224 pub response_format: ResponseFormat,
225}