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 })
413 }
414}
415
416/// Errors that can occur when assembling an [`AgentLoopConfig`] via
417/// [`Agent::build_config`].
418///
419/// Returned by the default trait impl when an [`Agent`] implementor neglected to
420/// override `model_config()`. `BasicAgent::build_config()` never returns this
421/// because its constructor requires a `ModelConfig`.
422#[derive(Debug, thiserror::Error)]
423pub enum AgentBuildError {
424 #[error(
425 "agent has no model_config; implement Agent::model_config() to return Some(...) \
426 or override Agent::build_config() entirely"
427 )]
428 MissingModelConfig,
429}