agent_base/engine/runtime/
mod.rs1use 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 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 pub fn set_client(&mut self, provider: Arc<dyn llm_trait::LlmProvider>) {
100 self.runner.llm_engine.set_provider(provider);
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 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 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 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 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 pub fn cancel(&self) {
296 self.runner.cancel();
297 }
298
299 pub fn reset_cancel(&self) {
301 self.runner.reset_cancel();
302 }
303
304 pub fn cancel_token(&self) -> CancellationToken {
306 self.runner.cancel_token()
307 }
308
309 pub fn is_cancelled(&self) -> bool {
311 self.runner.is_cancelled()
312 }
313
314 pub fn steer(&self, message: String) {
319 self.runner.message_queue.steer(message);
320 }
321
322 pub fn follow_up(&self, message: String) {
325 self.runner.message_queue.follow_up(message);
326 }
327
328 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 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::ReasoningEffort;
360 use crate::types::{ChatMessage, RuntimeEvent, SessionId};
361 use async_trait::async_trait;
362 use llm_trait::{Capabilities, ChatRequest, ChatResponse, ChatStream, LlmError, ProviderInfo};
363
364 struct StubProvider;
365
366 #[async_trait]
367 impl llm_trait::LlmProvider for StubProvider {
368 async fn stream(&self, _request: ChatRequest) -> Result<ChatStream, LlmError> {
369 Ok(ChatStream::new(Box::pin(futures_util::stream::empty())))
370 }
371
372 async fn chat(&self, _request: ChatRequest) -> Result<ChatResponse, LlmError> {
373 Ok(ChatResponse {
374 content: String::new(),
375 reasoning_content: None,
376 tool_calls: vec![],
377 usage: Default::default(),
378 finish_reason: llm_trait::FinishReason::Stop,
379 raw: None,
380 thinking_signature: None,
381 })
382 }
383
384 fn capabilities(&self) -> Capabilities {
385 Capabilities::default()
386 }
387
388 fn info(&self) -> ProviderInfo {
389 ProviderInfo {
390 name: "stub".to_string(),
391 model: "stub".to_string(),
392 version: None,
393 }
394 }
395 }
396
397 fn runtime() -> AgentRuntime {
398 crate::engine::AgentBuilder::new(Arc::new(StubProvider))
399 .build()
400 .unwrap()
401 }
402
403 #[tokio::test]
404 async fn create_session_and_lookup() {
405 let rt = runtime();
406 let id = rt.create_session().await;
407 assert_eq!(id.id, 1);
408 assert!(rt.session(&id).await.is_some());
409 assert!(rt.session_or_err(&id).await.is_ok());
410 }
411
412 #[tokio::test]
413 async fn add_messages_and_get() {
414 let rt = runtime();
415 let id = rt.create_session().await;
416 rt.add_system_message(&id, "sys").await.unwrap();
417 rt.add_user_message(&id, "hello").await.unwrap();
418 let msgs = rt.get_messages(&id).await.unwrap();
419 assert_eq!(msgs.len(), 2);
420 assert!(matches!(msgs[0], ChatMessage::System { .. }));
421 assert!(matches!(msgs[1], ChatMessage::User { .. }));
422 }
423
424 #[tokio::test]
425 async fn add_tool_result_appends_tool_message() {
426 let rt = runtime();
427 let id = rt.create_session().await;
428 rt.add_tool_result(&id, "call_1", "done").await.unwrap();
429 let msgs = rt.get_messages(&id).await.unwrap();
430 assert_eq!(msgs.len(), 1);
431 assert!(matches!(msgs[0], ChatMessage::Tool { .. }));
432 }
433
434 #[tokio::test]
435 async fn set_messages_replaces_history() {
436 let rt = runtime();
437 let id = rt.create_session().await;
438 rt.add_user_message(&id, "old").await.unwrap();
439 rt.set_messages(
440 &id,
441 vec![ChatMessage::system("sys"), ChatMessage::user("new")],
442 )
443 .await
444 .unwrap();
445 let msgs = rt.get_messages(&id).await.unwrap();
446 assert_eq!(msgs.len(), 2);
447 }
448
449 #[tokio::test]
450 async fn validate_session_errors_for_unknown() {
451 let rt = runtime();
452 let id = rt.create_session().await;
453 assert!(rt.validate_session(&id).await.is_ok());
454 let err = rt.validate_session(&SessionId::new(999)).await.unwrap_err();
455 assert!(matches!(err, AgentError::SessionNotFound(_)));
456 }
457
458 #[test]
459 fn config_and_set_reasoning_effort() {
460 let rt = runtime();
461 assert!(rt.config().system_prompt.is_none());
462
463 rt.set_reasoning_effort_sync(ReasoningEffort::High);
464 let cfg = rt.config();
465 let effort = cfg.reasoning.as_ref().and_then(|r| r.effort.as_ref());
466 assert!(matches!(effort, Some(ReasoningEffort::High)));
467 }
468
469 #[tokio::test]
470 async fn session_store_is_available() {
471 let rt = runtime();
472 assert!(rt.session_store().list().await.unwrap().is_empty());
473 }
474
475 #[tokio::test]
476 async fn emit_and_subscribe_event() {
477 let rt = runtime();
478 let mut rx = rt.subscribe_runtime_events();
479 rt.emit_event(RuntimeEvent::TextDelta {
480 session_id: SessionId::new(1),
481 text: "hi".into(),
482 agent_id: None,
483 trace_id: None,
484 });
485 let ev = rx.recv().await.unwrap();
486 assert!(matches!(ev, RuntimeEvent::TextDelta { .. }));
487 }
488
489 #[tokio::test]
490 async fn cancel_reset_and_is_cancelled() {
491 let rt = runtime();
492 assert!(!rt.is_cancelled());
493 rt.cancel();
494 assert!(rt.is_cancelled());
495 rt.reset_cancel();
496 assert!(!rt.is_cancelled());
497 }
498}