Skip to main content

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::future::Future;
6use std::pin::Pin;
7use std::sync::Arc;
8
9// ── Context transformation callbacks ────────────────────────────────────────
10/// All hook types use `Arc` (shared ownership) so they can be cloned into closures
11/// and stored without lifetime complications. `Box<dyn Fn>` would suffice for single-owner
12/// cases but `Arc` makes it trivially cheap to share across async tasks.
13/// Converts `AgentMessage[]` → `Message[]` before each LLM call.
14pub type ConvertToLlmFn = Arc<dyn Fn(&[AgentMessage]) -> Vec<Message> + Send + Sync>;
15/// Transforms the full context before `convert_to_llm` (for pruning, reordering, injection).
16pub type TransformContextFn = Arc<dyn Fn(Vec<AgentMessage>) -> Vec<AgentMessage> + Send + Sync>;
17/// Returns pending messages (steering interrupts or follow-up work) when polled.
18pub type GetMessagesFn = Box<dyn Fn() -> Vec<AgentMessage> + Send + Sync>;
19
20// ── 0.9.0 async lifecycle hooks ─────────────────────────────────────────────
21//
22// All lifecycle Fn types below are async-trait-style boxed futures. To
23// construct one from a sync closure body:
24//
25// ```rust,ignore
26// let hook: BeforeTurnFn = Arc::new(|messages, turn| {
27//     Box::pin(async move {
28//         // sync logic ...
29//         true
30//     })
31// });
32// ```
33//
34// For async closure bodies, simply `.await` inside the `async move` block.
35
36/// Boxed-future return type used by all 0.9.0 async lifecycle hooks. `T` is the
37/// hook's logical return value (often `bool` for veto-returning hooks or `()`
38/// for fire-and-forget hooks).
39pub type HookFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
40
41// ── Loop hooks ───────────────────────────────────────────────────────────────
42/// Called once before the entire agent loop begins (before `AgentStart` is emitted).
43///
44/// Arguments: `(messages, loop_index)` — `messages` is the full context at the time of the call;
45/// `loop_index` is always `0` (reserved for future multi-loop scenarios).
46/// Return `false` to abort: `AgentEnd` is emitted immediately with an empty message list.
47///
48/// 0.9.0: async hook. Wrap sync bodies in `Box::pin(async move { ... })`.
49pub type BeforeLoopFn =
50    Arc<dyn for<'a> Fn(&'a [AgentMessage], usize) -> HookFuture<'a, bool> + Send + Sync>;
51/// Called once after the entire agent loop ends (after `AgentEnd` is emitted).
52///
53/// Arguments: `(new_messages, accumulated_usage)` — `new_messages` are the messages produced
54/// by this loop call; `accumulated_usage` sums input/output tokens across all turns.
55///
56/// 0.9.0: async hook.
57pub type AfterLoopFn =
58    Arc<dyn for<'a> Fn(&'a [AgentMessage], &'a Usage) -> HookFuture<'a, ()> + Send + Sync>;
59
60// ── Turn hooks ───────────────────────────────────────────────────────────────
61/// Called before each LLM turn (before `TurnStart` is emitted).
62///
63/// Arguments: `(messages, turn_index)` — `messages` is the full context (steering messages
64/// queued for *this* turn are not yet visible); `turn_index` is 0-based.
65/// Return `false` to abort the turn: no `TurnStart`/`TurnEnd` events are emitted,
66/// but `AgentEnd` still fires normally.
67///
68/// 0.9.0: async hook.
69pub type BeforeTurnFn =
70    Arc<dyn for<'a> Fn(&'a [AgentMessage], usize) -> HookFuture<'a, bool> + Send + Sync>;
71/// Called after each LLM turn (after `TurnEnd` is emitted).
72///
73/// Arguments: `(messages, turn_usage)` — `turn_usage` covers only this turn's tokens.
74/// Fires on both the normal path and the error/abort path.
75///
76/// 0.9.0: async hook.
77pub type AfterTurnFn =
78    Arc<dyn for<'a> Fn(&'a [AgentMessage], &'a Usage) -> HookFuture<'a, ()> + Send + Sync>;
79
80// ── Tool execution hooks ─────────────────────────────────────────────────────
81/// Called before each tool call (before `ToolExecutionStart` is emitted).
82///
83/// Arguments: `(tool_name, tool_call_id, args)`.
84/// Return `false` to skip the call: an error `ToolResult` is synthesised so the LLM still
85/// receives a response, but `ToolExecutionStart`/`End` are **not** emitted.
86///
87/// 0.9.0: async hook.
88pub type BeforeToolExecutionFn = Arc<
89    dyn for<'a> Fn(&'a str, &'a str, &'a serde_json::Value) -> HookFuture<'a, bool> + Send + Sync,
90>;
91/// Called after each tool call (after `ToolExecutionEnd` is emitted).
92///
93/// Arguments: `(tool_name, tool_call_id, is_error)`.
94///
95/// 0.9.0: async hook.
96pub type AfterToolExecutionFn =
97    Arc<dyn for<'a> Fn(&'a str, &'a str, bool) -> HookFuture<'a, ()> + Send + Sync>;
98/// Called before each incremental tool update (before `ToolExecutionUpdate` is emitted).
99///
100/// Fires every time a tool calls `ctx.on_update(partial)` — potentially many times per call
101/// (e.g. each line of bash output). Arguments: `(tool_name, tool_call_id, text_content)`.
102/// Return `false` to suppress the streaming event; the tool keeps running and its final
103/// `ToolResult` (what the LLM sees) is **unaffected**.
104///
105/// 0.10.0: async hook (matches the 9 other lifecycle Fns async-migrated at
106/// 0.9.0). Wrap sync bodies in `Box::pin(async move { ... })`. The closure
107/// fires from inside the synchronous `ToolUpdateFn` callback that tools
108/// invoke during their async execute body; the agent loop bridges to the
109/// async hook via a `futures::executor::block_on` shim at the call site —
110/// see `agent_loop/tools.rs`.
111pub type BeforeToolExecutionUpdateFn =
112    Arc<dyn for<'a> Fn(&'a str, &'a str, &'a str) -> HookFuture<'a, bool> + Send + Sync>;
113/// Called after each incremental tool update (after `ToolExecutionUpdate` is emitted).
114///
115/// Only fires when the update was *not* suppressed by `BeforeToolExecutionUpdateFn`.
116/// Arguments: `(tool_name, tool_call_id, text_content)`.
117///
118/// 0.10.0: async hook. See [`BeforeToolExecutionUpdateFn`] for the
119/// bridging-from-sync-ToolUpdateFn rationale.
120pub type AfterToolExecutionUpdateFn =
121    Arc<dyn for<'a> Fn(&'a str, &'a str, &'a str) -> HookFuture<'a, ()> + Send + Sync>;
122
123/// Called when the LLM returns `StopReason::Error`. Argument: the error message string.
124///
125/// 0.9.0: async hook.
126pub type OnErrorFn = Arc<dyn for<'a> Fn(&'a str) -> HookFuture<'a, ()> + Send + Sync>;
127
128// ── Compaction hooks (G1) ───────────────────────────────────────────────────
129/// Called before compaction starts.
130///
131/// Arguments: `(estimated_tokens, message_count)`.
132/// Return `false` to skip compaction for this cycle.
133///
134/// 0.9.0: async hook.
135pub type BeforeCompactionStartFn =
136    Arc<dyn Fn(usize, usize) -> HookFuture<'static, bool> + Send + Sync>;
137/// Called after compaction completes.
138///
139/// Arguments: `(messages_before, messages_after, tokens_before, tokens_after)`.
140///
141/// 0.9.0: async hook.
142pub type AfterCompactionEndFn =
143    Arc<dyn Fn(usize, usize, usize, usize) -> HookFuture<'static, ()> + Send + Sync>;
144
145/// All static settings for a single [`agent_loop`] / [`agent_loop_continue`] call.
146///
147/// Build with the public fields directly or via [`crate::agent::Agent`]'s builder methods.
148/// The config is borrowed (`&AgentLoopConfig`) throughout the loop — it is never mutated.
149///
150/// ## Lifecycle hooks
151///
152/// All hook fields are `Option<Arc<dyn Fn(...)>>`. `None` means "no hook" (zero overhead).
153/// See the module-level doc for the guaranteed ordering relative to [`AgentEvent`]s.
154pub struct AgentLoopConfig {
155    /// Complete provider identity: model id, api_key, base_url, protocol, compat flags, cost rates.
156    /// The agent loop resolves the concrete `StreamProvider` from `model_config.api` via
157    /// `ProviderRegistry`. Set `provider_override` to bypass the registry for custom providers.
158    pub model_config: ModelConfig,
159
160    /// Custom provider override. When `Some`, bypasses `ProviderRegistry` dispatch and uses
161    /// this provider directly. Useful for testing (`MockProvider`) or custom implementations.
162    /// When `None` (the default), the provider is resolved from `model_config.api`.
163    pub provider_override: Option<Arc<dyn StreamProvider>>,
164
165    pub thinking_level: ThinkingLevel,
166    pub max_tokens: Option<u32>,
167    pub temperature: Option<f32>,
168
169    /// Convert AgentMessage[] → Message[] before each LLM call.
170    /// Default: keep only LLM-compatible messages.
171    pub convert_to_llm: Option<ConvertToLlmFn>,
172
173    /// Transform context before convert_to_llm (for pruning, compaction).
174    pub transform_context: Option<TransformContextFn>,
175
176    /// Get steering messages (user interruptions mid-run).
177    pub get_steering_messages: Option<GetMessagesFn>,
178
179    /// Get follow-up messages (queued work after agent finishes).
180    pub get_follow_up_messages: Option<GetMessagesFn>,
181
182    /// Context window configuration (auto-compaction).
183    /// Compaction strategies are now part of `ContextConfig.compaction` (G5 consolidation).
184    pub context_config: Option<ContextConfig>,
185
186    /// Execution limits (max turns, tokens, duration, cost).
187    /// Cost is tracked automatically using `model_config.cost` rates after each turn.
188    /// `ExecutionLimits.max_cost` enforcement is active whenever rates are non-zero.
189    pub execution_limits: Option<ExecutionLimits>,
190
191    /// Prompt caching configuration.
192    pub cache_config: CacheConfig, //from types.rs
193
194    /// Tool execution strategy (sequential, parallel, or batched).
195    pub tool_execution: ToolExecutionStrategy, // from types.rs
196
197    /// Per-tool execution timeout.
198    ///
199    /// When `Some(d)`, each individual `AgentTool::execute()` call is bounded by `d`.
200    /// On expiry, the tool's child cancel token is signalled (cooperative cleanup) and a
201    /// `ToolError::Timeout` is synthesised as the tool result — the LLM sees the failure
202    /// and the agent loop continues. A per-tool override via `AgentTool::timeout()` takes
203    /// precedence over this field. `None` (the default) means no per-tool timeout.
204    pub tool_timeout: Option<std::time::Duration>,
205
206    /// Retry configuration for transient provider errors.
207    pub retry_config: crate::provider::retry::RetryConfig,
208
209    //******* Callbacks Turn *******
210    /// Called before each LLM turn. Return `false` to abort the turn.
211    pub before_turn: Option<BeforeTurnFn>,
212    /// Called after each LLM turn with the current messages and the turn's usage.
213    pub after_turn: Option<AfterTurnFn>,
214
215    //******* Callbacks Loop *******
216    /// Called before each Agent loop. Return `false` to abort the loop.
217    pub before_loop: Option<BeforeLoopFn>,
218    /// Called after each Agent loop with the current messages and the loop's usage.
219    pub after_loop: Option<AfterLoopFn>,
220
221    //******* Callbacks Tool Execution *******
222    /// Called before each tool execution. Return `false` to skip the tool call.
223    pub before_tool_execution: Option<BeforeToolExecutionFn>,
224    /// Called after each tool execution.
225    pub after_tool_execution: Option<AfterToolExecutionFn>,
226    /// Called before each ToolExecutionUpdate event. Return `false` to suppress the event.
227    pub before_tool_execution_update: Option<BeforeToolExecutionUpdateFn>,
228    /// Called after each ToolExecutionUpdate event.
229    pub after_tool_execution_update: Option<AfterToolExecutionUpdateFn>,
230
231    /// Called when the LLM returns a `StopReason::Error`.
232    pub on_error: Option<OnErrorFn>,
233
234    //******* Callbacks Compaction (G1) *******
235    /// Called before compaction starts. Return `false` to skip compaction.
236    pub before_compaction_start: Option<BeforeCompactionStartFn>,
237    /// Called after compaction completes.
238    pub after_compaction_end: Option<AfterCompactionEndFn>,
239
240    /// Input filters applied to user messages before the LLM call.
241    /// Filters run in order; first `Reject` wins and discards any accumulated
242    /// warnings. `Warn` messages accumulate and are appended to the user message.
243    pub input_filters: Vec<Arc<dyn InputFilter>>, // from types.rs
244
245    /// The trigger type for the first TurnStart event in this run.
246    /// Defaults to `TurnTrigger::User`; set to `SubAgent` by sub-agent callers.
247    pub first_turn_trigger: TurnTrigger,
248
249    /// Stable identity for this config, used as the middle segment of `loop_id`:
250    ///   `loop_id = "{session_id}.{config_id}.{N}"`
251    ///
252    /// When `None` and the `Agent` wrapper is used, the identity is auto-derived by
253    /// `Agent::next_loop_id()` from the provider, model, and thinking level:
254    ///   `"{provider_id}.{model_slug}[.thinking]"`
255    ///
256    /// For direct callers of `agent_loop`, set `context.loop_id` explicitly — this field
257    /// is only read by `Agent::next_loop_id()` and has no effect inside `agent_loop` itself.
258    ///
259    /// Set explicitly for human-readable or deterministic loop IDs, e.g.:
260    ///   `config.config_id = Some("experiment-A".to_string());`
261    ///   → loop IDs: `ses_xyz.experiment-A.1`, `ses_xyz.experiment-A.2`, …
262    pub config_id: Option<String>,
263
264    /// G8 — Optional context translation strategy for cross-provider compatibility.
265    ///
266    /// When set, messages are translated through this strategy before being sent to
267    /// the LLM provider. This allows content types from one provider (e.g.,
268    /// `Content::Thinking` from Anthropic) to be translated or removed when targeting
269    /// a different provider. The translation is read-only — originals are never modified.
270    pub context_translation: Option<Arc<dyn ContextTranslationStrategy>>,
271
272    /// Shared state for PrunTool to communicate pruning requests to the loop.
273    pub prun_pending: Option<Arc<std::sync::Mutex<Vec<crate::tools::prun::PrunRequest>>>>,
274
275    /// Shared state for [`RevertTool`](crate::tools::RevertTool) to communicate
276    /// revert requests to the loop. `Some` iff the agent was constructed via
277    /// [`BasicAgent::with_revert_tool`](crate::agents::BasicAgent::with_revert_tool);
278    /// `apply_revert` (Phase 3) is gated on this being `Some`, so the LLM has
279    /// no path to invoke the tool when the builder did not opt in.
280    pub revert_pending: Option<Arc<std::sync::Mutex<Vec<crate::tools::revert::RevertRequest>>>>,
281
282    /// Shared slot recording the currently-executing tool.
283    ///
284    /// `Some(Arc<Mutex<Option<CurrentToolExecution>>>)` when the agent loop is
285    /// constructed via [`BasicAgent`](crate::agents::BasicAgent) (always
286    /// installed by `build_config`); `None` for direct callers of
287    /// `agent_loop()` who do not need pause-time introspection.
288    ///
289    /// The agent loop's `execute_single_tool` writes
290    /// `Some(CurrentToolExecution { name, timeout })` immediately before
291    /// invoking `AgentTool::execute()` and resets to `None` on return. External
292    /// consumers read the slot via the shared `Arc<Mutex<...>>` — see
293    /// [`BasicAgent::current_tool_timeout`](crate::agents::BasicAgent::current_tool_timeout).
294    ///
295    /// Single-tool model: under parallel / batched execution the slot reflects
296    /// the most-recently-started tool. See
297    /// [`CurrentToolExecution`](crate::context::CurrentToolExecution) for the
298    /// race-window characterization.
299    pub current_tool: Option<Arc<std::sync::Mutex<Option<crate::context::CurrentToolExecution>>>>,
300
301    /// Kind-aware render policy for Composition I trunk-context assembly.
302    ///
303    /// Consumed by the agent loop's context-build site
304    /// (`stream_assistant_response`) only when revert mode is active
305    /// (`active_node_id` is `Some`) — i.e. when the agent was constructed via
306    /// [`BasicAgent::with_revert_tool`](crate::agents::BasicAgent::with_revert_tool)
307    /// and `apply_revert` has set the active trunk pointer at least once.
308    /// Outside revert mode this field has no effect; the linear
309    /// [`build_working_context`](crate::types::AgentContext::build_working_context)
310    /// path is byte-identical to pre-0.10 behaviour.
311    ///
312    /// Defaults to `RevertRenderPolicy::default()` (5-turn window,
313    /// 3-tag-per-kind count cap). Override via
314    /// [`BasicAgent::with_revert_render_policy`](crate::agents::BasicAgent::with_revert_render_policy)
315    /// when downstream operators (e.g. i-phi's braking config) need a
316    /// different decay window.
317    pub revert_render_policy: RevertRenderPolicy,
318
319    /// Desired LLM output shape. Default `Text` preserves the historical free-form
320    /// behaviour; `JsonObject` / `JsonSchema` request constrained structured output
321    /// from providers that support it. See `provider::ResponseFormat` and the
322    /// capability matrix in `docs/specs/developer/provider.md`.
323    pub response_format: ResponseFormat,
324}