1use std::sync::Arc;
9use tokio::sync::mpsc;
10use tokio::sync::Mutex;
11
12use crate::agent::{Agent, AgentOutcome, StepEvent};
13use crate::error::Result;
14use crate::tools::BackgroundJobManager;
15
16pub struct AgentRunner {
42 agent: Agent,
43 total_turns: usize,
44 bg_manager: Option<Arc<Mutex<BackgroundJobManager>>>,
47}
48
49impl AgentRunner {
50 pub fn new(agent: Agent) -> Self {
52 Self {
53 agent,
54 total_turns: 0,
55 bg_manager: None,
56 }
57 }
58
59 pub fn with_bg_manager(agent: Agent, bg_manager: Arc<Mutex<BackgroundJobManager>>) -> Self {
64 Self {
65 agent,
66 total_turns: 0,
67 bg_manager: Some(bg_manager),
68 }
69 }
70
71 pub async fn turn(
79 &mut self,
80 goal: impl Into<String>,
81 events: Option<mpsc::UnboundedSender<StepEvent>>,
82 ) -> Result<AgentOutcome> {
83 self.agent.set_events(events);
84
85 let outcome = self.agent.run(goal).await?;
86
87 self.agent.set_transcript(outcome.transcript.clone());
89 self.agent.set_events(None);
90 self.total_turns += 1;
91
92 Ok(outcome)
93 }
94
95 pub fn clear(&mut self) {
100 self.agent.set_transcript(Vec::new());
101 self.total_turns = 0;
102 if let Some(ref mgr) = self.bg_manager {
103 if let Ok(mut mgr) = mgr.try_lock() {
105 mgr.clear();
106 }
107 }
108 }
109
110 pub fn turns(&self) -> usize {
112 self.total_turns
113 }
114
115 pub fn agent(&self) -> &Agent {
117 &self.agent
118 }
119
120 pub fn agent_mut(&mut self) -> &mut Agent {
122 &mut self.agent
123 }
124
125 pub async fn run_loop(
133 &mut self,
134 initial_goal: impl Into<String>,
135 wakeup_slot: &crate::tools::WakeupSlot,
136 events: Option<mpsc::UnboundedSender<StepEvent>>,
137 ) -> Result<Vec<AgentOutcome>> {
138 let mut outcomes = Vec::new();
139 let mut next_goal = initial_goal.into();
140
141 loop {
142 let outcome = self.turn(&next_goal, events.clone()).await?;
143 outcomes.push(outcome);
144
145 let wakeup = wakeup_slot.lock().ok().and_then(|mut slot| slot.take());
147
148 match wakeup {
149 Some(req) => {
150 tokio::time::sleep(req.delay).await;
151 next_goal = req.prompt;
152 }
153 None => break, }
155 }
156 Ok(outcomes)
157 }
158
159 pub async fn run_event_loop(
172 &mut self,
173 initial_goal: impl Into<String>,
174 wakeup_slot: &crate::tools::WakeupSlot,
175 events: Option<mpsc::UnboundedSender<StepEvent>>,
176 ) -> Result<Vec<AgentOutcome>> {
177 let mut outcomes = Vec::new();
178 let mut next_goal = initial_goal.into();
179
180 loop {
181 let outcome = self.turn(&next_goal, events.clone()).await?;
182 outcomes.push(outcome);
183
184 let wakeup = wakeup_slot
186 .lock()
187 .ok()
188 .and_then(|mut slot| slot.take());
189 if let Some(req) = wakeup {
190 tokio::time::sleep(req.delay).await;
191 next_goal = req.prompt;
192 continue;
193 }
194
195 if let Some(ref mgr) = self.bg_manager {
197 if let Ok(mut mgr) = mgr.try_lock() {
198 if let Some((id, output)) = mgr.take_completed() {
199 next_goal = format!(
200 "Background job '{}' completed:\n{}",
201 id, output
202 );
203 continue;
204 }
205 }
206 }
207
208 break;
210 }
211 Ok(outcomes)
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use std::sync::Arc;
218
219 use tokio::sync::mpsc;
220
221 use crate::agent::StepEvent;
222 use crate::llm::{Completion, MockProvider};
223 use crate::message::{Message, Role};
224 use crate::tools::ToolRegistry;
225 use crate::Agent;
226
227 use super::*;
228
229 fn make_agent(script: Vec<Completion>) -> Agent {
230 let provider = Arc::new(MockProvider::new(script));
231 Agent::builder()
232 .llm(provider)
233 .tools(ToolRegistry::default())
234 .system_prompt("You are a helpful assistant.")
235 .max_steps(5)
236 .build()
237 .unwrap()
238 }
239
240 fn user_content(msg: &Message) -> Option<&str> {
241 if msg.role == Role::User {
242 Some(msg.content.as_str())
243 } else {
244 None
245 }
246 }
247
248 #[tokio::test]
249 async fn preserves_transcript_across_turns() {
250 let script = vec![
251 Completion {
252 content: "First response".into(),
253 tool_calls: vec![],
254 finish_reason: Some("stop".to_string()),
255 usage: None,
256 reasoning_content: None,
257 },
258 Completion {
259 content: "Second response".into(),
260 tool_calls: vec![],
261 finish_reason: Some("stop".to_string()),
262 usage: None,
263 reasoning_content: None,
264 },
265 ];
266 let agent = make_agent(script);
267 let mut runner = AgentRunner::new(agent);
268
269 let outcome1 = runner.turn("First goal", None).await.unwrap();
270 assert_eq!(runner.turns(), 1);
271 assert!(outcome1
272 .transcript
273 .iter()
274 .any(|m| { m.role == Role::User && m.content == "First goal" }));
275
276 let outcome2 = runner.turn("Second goal", None).await.unwrap();
277 assert_eq!(runner.turns(), 2);
278
279 let texts: Vec<&str> = outcome2
281 .transcript
282 .iter()
283 .filter_map(user_content)
284 .collect();
285 assert!(
286 texts.contains(&"First goal"),
287 "first turn goal should be in transcript: {texts:?}"
288 );
289 assert!(
290 texts.contains(&"Second goal"),
291 "second turn goal should be in transcript: {texts:?}"
292 );
293 }
294
295 #[tokio::test]
296 async fn clear_resets_transcript() {
297 let script = vec![
299 Completion {
300 content: "Response".into(),
301 tool_calls: vec![],
302 finish_reason: Some("stop".to_string()),
303 usage: None,
304 reasoning_content: None,
305 },
306 Completion {
307 content: "After clear".into(),
308 tool_calls: vec![],
309 finish_reason: Some("stop".to_string()),
310 usage: None,
311 reasoning_content: None,
312 },
313 ];
314 let agent = make_agent(script);
315 let mut runner = AgentRunner::new(agent);
316
317 runner.turn("Goal", None).await.unwrap();
318 assert_eq!(runner.turns(), 1);
319
320 runner.clear();
321 assert_eq!(runner.turns(), 0);
322
323 let agent = runner.agent();
325 let _ = agent;
328 let outcome = runner.turn("New goal", None).await.unwrap();
329 let user_msgs: Vec<&str> = outcome.transcript.iter().filter_map(user_content).collect();
330 assert_eq!(user_msgs, vec!["New goal"]);
331 }
332
333 #[tokio::test]
334 async fn turns_increments() {
335 let script = vec![
336 Completion {
337 content: "One".into(),
338 tool_calls: vec![],
339 finish_reason: Some("stop".to_string()),
340 usage: None,
341 reasoning_content: None,
342 },
343 Completion {
344 content: "Two".into(),
345 tool_calls: vec![],
346 finish_reason: Some("stop".to_string()),
347 usage: None,
348 reasoning_content: None,
349 },
350 Completion {
351 content: "Three".into(),
352 tool_calls: vec![],
353 finish_reason: Some("stop".to_string()),
354 usage: None,
355 reasoning_content: None,
356 },
357 ];
358 let agent = make_agent(script);
359 let mut runner = AgentRunner::new(agent);
360
361 assert_eq!(runner.turns(), 0);
362 runner.turn("A", None).await.unwrap();
363 assert_eq!(runner.turns(), 1);
364 runner.turn("B", None).await.unwrap();
365 assert_eq!(runner.turns(), 2);
366 runner.turn("C", None).await.unwrap();
367 assert_eq!(runner.turns(), 3);
368 }
369
370 #[tokio::test]
371 async fn events_forwarded_when_provided() {
372 let script = vec![Completion {
373 content: "Hello world".into(),
374 tool_calls: vec![],
375 finish_reason: Some("stop".to_string()),
376 usage: None,
377 reasoning_content: None,
378 }];
379 let agent = make_agent(script);
380 let mut runner = AgentRunner::new(agent);
381
382 let (tx, mut rx) = mpsc::unbounded_channel();
383 runner.turn("Goal", Some(tx)).await.unwrap();
384
385 let events: Vec<StepEvent> = {
387 let mut v = Vec::new();
388 while let Ok(ev) = rx.try_recv() {
389 v.push(ev);
390 }
391 v
392 };
393 assert!(!events.is_empty(), "should have received events");
394 assert!(
395 events
396 .iter()
397 .any(|e| matches!(e, StepEvent::Finished { .. })),
398 "should have a Finished event"
399 );
400 }
401
402 #[tokio::test]
403 async fn no_events_when_none_passed() {
404 let script = vec![Completion {
405 content: "Silent".into(),
406 tool_calls: vec![],
407 finish_reason: Some("stop".to_string()),
408 usage: None,
409 reasoning_content: None,
410 }];
411 let agent = make_agent(script);
412 let mut runner = AgentRunner::new(agent);
413
414 runner.turn("Goal", None).await.unwrap();
415
416 assert_eq!(runner.turns(), 1);
421 }
422
423 #[tokio::test]
424 async fn agent_accessors_work() {
425 let script = vec![Completion {
426 content: "Test".into(),
427 tool_calls: vec![],
428 finish_reason: Some("stop".to_string()),
429 usage: None,
430 reasoning_content: None,
431 }];
432 let agent = make_agent(script);
433 let mut runner = AgentRunner::new(agent);
434
435 let _agent_ref = runner.agent();
437 let _agent_mut = runner.agent_mut();
439 }
440
441 #[tokio::test]
442 async fn run_event_loop_triggers_on_wakeup() {
443 use std::sync::Mutex as StdMutex;
444 use crate::tools::WakeupSlot;
445
446 let script = vec![
447 Completion {
448 content: "Turn 1".into(),
449 tool_calls: vec![],
450 finish_reason: Some("stop".to_string()),
451 usage: None,
452 reasoning_content: None,
453 },
454 Completion {
455 content: "Turn 2".into(),
456 tool_calls: vec![],
457 finish_reason: Some("stop".to_string()),
458 usage: None,
459 reasoning_content: None,
460 },
461 ];
462 let agent = make_agent(script);
463 let mut runner = AgentRunner::new(agent);
464 let wakeup_slot: WakeupSlot = Arc::new(StdMutex::new(None));
465
466 {
468 let mut slot = wakeup_slot.lock().unwrap();
469 *slot = Some(crate::tools::WakeupRequest {
470 delay: std::time::Duration::from_millis(1),
471 reason: "continue".into(),
472 prompt: "Second goal".into(),
473 });
474 }
475
476 let outcomes = runner
477 .run_event_loop("First goal", &wakeup_slot, None)
478 .await
479 .unwrap();
480
481 assert_eq!(outcomes.len(), 2, "should have run 2 turns");
482 assert_eq!(runner.turns(), 2);
483 }
484
485 #[tokio::test]
486 async fn run_event_loop_ends_when_no_wakeup_or_bg() {
487 let script = vec![Completion {
488 content: "Only turn".into(),
489 tool_calls: vec![],
490 finish_reason: Some("stop".to_string()),
491 usage: None,
492 reasoning_content: None,
493 }];
494 let agent = make_agent(script);
495 let mut runner = AgentRunner::new(agent);
496 let wakeup_slot = Arc::new(std::sync::Mutex::new(None));
497
498 let outcomes = runner
499 .run_event_loop("Single goal", &wakeup_slot, None)
500 .await
501 .unwrap();
502
503 assert_eq!(outcomes.len(), 1, "should have run exactly 1 turn");
504 assert_eq!(runner.turns(), 1);
505 }
506
507 #[tokio::test]
508 async fn run_event_loop_triggers_on_bg_completion() {
509 use std::sync::Mutex as StdMutex;
510 use crate::tools::WakeupSlot;
511
512 let script = vec![
513 Completion {
514 content: "Turn 1".into(),
515 tool_calls: vec![],
516 finish_reason: Some("stop".to_string()),
517 usage: None,
518 reasoning_content: None,
519 },
520 Completion {
521 content: "Turn 2".into(),
522 tool_calls: vec![],
523 finish_reason: Some("stop".to_string()),
524 usage: None,
525 reasoning_content: None,
526 },
527 ];
528 let agent = make_agent(script);
529 let bg_manager = Arc::new(Mutex::new(BackgroundJobManager::new()));
530 let mut runner = AgentRunner::with_bg_manager(agent, bg_manager.clone());
531 let wakeup_slot: WakeupSlot = Arc::new(StdMutex::new(None));
532
533 {
535 let mut mgr = bg_manager.lock().await;
536 mgr.insert(crate::tools::run_background::Job {
537 state: crate::tools::run_background::JobState::Completed {
538 stdout: "build succeeded".into(),
539 stderr: "".into(),
540 exit_code: 0,
541 },
542 created_at: std::time::Instant::now(),
543 });
544 }
545
546 let outcomes = runner
547 .run_event_loop("First goal", &wakeup_slot, None)
548 .await
549 .unwrap();
550
551 assert_eq!(outcomes.len(), 2, "should have run 2 turns (initial + bg trigger)");
552 assert_eq!(runner.turns(), 2);
553 }
554}