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 get_model_override(&self) -> Option<String> {
105 self.runner.llm_engine.get_model_override()
106 }
107
108 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 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 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 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 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 pub fn cancel(&self) {
309 self.runner.cancel();
310 }
311
312 pub fn reset_cancel(&self) {
314 self.runner.reset_cancel();
315 }
316
317 pub fn cancel_token(&self) -> CancellationToken {
319 self.runner.cancel_token()
320 }
321
322 pub fn is_cancelled(&self) -> bool {
324 self.runner.is_cancelled()
325 }
326
327 pub fn steer(&self, message: String) {
332 self.runner.message_queue.steer(message);
333 }
334
335 pub fn follow_up(&self, message: String) {
338 self.runner.message_queue.follow_up(message);
339 }
340
341 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 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}