Skip to main content

phi_core/agents/
agent.rs

1//! The `Agent` trait — the runtime interface for all agent implementations.
2//!
3//! This trait defines the core capabilities that any agent must provide:
4//! prompting, state access, message management, and control. Builder methods
5//! (configuration-time concerns) are intentionally excluded — each concrete
6//! implementation provides its own builder API.
7//!
8//! # Implementations
9//!
10//! - [`BasicAgent`](super::BasicAgent) — the default in-memory implementation. Owns a single
11//!   linear message history and runs the `agent_loop` directly.
12//!
13//! # Object Safety
14//!
15//! The trait is object-safe: methods use `String` (not `impl Into<String>`)
16//! so `Box<dyn Agent>` and `&mut dyn Agent` work for runtime polymorphism.
17//!
18//! # Default Implementations
19//!
20//! - `prompt` / `prompt_messages` / `continue_loop` — delegate to the `_with_sender` variants.
21//! - `prompt_with_sender` — wraps text in `AgentMessage::Llm(LlmMessage::new(Message::user(...)))`, calls
22//!   `prompt_messages_with_sender`.
23//! - Steering/follow-up queue methods — no-ops. Override to support mid-run interrupts.
24//! - `last_loop_id` — returns `None`. Override if your impl tracks loop identity.
25
26use crate::agent_loop::{
27    AfterCompactionEndFn, AfterLoopFn, AfterToolExecutionFn, AfterToolExecutionUpdateFn,
28    AfterTurnFn, AgentLoopConfig, BeforeCompactionStartFn, BeforeLoopFn, BeforeToolExecutionFn,
29    BeforeToolExecutionUpdateFn, BeforeTurnFn, ConvertToLlmFn, TransformContextFn,
30};
31use crate::agents::AgentProfile;
32use crate::context::{ContextConfig, ExecutionLimits};
33use crate::provider::ModelConfig;
34use crate::types::*;
35use std::path::Path;
36use std::sync::Arc;
37use tokio::sync::mpsc;
38
39/// Controls how messages are drained from the steering/follow-up queues per turn.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum QueueMode {
42    /// Deliver one message per turn — allows the LLM to react to each steering message individually.
43    OneAtATime,
44    /// Deliver all queued messages at once — batches all pending steers into one turn.
45    All,
46}
47
48/// The core runtime interface for an agent.
49///
50/// Programs against this trait to remain independent of the specific agent implementation.
51/// Use [`BasicAgent`](super::BasicAgent) for the default in-memory implementation, or implement
52/// this trait for richer implementations with persistence, branching, or distributed execution.
53///
54/// # Required Methods
55///
56/// The two primary required methods are `prompt_messages_with_sender` and
57/// `continue_loop_with_sender` — all other prompting variants have default implementations
58/// that delegate to these.
59#[async_trait::async_trait]
60pub trait Agent: Send {
61    // ── Prompting (required) ─────────────────────────────────────────────────────
62
63    /// Send messages as a prompt, streaming events to a caller-provided sender.
64    ///
65    /// This is the primary required prompting method — all other `prompt*` variants
66    /// have default implementations that delegate here.
67    async fn prompt_messages_with_sender(
68        &mut self,
69        messages: Vec<AgentMessage>,
70        tx: mpsc::UnboundedSender<AgentEvent>,
71    );
72
73    /// Continue from current context, streaming events to a caller-provided sender.
74    ///
75    /// `kind` describes how this continuation relates to prior loops:
76    /// - `Default` — unspecified continuation
77    /// - `Rerun { tag }` — retry from the same context state
78    /// - `Branch { tag }` — explore a different path from the same starting point
79    async fn continue_loop_with_sender(
80        &mut self,
81        tx: mpsc::UnboundedSender<AgentEvent>,
82        kind: ContinuationKind,
83    );
84
85    // ── Prompting (defaulted via _with_sender) ───────────────────────────────────
86
87    /// Send a text prompt, streaming events to a caller-provided sender.
88    ///
89    /// Default: wraps `text` in `AgentMessage::Llm(LlmMessage::new(Message::user(text)))` and calls
90    /// `prompt_messages_with_sender`.
91    async fn prompt_with_sender(&mut self, text: String, tx: mpsc::UnboundedSender<AgentEvent>) {
92        let msg = AgentMessage::Llm(LlmMessage::new(Message::user(text)));
93        self.prompt_messages_with_sender(vec![msg], tx).await;
94    }
95
96    /// Send a text prompt. Returns a stream of `AgentEvent`s.
97    ///
98    /// Default: creates an internal channel and calls `prompt_with_sender`.
99    async fn prompt(&mut self, text: String) -> mpsc::UnboundedReceiver<AgentEvent> {
100        let (tx, rx) = mpsc::unbounded_channel();
101        self.prompt_with_sender(text, tx).await;
102        rx
103    }
104
105    /// Send messages as a prompt. Returns a stream of `AgentEvent`s.
106    ///
107    /// Default: creates an internal channel and calls `prompt_messages_with_sender`.
108    async fn prompt_messages(
109        &mut self,
110        messages: Vec<AgentMessage>,
111    ) -> mpsc::UnboundedReceiver<AgentEvent> {
112        let (tx, rx) = mpsc::unbounded_channel();
113        self.prompt_messages_with_sender(messages, tx).await;
114        rx
115    }
116
117    /// Continue from current context. Returns a stream of `AgentEvent`s.
118    ///
119    /// Default: creates an internal channel and calls `continue_loop_with_sender(Default)`.
120    async fn continue_loop(&mut self) -> mpsc::UnboundedReceiver<AgentEvent> {
121        let (tx, rx) = mpsc::unbounded_channel();
122        self.continue_loop_with_sender(tx, ContinuationKind::Default)
123            .await;
124        rx
125    }
126
127    // ── State (required) ─────────────────────────────────────────────────────────
128
129    /// Full message history.
130    fn messages(&self) -> &[AgentMessage];
131
132    /// Whether the agent is currently running a loop.
133    fn is_streaming(&self) -> bool;
134
135    /// Stable UUID assigned at construction; included in every `AgentStart` event.
136    fn agent_id(&self) -> &str;
137
138    /// Stable UUID assigned at construction; groups all loops from this instance.
139    fn session_id(&self) -> &str;
140
141    // ── State (defaulted) ────────────────────────────────────────────────────────
142
143    /// The `loop_id` of the most recently started loop; `None` before first run.
144    ///
145    /// Default: returns `None`. Override to track loop identity.
146    fn last_loop_id(&self) -> Option<&str> {
147        None
148    }
149
150    // ── Message mutation (required) ──────────────────────────────────────────────
151
152    /// Clear all messages from history.
153    fn clear_messages(&mut self);
154
155    /// Append a single message to history.
156    fn append_message(&mut self, msg: AgentMessage);
157
158    /// Replace the entire message history.
159    fn replace_messages(&mut self, msgs: Vec<AgentMessage>);
160
161    /// Serialize message history to JSON.
162    fn save_messages(&self) -> Result<String, serde_json::Error>;
163
164    /// Restore message history from JSON.
165    fn restore_messages(&mut self, json: &str) -> Result<(), serde_json::Error>;
166
167    /// Replace the tool set.
168    fn set_tools(&mut self, tools: Vec<Arc<dyn AgentTool>>);
169
170    // ── Control (required) ───────────────────────────────────────────────────────
171
172    /// Cancel the current run via `CancellationToken`.
173    fn abort(&self);
174
175    /// Clear all state (messages, queues, streaming flag).
176    fn reset(&mut self);
177
178    // ── Steering/follow-up queues (defaulted — no-ops) ───────────────────────────
179
180    /// Queue a steering message — interrupts the agent mid-tool-execution.
181    ///
182    /// Default: no-op. Override to support mid-run interrupts.
183    fn steer(&self, _msg: AgentMessage) {}
184
185    /// Queue a follow-up message — processed after the current agent turn completes.
186    ///
187    /// Default: no-op.
188    fn follow_up(&self, _msg: AgentMessage) {}
189
190    /// Clear all pending steering messages. Default: no-op.
191    fn clear_steering_queue(&self) {}
192
193    /// Clear all pending follow-up messages. Default: no-op.
194    fn clear_follow_up_queue(&self) {}
195
196    /// Clear both steering and follow-up queues.
197    fn clear_all_queues(&self) {
198        self.clear_steering_queue();
199        self.clear_follow_up_queue();
200    }
201
202    /// Set how steering messages are delivered. Default: no-op.
203    fn set_steering_mode(&mut self, _mode: QueueMode) {}
204
205    /// Set how follow-up messages are delivered. Default: no-op.
206    fn set_follow_up_mode(&mut self, _mode: QueueMode) {}
207
208    // ── Configuration access (defaulted) ──────────────────────────────────
209
210    /// The agent's profile blueprint. Default: `None`.
211    fn profile(&self) -> Option<&AgentProfile> {
212        None
213    }
214
215    /// The agent's system prompt. Default: empty string.
216    fn system_prompt(&self) -> &str {
217        ""
218    }
219
220    /// The agent's model configuration. Default: `None`.
221    fn model_config(&self) -> Option<&ModelConfig> {
222        None
223    }
224
225    /// The agent's thinking level. Default: `ThinkingLevel::Off`.
226    fn thinking_level(&self) -> ThinkingLevel {
227        ThinkingLevel::Off
228    }
229
230    /// The agent's temperature setting. Default: `None`.
231    fn temperature(&self) -> Option<f32> {
232        None
233    }
234
235    /// The agent's max tokens setting. Default: `None`.
236    fn max_tokens(&self) -> Option<u32> {
237        None
238    }
239
240    /// The agent's context config. Default: `None`.
241    fn context_config(&self) -> Option<&ContextConfig> {
242        None
243    }
244
245    /// The agent's execution limits. Default: `None`.
246    fn execution_limits(&self) -> Option<&ExecutionLimits> {
247        None
248    }
249
250    /// The agent's cache config. Default: `CacheConfig::default()`.
251    fn cache_config(&self) -> CacheConfig {
252        CacheConfig::default()
253    }
254
255    /// The agent's tool execution strategy. Default: `ToolExecutionStrategy::default()`.
256    fn tool_execution(&self) -> ToolExecutionStrategy {
257        ToolExecutionStrategy::default()
258    }
259
260    /// The agent's per-tool execution timeout. Default: `None` (no per-tool timeout).
261    ///
262    /// When `Some(d)`, each `AgentTool::execute()` call is bounded by `d`. An
263    /// individual tool's `AgentTool::timeout()` override takes precedence. On timeout
264    /// the tool's child cancel token is fired and a `ToolError::Timeout` is returned
265    /// to the LLM — the agent loop continues.
266    fn tool_timeout(&self) -> Option<std::time::Duration> {
267        None
268    }
269
270    /// The agent's desired output shape. Default: `ResponseFormat::Text` (free-form text).
271    ///
272    /// Override on agents that want JSON-mode output by default. See
273    /// `provider::ResponseFormat` and the capability matrix in
274    /// `docs/specs/developer/provider.md` for per-provider coverage.
275    fn response_format(&self) -> crate::provider::ResponseFormat {
276        crate::provider::ResponseFormat::Text
277    }
278
279    /// The agent's retry config. Default: `RetryConfig::default()`.
280    fn retry_config(&self) -> crate::provider::retry::RetryConfig {
281        crate::provider::retry::RetryConfig::default()
282    }
283
284    // ── Session (defaulted) ───────────────────────────────────────────────
285
286    /// The agent's current session. Default: `None`.
287    fn session(&self) -> Option<&crate::session::Session> {
288        None
289    }
290
291    /// The agent's workspace directory. File paths in system prompt blocks
292    /// resolve relative to this. Default: `None` (uses current directory).
293    fn workspace(&self) -> Option<&Path> {
294        None
295    }
296
297    // ── Hook setters (defaulted — no-ops) ─────────────────────────────────
298
299    /// Set the before-turn hook. Default: no-op.
300    fn set_before_turn(&mut self, _f: Option<BeforeTurnFn>) {}
301
302    /// Set the after-turn hook. Default: no-op.
303    fn set_after_turn(&mut self, _f: Option<AfterTurnFn>) {}
304
305    /// Set the before-loop hook. Default: no-op.
306    fn set_before_loop(&mut self, _f: Option<BeforeLoopFn>) {}
307
308    /// Set the after-loop hook. Default: no-op.
309    fn set_after_loop(&mut self, _f: Option<AfterLoopFn>) {}
310
311    /// Set the before-tool-execution hook. Default: no-op.
312    fn set_before_tool_execution(&mut self, _f: Option<BeforeToolExecutionFn>) {}
313
314    /// Set the after-tool-execution hook. Default: no-op.
315    fn set_after_tool_execution(&mut self, _f: Option<AfterToolExecutionFn>) {}
316
317    /// Set the before-tool-execution-update hook. Default: no-op.
318    fn set_before_tool_execution_update(&mut self, _f: Option<BeforeToolExecutionUpdateFn>) {}
319
320    /// Set the after-tool-execution-update hook. Default: no-op.
321    fn set_after_tool_execution_update(&mut self, _f: Option<AfterToolExecutionUpdateFn>) {}
322
323    /// Set the convert-to-LLM function. Default: no-op.
324    fn set_convert_to_llm(&mut self, _f: Option<ConvertToLlmFn>) {}
325
326    /// Set the transform-context function. Default: no-op.
327    fn set_transform_context(&mut self, _f: Option<TransformContextFn>) {}
328
329    /// Set the block compaction strategy. Default: no-op.
330    fn set_block_compaction_strategy(
331        &mut self,
332        _s: Option<Arc<dyn crate::context::BlockCompactionStrategy>>,
333    ) {
334    }
335
336    /// Set the before-compaction-start hook (G1). Default: no-op.
337    fn set_before_compaction_start(&mut self, _f: Option<BeforeCompactionStartFn>) {}
338
339    /// Set the after-compaction-end hook (G1). Default: no-op.
340    fn set_after_compaction_end(&mut self, _f: Option<AfterCompactionEndFn>) {}
341
342    /// Enable or disable the prun tool. Default: no-op.
343    fn set_prun_enabled(&mut self, _enabled: bool) {}
344
345    /// Set the context translation strategy (G8). Default: no-op.
346    fn set_context_translation(
347        &mut self,
348        _s: Option<Arc<dyn crate::provider::context_translation::ContextTranslationStrategy>>,
349    ) {
350    }
351
352    /// Get the context translation strategy (G8). Default: None.
353    fn context_translation(
354        &self,
355    ) -> Option<Arc<dyn crate::provider::context_translation::ContextTranslationStrategy>> {
356        None
357    }
358
359    // ── Config assembly (defaulted) ───────────────────────────────────────
360
361    /// Assemble an [`AgentLoopConfig`] from this agent's current settings.
362    ///
363    /// The default implementation builds a config from the trait's accessor methods.
364    /// `BasicAgent` overrides this to additionally wire steering queues, hooks, and
365    /// other implementation-specific state.
366    ///
367    /// # Errors
368    ///
369    /// Returns `Err(AgentBuildError::MissingModelConfig)` if `model_config()` returns
370    /// `None`. Implementors of custom `Agent` types must either override
371    /// `model_config()` to return `Some(...)` or override `build_config()` entirely.
372    /// `BasicAgent` always returns `Ok(...)` because its constructor requires a
373    /// `ModelConfig`.
374    fn build_config(&self) -> Result<AgentLoopConfig, AgentBuildError> {
375        let model_config = self
376            .model_config()
377            .ok_or(AgentBuildError::MissingModelConfig)?
378            .clone();
379        Ok(AgentLoopConfig {
380            model_config,
381            provider_override: None,
382            thinking_level: self.thinking_level(),
383            max_tokens: self.max_tokens(),
384            temperature: self.temperature(),
385            convert_to_llm: None,
386            transform_context: None,
387            get_steering_messages: None,
388            get_follow_up_messages: None,
389            context_config: self.context_config().cloned(),
390            execution_limits: self.execution_limits().cloned(),
391            cache_config: self.cache_config(),
392            tool_execution: self.tool_execution(),
393            tool_timeout: self.tool_timeout(),
394            response_format: self.response_format(),
395            retry_config: self.retry_config(),
396            before_turn: None,
397            after_turn: None,
398            before_loop: None,
399            after_loop: None,
400            before_tool_execution: None,
401            after_tool_execution: None,
402            before_tool_execution_update: None,
403            after_tool_execution_update: None,
404            before_compaction_start: None,
405            after_compaction_end: None,
406            on_error: None,
407            input_filters: vec![],
408            first_turn_trigger: TurnTrigger::User,
409            config_id: None,
410            context_translation: self.context_translation(),
411            prun_pending: None,
412            revert_pending: None,
413            current_tool: None,
414            revert_render_policy: RevertRenderPolicy::default(),
415        })
416    }
417}
418
419/// Errors that can occur when assembling an [`AgentLoopConfig`] via
420/// [`Agent::build_config`].
421///
422/// Returned by the default trait impl when an [`Agent`] implementor neglected to
423/// override `model_config()`. `BasicAgent::build_config()` never returns this
424/// because its constructor requires a `ModelConfig`.
425#[derive(Debug, thiserror::Error)]
426pub enum AgentBuildError {
427    #[error(
428        "agent has no model_config; implement Agent::model_config() to return Some(...) \
429         or override Agent::build_config() entirely"
430    )]
431    MissingModelConfig,
432}