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_loop;
21mod session_manager;
22mod tool_engine;
23
24pub(super) const DEFAULT_MAX_TURNS: u32 = 50;
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 client(&self) -> Arc<dyn crate::llm::StreamClient> {
94        self.runner.llm_engine.get_client()
95    }
96
97    /// Replace the LLM client at runtime (e.g., model switch).
98    /// Requires `&mut self` — obtain via `runtime.lock().await`.
99    pub fn set_client(&mut self, client: Arc<dyn crate::llm::StreamClient>) {
100        self.runner.llm_engine.set_client(client);
101    }
102
103    pub fn tools_mut(&self) -> Arc<tokio::sync::RwLock<crate::tool::ToolRegistry>> {
104        self.runner.tool_engine.tools_arc()
105    }
106
107    pub fn config(&self) -> tokio::sync::RwLockReadGuard<'_, AgentConfig> {
108        self.runner.config.blocking_read()
109    }
110
111    /// 设置 reasoning effort(异步版本)
112    pub async fn set_reasoning_effort(&self, effort: crate::llm::ReasoningEffort) {
113        let mut config = self.runner.config.write().await;
114        let mut reasoning = config.reasoning.take().unwrap_or_default();
115        reasoning.effort = Some(effort);
116        config.reasoning = Some(reasoning);
117    }
118
119    /// 设置 reasoning effort(同步版本,只在同步上下文中使用)
120    pub fn set_reasoning_effort_sync(&self, effort: crate::llm::ReasoningEffort) {
121        let mut config = self.runner.config.blocking_write();
122        let mut reasoning = config.reasoning.take().unwrap_or_default();
123        reasoning.effort = Some(effort);
124        config.reasoning = Some(reasoning);
125    }
126
127    pub fn approval_handler(&self) -> Option<&Arc<dyn ApprovalHandler>> {
128        self.runner.tool_engine.approval_handler()
129    }
130
131    pub fn tool_policy(&self) -> Option<&Arc<dyn ToolPolicy>> {
132        self.runner.tool_engine.tool_policy()
133    }
134
135    pub async fn cached_approval(&self, session_id: &SessionId, action_key: &str) -> bool {
136        self.runner
137            .session_manager
138            .cached_approval(session_id, action_key)
139            .await
140    }
141
142    pub async fn cache_approval(&self, session_id: &SessionId, action_key: String) {
143        self.runner
144            .session_manager
145            .cache_approval(session_id, action_key)
146            .await
147    }
148
149    pub async fn save_checkpoint(
150        &self,
151        session_id: &SessionId,
152        checkpoint: CheckpointData,
153    ) -> AgentResult<()> {
154        self.emit_event(RuntimeEvent::Checkpoint {
155            session_id: session_id.clone(),
156            checkpoint,
157            agent_id: None,
158            trace_id: None,
159        });
160        Ok(())
161    }
162
163    pub async fn load_checkpoint(
164        &self,
165        _session_id: &SessionId,
166        _checkpoint: &CheckpointData,
167    ) -> AgentResult<Option<CheckpointData>> {
168        Ok(None)
169    }
170
171    pub async fn run<F>(&self, session_id: SessionId, on_event: F) -> AgentResult<RunOutcome>
172    where
173        F: FnMut(RuntimeEvent) -> AgentResult<()> + Send + 'static,
174    {
175        self.runner.run(session_id, on_event).await
176    }
177
178    pub async fn run_turn<F>(
179        &self,
180        session_id: SessionId,
181        user_input: &str,
182        on_event: F,
183    ) -> AgentResult<RunOutcome>
184    where
185        F: FnMut(RuntimeEvent) -> AgentResult<()> + Send + 'static,
186    {
187        self.runner.run_turn(session_id, user_input, on_event).await
188    }
189
190    pub async fn run_turn_collect(
191        &self,
192        session_id: SessionId,
193        user_input: &str,
194    ) -> AgentResult<(Vec<RuntimeEvent>, RunOutcome)> {
195        self.runner.run_turn_collect(session_id, user_input).await
196    }
197
198    pub async fn add_user_message(
199        &self,
200        session_id: &SessionId,
201        text: impl Into<String>,
202    ) -> AgentResult<()> {
203        let text = text.into();
204        self.with_session_mut(session_id, |session| {
205            session.push_message(MessageRole::User, &text);
206        })
207        .await
208    }
209
210    pub async fn add_system_message(
211        &self,
212        session_id: &SessionId,
213        text: impl Into<String>,
214    ) -> AgentResult<()> {
215        let text = text.into();
216        self.with_session_mut(session_id, |session| {
217            session.push_message(MessageRole::System, &text);
218        })
219        .await
220    }
221
222    pub async fn add_tool_result(
223        &self,
224        session_id: &SessionId,
225        tool_call_id: &str,
226        summary: impl Into<String>,
227    ) -> AgentResult<()> {
228        let summary = summary.into();
229        self.with_session_mut(session_id, |session| {
230            session.push_tool_result(tool_call_id, summary.clone());
231        })
232        .await
233    }
234
235    pub async fn get_messages(
236        &self,
237        session_id: &SessionId,
238    ) -> AgentResult<Vec<crate::types::ChatMessage>> {
239        let session = self.session_or_err(session_id).await?;
240        Ok(session.chat_messages().to_vec())
241    }
242
243    /// Replace the chat messages for a session — only for persistence restore.
244    /// Validates message sequence before applying.
245    ///
246    /// 仅供持久化恢复使用。
247    pub async fn set_messages(
248        &self,
249        session_id: &SessionId,
250        messages: Vec<crate::types::ChatMessage>,
251    ) -> AgentResult<()> {
252        self.with_session_mut(session_id, |session| session.set_chat_messages(messages))
253            .await?
254            .map_err(AgentError::internal)
255    }
256
257    pub async fn validate_session(&self, session_id: &SessionId) -> AgentResult<()> {
258        if self
259            .runner
260            .session_manager
261            .session(session_id)
262            .await
263            .is_none()
264        {
265            return Err(AgentError::session_not_found(session_id.id));
266        }
267        Ok(())
268    }
269
270    pub fn session_store(&self) -> Arc<dyn SessionStore> {
271        self.runner.session_manager.session_store().clone()
272    }
273
274    // ── Observability hook ──
275
276    /// Register a turn-end callback. The callback receives a [`TurnContext`]
277    /// with raw data about the completed turn iteration. Consumers (e.g.
278    /// phi-telemetry) use this to build their own metrics without agent-base
279    /// knowing anything about metrics.
280    pub fn on_turn_end<F>(&self, f: F)
281    where
282        F: Fn(&TurnContext) + Send + Sync + 'static,
283    {
284        self.runner
285            .turn_end_callbacks
286            .write()
287            .unwrap()
288            .push(Arc::new(f));
289    }
290
291    // --- Cancellation support ---
292
293    /// Cancel the currently executing run_turn / run.
294    /// No-op if there is no current execution.
295    pub fn cancel(&self) {
296        self.runner.cancel();
297    }
298
299    /// Reset the cancel token (called automatically before each run_turn)
300    pub fn reset_cancel(&self) {
301        self.runner.reset_cancel();
302    }
303
304    /// Get a clone of the cancel token
305    pub fn cancel_token(&self) -> CancellationToken {
306        self.runner.cancel_token()
307    }
308
309    /// Check if cancellation has been requested
310    pub fn is_cancelled(&self) -> bool {
311        self.runner.is_cancelled()
312    }
313
314    // ── Message Queue (P2) ──
315
316    /// Push a steering message — will be processed at the start of the next turn
317    /// in the current `run_managed()` loop.
318    pub fn steer(&self, message: String) {
319        self.runner.message_queue.steer(message);
320    }
321
322    /// Push a follow-up message — will be processed after the inner turn loop
323    /// stops naturally (no tool calls or max turns).
324    pub fn follow_up(&self, message: String) {
325        self.runner.message_queue.follow_up(message);
326    }
327
328    /// Run the agent in managed mode with message queue support.
329    ///
330    /// This wraps `run_turn()` (or `run()`) in an outer loop: after the inner
331    /// turn loop completes, any follow-up messages are drained and a new inner
332    /// loop is started. Steering messages are drained automatically at each
333    /// iteration of the inner turn loop.
334    ///
335    /// The `on_event` callback receives all events from every inner run.
336    pub async fn run_managed<F>(
337        &self,
338        session_id: SessionId,
339        user_input: &str,
340        on_event: F,
341    ) -> AgentResult<RunOutcome>
342    where
343        F: FnMut(RuntimeEvent) -> AgentResult<()> + Send + 'static,
344    {
345        self.runner
346            .run_managed(session_id, user_input, on_event)
347            .await
348    }
349
350    /// Set the drain mode for the message queues.
351    pub fn set_queue_mode(&self, mode: crate::engine::runtime::message_queue::QueueMode) {
352        self.runner.message_queue.set_mode(mode);
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use crate::llm::{
360        LlmCapabilities, ReasoningConfig, ReasoningEffort, StreamChunk, StreamClient,
361    };
362    use crate::types::{ChatMessage, ResponseFormat};
363    use async_trait::async_trait;
364    use futures_core::Stream;
365    use serde_json::Value;
366    use std::pin::Pin;
367
368    struct StubClient;
369
370    #[async_trait]
371    impl StreamClient for StubClient {
372        async fn stream(
373            &self,
374            _messages: &[ChatMessage],
375            _tools: &[Value],
376            _reasoning: Option<&ReasoningConfig>,
377            _response_format: Option<&ResponseFormat>,
378        ) -> AgentResult<Pin<Box<dyn Stream<Item = AgentResult<StreamChunk>> + Send>>> {
379            Ok(Box::pin(futures_util::stream::empty()))
380        }
381
382        fn capabilities(&self) -> LlmCapabilities {
383            LlmCapabilities::default()
384        }
385    }
386
387    fn runtime() -> AgentRuntime {
388        crate::engine::AgentBuilder::new(Arc::new(StubClient))
389            .build()
390            .unwrap()
391    }
392
393    #[tokio::test]
394    async fn create_session_and_lookup() {
395        let rt = runtime();
396        let id = rt.create_session().await;
397        assert_eq!(id.id, 1);
398        assert!(rt.session(&id).await.is_some());
399        assert!(rt.session_or_err(&id).await.is_ok());
400    }
401
402    #[tokio::test]
403    async fn add_messages_and_get() {
404        let rt = runtime();
405        let id = rt.create_session().await;
406        rt.add_system_message(&id, "sys").await.unwrap();
407        rt.add_user_message(&id, "hello").await.unwrap();
408        let msgs = rt.get_messages(&id).await.unwrap();
409        assert_eq!(msgs.len(), 2);
410        assert!(matches!(msgs[0], ChatMessage::System { .. }));
411        assert!(matches!(msgs[1], ChatMessage::User { .. }));
412    }
413
414    #[tokio::test]
415    async fn add_tool_result_appends_tool_message() {
416        let rt = runtime();
417        let id = rt.create_session().await;
418        rt.add_tool_result(&id, "call_1", "done").await.unwrap();
419        let msgs = rt.get_messages(&id).await.unwrap();
420        assert_eq!(msgs.len(), 1);
421        assert!(matches!(msgs[0], ChatMessage::Tool { .. }));
422    }
423
424    #[tokio::test]
425    async fn set_messages_replaces_history() {
426        let rt = runtime();
427        let id = rt.create_session().await;
428        rt.add_user_message(&id, "old").await.unwrap();
429        rt.set_messages(
430            &id,
431            vec![ChatMessage::system("sys"), ChatMessage::user("new")],
432        )
433        .await
434        .unwrap();
435        let msgs = rt.get_messages(&id).await.unwrap();
436        assert_eq!(msgs.len(), 2);
437    }
438
439    #[tokio::test]
440    async fn validate_session_errors_for_unknown() {
441        let rt = runtime();
442        let id = rt.create_session().await;
443        assert!(rt.validate_session(&id).await.is_ok());
444        let err = rt.validate_session(&SessionId::new(999)).await.unwrap_err();
445        assert!(matches!(err, AgentError::SessionNotFound(_)));
446    }
447
448    #[test]
449    fn config_and_set_reasoning_effort() {
450        let rt = runtime();
451        assert!(rt.config().system_prompt.is_none());
452
453        rt.set_reasoning_effort_sync(ReasoningEffort::High);
454        let cfg = rt.config();
455        let effort = cfg.reasoning.as_ref().and_then(|r| r.effort.as_ref());
456        assert!(matches!(effort, Some(ReasoningEffort::High)));
457    }
458
459    #[tokio::test]
460    async fn session_store_is_available() {
461        let rt = runtime();
462        assert!(rt.session_store().list().await.unwrap().is_empty());
463    }
464
465    #[tokio::test]
466    async fn emit_and_subscribe_event() {
467        let rt = runtime();
468        let mut rx = rt.subscribe_runtime_events();
469        rt.emit_event(RuntimeEvent::TextDelta {
470            session_id: SessionId::new(1),
471            text: "hi".into(),
472            agent_id: None,
473            trace_id: None,
474        });
475        let ev = rx.recv().await.unwrap();
476        assert!(matches!(ev, RuntimeEvent::TextDelta { .. }));
477    }
478
479    #[tokio::test]
480    async fn cancel_reset_and_is_cancelled() {
481        let rt = runtime();
482        assert!(!rt.is_cancelled());
483        rt.cancel();
484        assert!(rt.is_cancelled());
485        rt.reset_cancel();
486        assert!(!rt.is_cancelled());
487    }
488}