Skip to main content

agent_base/engine/runtime/
mod.rs

1use std::sync::Arc;
2
3use tokio_util::sync::CancellationToken;
4
5use crate::engine::AgentSession;
6use crate::engine::session_store::SessionStore;
7use crate::types::{
8    AgentConfig, AgentError, AgentResult, CheckpointData, MessageRole, RunOutcome, RuntimeEvent,
9    SessionId, TurnContext,
10};
11
12use super::approval::ApprovalHandler;
13use crate::tool::ToolPolicy;
14
15mod event_bus;
16pub(crate) use event_bus::EventBus;
17mod llm_engine;
18mod message_queue;
19mod plan_runner;
20mod react;
21mod session_manager;
22mod tool_engine;
23
24pub(super) const DEFAULT_MAX_TURNS: u32 = 160;
25
26pub use llm_engine::LlmEngine;
27pub use message_queue::QueueMode;
28pub(crate) use plan_runner::RuntimeCore;
29pub use session_manager::SessionManager;
30pub(crate) use tool_engine::ToolEngine;
31
32#[derive(Clone)]
33pub struct AgentRuntime {
34    pub(crate) runner: Arc<RuntimeCore>,
35}
36
37impl AgentRuntime {
38    pub async fn create_session(&self) -> SessionId {
39        let config = self.runner.config.read().await;
40        self.runner
41            .session_manager
42            .create_session(config.system_prompt.as_deref())
43            .await
44    }
45
46    pub async fn restore_session(&self, session_id: &SessionId) -> Option<AgentSession> {
47        self.runner
48            .session_manager
49            .restore_session(session_id)
50            .await
51    }
52
53    pub async fn session(&self, session_id: &SessionId) -> Option<AgentSession> {
54        self.runner.session_manager.session(session_id).await
55    }
56
57    pub async fn session_or_err(&self, session_id: &SessionId) -> AgentResult<AgentSession> {
58        self.runner.session_manager.session_or_err(session_id).await
59    }
60
61    pub async fn with_session_mut<F, R>(&self, session_id: &SessionId, f: F) -> AgentResult<R>
62    where
63        F: FnOnce(&mut AgentSession) -> R,
64    {
65        self.runner
66            .session_manager
67            .with_session_mut(session_id, f)
68            .await
69    }
70
71    pub fn emit_event(&self, event: RuntimeEvent) {
72        self.runner.event_bus.emit(event);
73    }
74
75    /// Subscribe to runtime events from the internal broadcast channel.
76    ///
77    /// Events are delivered directly from the runtime's event bus (capacity 2048).
78    /// Slow consumers may receive `Lagged(n)` errors if they cannot keep up —
79    /// ensure the receiver loop processes events promptly or use a buffering
80    /// layer in the consumer if backpressure is a concern.
81    pub fn subscribe_runtime_events(&self) -> tokio::sync::broadcast::Receiver<RuntimeEvent> {
82        self.runner.event_bus.subscribe()
83    }
84
85    pub fn session_manager(&self) -> &SessionManager {
86        &self.runner.session_manager
87    }
88
89    pub fn llm_engine(&self) -> &LlmEngine {
90        &self.runner.llm_engine
91    }
92
93    pub fn provider(&self) -> Arc<dyn llm_trait::LlmProvider> {
94        self.runner.llm_engine.get_provider()
95    }
96
97    /// Replace the LLM provider at runtime (e.g., model switch).
98    /// Requires `&mut self` — obtain via `runtime.lock().await`.
99    pub fn set_client(&mut self, provider: Arc<dyn llm_trait::LlmProvider>) {
100        self.runner.llm_engine.set_provider(provider);
101    }
102
103    /// Get the model override, if set.
104    pub fn get_model_override(&self) -> Option<String> {
105        self.runner.llm_engine.get_model_override()
106    }
107
108    /// Set a model override for all requests.
109    ///
110    /// This is used by sub-agents to specify their model tier (e.g., "lite").
111    /// The override is applied to all ChatRequests before sending to the provider.
112    pub fn set_model_override(&self, model: Option<String>) {
113        self.runner.llm_engine.set_model_override(model);
114    }
115
116    pub fn tools_mut(&self) -> Arc<tokio::sync::RwLock<crate::tool::ToolRegistry>> {
117        self.runner.tool_engine.tools_arc()
118    }
119
120    pub fn config(&self) -> tokio::sync::RwLockReadGuard<'_, AgentConfig> {
121        self.runner.config.blocking_read()
122    }
123
124    /// 设置 reasoning effort(异步版本)
125    pub async fn set_reasoning_effort(&self, effort: crate::llm::ReasoningEffort) {
126        let mut config = self.runner.config.write().await;
127        let mut reasoning = config.reasoning.take().unwrap_or_default();
128        reasoning.effort = Some(effort);
129        config.reasoning = Some(reasoning);
130    }
131
132    /// 设置 reasoning effort(同步版本,只在同步上下文中使用)
133    pub fn set_reasoning_effort_sync(&self, effort: crate::llm::ReasoningEffort) {
134        let mut config = self.runner.config.blocking_write();
135        let mut reasoning = config.reasoning.take().unwrap_or_default();
136        reasoning.effort = Some(effort);
137        config.reasoning = Some(reasoning);
138    }
139
140    pub fn approval_handler(&self) -> Option<&Arc<dyn ApprovalHandler>> {
141        self.runner.tool_engine.approval_handler()
142    }
143
144    pub fn tool_policy(&self) -> Option<&Arc<dyn ToolPolicy>> {
145        self.runner.tool_engine.tool_policy()
146    }
147
148    pub async fn cached_approval(&self, session_id: &SessionId, action_key: &str) -> bool {
149        self.runner
150            .session_manager
151            .cached_approval(session_id, action_key)
152            .await
153    }
154
155    pub async fn cache_approval(&self, session_id: &SessionId, action_key: String) {
156        self.runner
157            .session_manager
158            .cache_approval(session_id, action_key)
159            .await
160    }
161
162    pub async fn save_checkpoint(
163        &self,
164        session_id: &SessionId,
165        checkpoint: CheckpointData,
166    ) -> AgentResult<()> {
167        self.emit_event(RuntimeEvent::Checkpoint {
168            session_id: session_id.clone(),
169            checkpoint,
170            agent_id: None,
171            trace_id: None,
172        });
173        Ok(())
174    }
175
176    pub async fn load_checkpoint(
177        &self,
178        _session_id: &SessionId,
179        _checkpoint: &CheckpointData,
180    ) -> AgentResult<Option<CheckpointData>> {
181        Ok(None)
182    }
183
184    pub async fn run<F>(&self, session_id: SessionId, on_event: F) -> AgentResult<RunOutcome>
185    where
186        F: FnMut(RuntimeEvent) -> AgentResult<()> + Send + 'static,
187    {
188        self.runner.run(session_id, on_event).await
189    }
190
191    pub async fn run_turn<F>(
192        &self,
193        session_id: SessionId,
194        user_input: &str,
195        on_event: F,
196    ) -> AgentResult<RunOutcome>
197    where
198        F: FnMut(RuntimeEvent) -> AgentResult<()> + Send + 'static,
199    {
200        self.runner.run_turn(session_id, user_input, on_event).await
201    }
202
203    pub async fn run_turn_collect(
204        &self,
205        session_id: SessionId,
206        user_input: &str,
207    ) -> AgentResult<(Vec<RuntimeEvent>, RunOutcome)> {
208        self.runner.run_turn_collect(session_id, user_input).await
209    }
210
211    pub async fn add_user_message(
212        &self,
213        session_id: &SessionId,
214        text: impl Into<String>,
215    ) -> AgentResult<()> {
216        let text = text.into();
217        self.with_session_mut(session_id, |session| {
218            session.push_message(MessageRole::User, &text);
219        })
220        .await
221    }
222
223    pub async fn add_system_message(
224        &self,
225        session_id: &SessionId,
226        text: impl Into<String>,
227    ) -> AgentResult<()> {
228        let text = text.into();
229        self.with_session_mut(session_id, |session| {
230            session.push_message(MessageRole::System, &text);
231        })
232        .await
233    }
234
235    pub async fn add_tool_result(
236        &self,
237        session_id: &SessionId,
238        tool_call_id: &str,
239        summary: impl Into<String>,
240    ) -> AgentResult<()> {
241        let summary = summary.into();
242        self.with_session_mut(session_id, |session| {
243            session.push_tool_result(tool_call_id, summary.clone());
244        })
245        .await
246    }
247
248    pub async fn get_messages(
249        &self,
250        session_id: &SessionId,
251    ) -> AgentResult<Vec<crate::types::ChatMessage>> {
252        let session = self.session_or_err(session_id).await?;
253        Ok(session.chat_messages().to_vec())
254    }
255
256    /// Replace the chat messages for a session — only for persistence restore.
257    /// Validates message sequence before applying.
258    ///
259    /// 仅供持久化恢复使用。
260    pub async fn set_messages(
261        &self,
262        session_id: &SessionId,
263        messages: Vec<crate::types::ChatMessage>,
264    ) -> AgentResult<()> {
265        self.with_session_mut(session_id, |session| session.set_chat_messages(messages))
266            .await?
267            .map_err(AgentError::internal)
268    }
269
270    pub async fn validate_session(&self, session_id: &SessionId) -> AgentResult<()> {
271        if self
272            .runner
273            .session_manager
274            .session(session_id)
275            .await
276            .is_none()
277        {
278            return Err(AgentError::session_not_found(session_id.id));
279        }
280        Ok(())
281    }
282
283    pub fn session_store(&self) -> Arc<dyn SessionStore> {
284        self.runner.session_manager.session_store().clone()
285    }
286
287    // ── Observability hook ──
288
289    /// Register a turn-end callback. The callback receives a [`TurnContext`]
290    /// with raw data about the completed turn iteration. Consumers (e.g.
291    /// phi-telemetry) use this to build their own metrics without agent-base
292    /// knowing anything about metrics.
293    pub fn on_turn_end<F>(&self, f: F)
294    where
295        F: Fn(&TurnContext) + Send + Sync + 'static,
296    {
297        self.runner
298            .turn_end_callbacks
299            .write()
300            .unwrap()
301            .push(Arc::new(f));
302    }
303
304    // --- Cancellation support ---
305
306    /// Cancel the currently executing run_turn / run.
307    /// No-op if there is no current execution.
308    pub fn cancel(&self) {
309        self.runner.cancel();
310    }
311
312    /// Reset the cancel token (called automatically before each run_turn)
313    pub fn reset_cancel(&self) {
314        self.runner.reset_cancel();
315    }
316
317    /// Get a clone of the cancel token
318    pub fn cancel_token(&self) -> CancellationToken {
319        self.runner.cancel_token()
320    }
321
322    /// Check if cancellation has been requested
323    pub fn is_cancelled(&self) -> bool {
324        self.runner.is_cancelled()
325    }
326
327    // ── Message Queue (P2) ──
328
329    /// Push a steering message — will be processed at the start of the next turn
330    /// in the current `run_managed()` loop.
331    pub fn steer(&self, message: String) {
332        self.runner.message_queue.steer(message);
333    }
334
335    /// Push a follow-up message — will be processed after the inner turn loop
336    /// stops naturally (no tool calls or max turns).
337    pub fn follow_up(&self, message: String) {
338        self.runner.message_queue.follow_up(message);
339    }
340
341    /// Run the agent in managed mode with message queue support.
342    ///
343    /// This wraps `run_turn()` (or `run()`) in an outer loop: after the inner
344    /// turn loop completes, any follow-up messages are drained and a new inner
345    /// loop is started. Steering messages are drained automatically at each
346    /// iteration of the inner turn loop.
347    ///
348    /// The `on_event` callback receives all events from every inner run.
349    pub async fn run_managed<F>(
350        &self,
351        session_id: SessionId,
352        user_input: &str,
353        on_event: F,
354    ) -> AgentResult<RunOutcome>
355    where
356        F: FnMut(RuntimeEvent) -> AgentResult<()> + Send + 'static,
357    {
358        self.runner
359            .run_managed(session_id, user_input, on_event)
360            .await
361    }
362
363    /// Set the drain mode for the message queues.
364    pub fn set_queue_mode(&self, mode: crate::engine::runtime::message_queue::QueueMode) {
365        self.runner.message_queue.set_mode(mode);
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372    use crate::llm::ReasoningEffort;
373    use crate::types::{ChatMessage, RuntimeEvent, SessionId};
374    use async_trait::async_trait;
375    use llm_trait::{Capabilities, ChatRequest, ChatResponse, ChatStream, LlmError, ProviderInfo};
376
377    struct StubProvider;
378
379    #[async_trait]
380    impl llm_trait::LlmProvider for StubProvider {
381        async fn stream(&self, _request: ChatRequest) -> Result<ChatStream, LlmError> {
382            Ok(ChatStream::new(Box::pin(futures_util::stream::empty())))
383        }
384
385        async fn chat(&self, _request: ChatRequest) -> Result<ChatResponse, LlmError> {
386            Ok(ChatResponse {
387                content: String::new(),
388                reasoning_content: None,
389                tool_calls: vec![],
390                usage: Default::default(),
391                finish_reason: llm_trait::FinishReason::Stop,
392                raw: None,
393                thinking_signature: None,
394            })
395        }
396
397        fn capabilities(&self) -> Capabilities {
398            Capabilities::default()
399        }
400
401        fn info(&self) -> ProviderInfo {
402            ProviderInfo {
403                name: "stub".to_string(),
404                model: "stub".to_string(),
405                version: None,
406            }
407        }
408    }
409
410    fn runtime() -> AgentRuntime {
411        crate::engine::AgentBuilder::new(Arc::new(StubProvider))
412            .build()
413            .unwrap()
414    }
415
416    #[tokio::test]
417    async fn create_session_and_lookup() {
418        let rt = runtime();
419        let id = rt.create_session().await;
420        assert_eq!(id.id, 1);
421        assert!(rt.session(&id).await.is_some());
422        assert!(rt.session_or_err(&id).await.is_ok());
423    }
424
425    #[tokio::test]
426    async fn add_messages_and_get() {
427        let rt = runtime();
428        let id = rt.create_session().await;
429        rt.add_system_message(&id, "sys").await.unwrap();
430        rt.add_user_message(&id, "hello").await.unwrap();
431        let msgs = rt.get_messages(&id).await.unwrap();
432        assert_eq!(msgs.len(), 2);
433        assert!(matches!(msgs[0], ChatMessage::System { .. }));
434        assert!(matches!(msgs[1], ChatMessage::User { .. }));
435    }
436
437    #[tokio::test]
438    async fn add_tool_result_appends_tool_message() {
439        let rt = runtime();
440        let id = rt.create_session().await;
441        rt.add_tool_result(&id, "call_1", "done").await.unwrap();
442        let msgs = rt.get_messages(&id).await.unwrap();
443        assert_eq!(msgs.len(), 1);
444        assert!(matches!(msgs[0], ChatMessage::Tool { .. }));
445    }
446
447    #[tokio::test]
448    async fn set_messages_replaces_history() {
449        let rt = runtime();
450        let id = rt.create_session().await;
451        rt.add_user_message(&id, "old").await.unwrap();
452        rt.set_messages(
453            &id,
454            vec![ChatMessage::system("sys"), ChatMessage::user("new")],
455        )
456        .await
457        .unwrap();
458        let msgs = rt.get_messages(&id).await.unwrap();
459        assert_eq!(msgs.len(), 2);
460    }
461
462    #[tokio::test]
463    async fn validate_session_errors_for_unknown() {
464        let rt = runtime();
465        let id = rt.create_session().await;
466        assert!(rt.validate_session(&id).await.is_ok());
467        let err = rt.validate_session(&SessionId::new(999)).await.unwrap_err();
468        assert!(matches!(err, AgentError::SessionNotFound(_)));
469    }
470
471    #[test]
472    fn config_and_set_reasoning_effort() {
473        let rt = runtime();
474        assert!(rt.config().system_prompt.is_none());
475
476        rt.set_reasoning_effort_sync(ReasoningEffort::High);
477        let cfg = rt.config();
478        let effort = cfg.reasoning.as_ref().and_then(|r| r.effort.as_ref());
479        assert!(matches!(effort, Some(ReasoningEffort::High)));
480    }
481
482    #[tokio::test]
483    async fn session_store_is_available() {
484        let rt = runtime();
485        assert!(rt.session_store().list().await.unwrap().is_empty());
486    }
487
488    #[tokio::test]
489    async fn emit_and_subscribe_event() {
490        let rt = runtime();
491        let mut rx = rt.subscribe_runtime_events();
492        rt.emit_event(RuntimeEvent::TextDelta {
493            session_id: SessionId::new(1),
494            text: "hi".into(),
495            agent_id: None,
496            trace_id: None,
497        });
498        let ev = rx.recv().await.unwrap();
499        assert!(matches!(ev, RuntimeEvent::TextDelta { .. }));
500    }
501
502    #[tokio::test]
503    async fn cancel_reset_and_is_cancelled() {
504        let rt = runtime();
505        assert!(!rt.is_cancelled());
506        rt.cancel();
507        assert!(rt.is_cancelled());
508        rt.reset_cancel();
509        assert!(!rt.is_cancelled());
510    }
511}