1use crate::agent_loop::{run_agent_loop, run_agent_loop_continue};
18use crate::events::{AgentEmitter, AgentEvent, BroadcastEmitter};
19use crate::hooks::{default_convert_to_llm_fn, AgentLoopConfig, ConvertToLlm};
20use crate::message::AgentMessage;
21use crate::queue::PendingMessageQueue;
22use crate::stream_fn::{get_default_stream_fn, StreamFn};
23use crate::types::{AgentContext, AgentState, QueueMode, ToolExecutionMode};
24
25use rpi_ai::types::{AssistantMessage, UserContent, UserMessage};
26use rpi_ai::Model;
27use std::collections::HashSet;
28use std::sync::{Arc, Mutex};
29use tokio::sync::{broadcast, Notify};
30use tokio_util::sync::CancellationToken;
31
32#[derive(Default)]
36pub struct AgentOptions {
37 pub initial_state: Option<InitialState>,
38 pub convert_to_llm: Option<ConvertToLlm>,
39 pub stream_fn: Option<StreamFn>,
40 pub thinking_level: Option<rpi_ai::types::ThinkingLevel>,
41 pub queue_mode: Option<QueueMode>,
42 pub follow_up_mode: Option<QueueMode>,
43 pub tool_execution: Option<ToolExecutionMode>,
44 pub session_id: Option<String>,
45}
46
47#[derive(Default)]
49pub struct InitialState {
50 pub system_prompt: Option<String>,
51 pub model: Option<Model>,
52 pub thinking_level: Option<rpi_ai::types::ThinkingLevel>,
53 pub tools: Option<Vec<Arc<dyn crate::agent_tool::AgentTool>>>,
54 pub messages: Option<Vec<AgentMessage>>,
55}
56
57struct MutableAgentState {
59 system_prompt: String,
60 model: Model,
61 thinking_level: rpi_ai::types::ThinkingLevel,
62 tools: Vec<Arc<dyn crate::agent_tool::AgentTool>>,
63 messages: Vec<AgentMessage>,
64 is_streaming: bool,
65 streaming_message: Option<Arc<AssistantMessage>>,
68 pending_tool_calls: HashSet<String>,
69 error_message: Option<String>,
70}
71
72impl MutableAgentState {
73 fn snapshot(&self) -> AgentState {
74 AgentState {
75 system_prompt: self.system_prompt.clone(),
76 model: self.model.clone(),
77 thinking_level: self.thinking_level,
78 tools: self.tools.clone(),
79 messages: self.messages.clone(),
80 is_streaming: self.is_streaming,
81 streaming_message: self.streaming_message.clone(),
82 pending_tool_calls: self.pending_tool_calls.clone(),
83 error_message: self.error_message.clone(),
84 }
85 }
86
87 fn reduce(&mut self, event: &AgentEvent) {
89 match event {
90 AgentEvent::MessageStart { message } => {
91 self.streaming_message = message
94 .as_assistant()
95 .map(|assistant| Arc::new(assistant.clone()));
96 }
97 AgentEvent::MessageUpdate { message, .. } => {
98 self.streaming_message = Some(Arc::clone(message));
99 }
100 AgentEvent::MessageEnd { message } => {
101 self.streaming_message = None;
102 self.messages.push(message.clone());
103 }
104 AgentEvent::ToolExecutionStart { tool_call_id, .. } => {
105 self.pending_tool_calls.insert(tool_call_id.clone());
106 }
107 AgentEvent::ToolExecutionEnd { tool_call_id, .. } => {
108 self.pending_tool_calls.remove(tool_call_id);
109 }
110 AgentEvent::TurnEnd { message, .. } => {
111 if let Some(am) = message.as_assistant() {
112 if am.error_message.is_some() {
113 self.error_message = am.error_message.clone();
114 }
115 }
116 }
117 AgentEvent::AgentEnd { .. } => {
118 self.streaming_message = None;
119 }
120 _ => {}
121 }
122 }
123}
124
125struct ActiveRun {
127 abort: CancellationToken,
128 done: Arc<Notify>,
129}
130
131struct StatefulEmitter {
134 state: Arc<Mutex<MutableAgentState>>,
135 broadcast: BroadcastEmitter,
136}
137
138impl AgentEmitter for StatefulEmitter {
139 fn emit(&self, event: AgentEvent) -> futures::future::BoxFuture<'static, ()> {
140 self.state.lock().expect("state lock").reduce(&event);
141 self.broadcast.try_emit(event);
142 Box::pin(async {})
143 }
144 fn try_emit(&self, event: AgentEvent) {
145 self.state.lock().expect("state lock").reduce(&event);
146 self.broadcast.try_emit(event);
147 }
148}
149
150type SharedQueue = Arc<Mutex<PendingMessageQueue>>;
154
155#[derive(Clone)]
158pub struct Agent {
159 inner: Arc<Inner>,
160}
161
162struct Inner {
163 state: Arc<Mutex<MutableAgentState>>,
164 convert_to_llm: ConvertToLlm,
165 stream_fn: StreamFn,
166 steering_queue: SharedQueue,
167 follow_up_queue: SharedQueue,
168 session_id: Option<String>,
169 tool_execution: ToolExecutionMode,
170 event_tx: broadcast::Sender<AgentEvent>,
171 active_run: Mutex<Option<ActiveRun>>,
172}
173
174pub struct AgentBuilder {
176 opts: AgentOptions,
177}
178
179impl AgentBuilder {
180 pub fn new() -> Self {
181 Self {
182 opts: AgentOptions::default(),
183 }
184 }
185
186 pub fn model(mut self, model: Model) -> Self {
187 self.opts
188 .initial_state
189 .get_or_insert_with(InitialState::default)
190 .model = Some(model);
191 self
192 }
193
194 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
195 self.opts
196 .initial_state
197 .get_or_insert_with(InitialState::default)
198 .system_prompt = Some(prompt.into());
199 self
200 }
201
202 pub fn thinking_level(mut self, level: rpi_ai::types::ThinkingLevel) -> Self {
203 self.opts
204 .initial_state
205 .get_or_insert_with(InitialState::default)
206 .thinking_level = Some(level);
207 self
208 }
209
210 pub fn tools(mut self, tools: Vec<Arc<dyn crate::agent_tool::AgentTool>>) -> Self {
211 self.opts
212 .initial_state
213 .get_or_insert_with(InitialState::default)
214 .tools = Some(tools);
215 self
216 }
217
218 pub fn messages(mut self, messages: Vec<AgentMessage>) -> Self {
219 self.opts
220 .initial_state
221 .get_or_insert_with(InitialState::default)
222 .messages = Some(messages);
223 self
224 }
225
226 pub fn stream_fn(mut self, stream_fn: StreamFn) -> Self {
227 self.opts.stream_fn = Some(stream_fn);
228 self
229 }
230
231 pub fn convert_to_llm(mut self, f: ConvertToLlm) -> Self {
232 self.opts.convert_to_llm = Some(f);
233 self
234 }
235
236 pub fn queue_mode(mut self, mode: QueueMode) -> Self {
237 self.opts.queue_mode = Some(mode);
238 self
239 }
240
241 pub fn follow_up_mode(mut self, mode: QueueMode) -> Self {
242 self.opts.follow_up_mode = Some(mode);
243 self
244 }
245
246 pub fn tool_execution(mut self, mode: ToolExecutionMode) -> Self {
247 self.opts.tool_execution = Some(mode);
248 self
249 }
250
251 pub fn session_id(mut self, id: impl Into<String>) -> Self {
252 self.opts.session_id = Some(id.into());
253 self
254 }
255
256 pub fn build(self) -> Result<Agent, crate::AgentError> {
259 let stream_fn = match self.opts.stream_fn {
260 Some(f) => f,
261 None => get_default_stream_fn()?,
262 };
263
264 let initial = self.opts.initial_state.unwrap_or_default();
265 let model = initial.model.unwrap_or_else(default_model);
266 let thinking_level = self
267 .opts
268 .thinking_level
269 .or(initial.thinking_level)
270 .unwrap_or(rpi_ai::types::ThinkingLevel::Off);
271 let convert_to_llm = self
272 .opts
273 .convert_to_llm
274 .unwrap_or_else(default_convert_to_llm_fn);
275
276 let state = MutableAgentState {
277 system_prompt: initial.system_prompt.unwrap_or_default(),
278 model: model.clone(),
279 thinking_level,
280 tools: initial.tools.unwrap_or_default(),
281 messages: initial.messages.unwrap_or_default(),
282 is_streaming: false,
283 streaming_message: None,
284 pending_tool_calls: HashSet::new(),
285 error_message: None,
286 };
287
288 let (event_tx, _) = broadcast::channel(256);
289
290 let queue_mode = self.opts.queue_mode.unwrap_or_default();
291 let follow_up_mode = self.opts.follow_up_mode.unwrap_or_default();
292
293 let inner = Inner {
294 state: Arc::new(Mutex::new(state)),
295 convert_to_llm,
296 stream_fn,
297 steering_queue: Arc::new(Mutex::new(PendingMessageQueue::new(queue_mode))),
298 follow_up_queue: Arc::new(Mutex::new(PendingMessageQueue::new(follow_up_mode))),
299 session_id: self.opts.session_id,
300 tool_execution: self.opts.tool_execution.unwrap_or_default(),
301 event_tx,
302 active_run: Mutex::new(None),
303 };
304
305 Ok(Agent {
306 inner: Arc::new(inner),
307 })
308 }
309}
310
311impl Default for AgentBuilder {
312 fn default() -> Self {
313 Self::new()
314 }
315}
316
317impl Agent {
318 pub fn subscribe(&self) -> broadcast::Receiver<AgentEvent> {
321 self.inner.event_tx.subscribe()
322 }
323
324 pub fn state(&self) -> AgentState {
327 self.inner.state.lock().expect("state lock").snapshot()
328 }
329
330 pub fn steer(&self, message: AgentMessage) {
332 self.inner
333 .steering_queue
334 .lock()
335 .expect("steer lock")
336 .enqueue(message);
337 }
338
339 pub fn follow_up(&self, message: AgentMessage) {
341 self.inner
342 .follow_up_queue
343 .lock()
344 .expect("followup lock")
345 .enqueue(message);
346 }
347
348 pub fn has_queued_messages(&self) -> bool {
350 let s = self.inner.steering_queue.lock().expect("steer lock");
351 if !s.is_empty() {
352 return true;
353 }
354 let f = self.inner.follow_up_queue.lock().expect("followup lock");
355 !f.is_empty()
356 }
357
358 pub fn abort(&self) {
360 if let Some(run) = self.inner.active_run.lock().expect("run lock").as_ref() {
361 run.abort.cancel();
362 }
363 }
364
365 pub async fn wait_for_idle(&self) {
367 let notify = {
368 let guard = self.inner.active_run.lock().expect("run lock");
369 guard.as_ref().map(|r| Arc::clone(&r.done))
370 };
371 if let Some(n) = notify {
372 n.notified().await;
373 }
374 }
375
376 pub async fn prompt(&self, text: impl Into<String>) -> Result<(), crate::AgentError> {
378 let message = AgentMessage::User(UserMessage::new(
379 UserContent::Text(text.into()),
380 crate::clock::now_ms(),
381 ));
382 self.prompt_messages(vec![message]).await
383 }
384
385 pub async fn prompt_message(&self, message: AgentMessage) -> Result<(), crate::AgentError> {
387 self.prompt_messages(vec![message]).await
388 }
389
390 pub async fn prompt_messages(
392 &self,
393 messages: Vec<AgentMessage>,
394 ) -> Result<(), crate::AgentError> {
395 self.start_active_run()?;
396 let run_done = self.current_done();
397 let outcome = self.run_prompt(messages).await;
398 self.finish_run();
399 if let Some(n) = run_done {
400 n.notify_waiters();
401 }
402 outcome
403 }
404
405 pub async fn continue_run(&self) -> Result<(), crate::AgentError> {
408 self.start_active_run()?;
409 let run_done = self.current_done();
410 let outcome = self.run_continue().await;
411 self.finish_run();
412 if let Some(n) = run_done {
413 n.notify_waiters();
414 }
415 outcome
416 }
417
418 pub fn reset(&self) -> Result<(), crate::AgentError> {
420 let mut state = self.inner.state.lock().expect("state lock");
421 if self.inner.active_run.lock().expect("run lock").is_some() {
422 return Err(crate::AgentError::State(
423 "Agent is already processing. Wait for completion before resetting.".into(),
424 ));
425 }
426 state.messages.clear();
427 state.is_streaming = false;
428 state.streaming_message = None;
429 state.pending_tool_calls.clear();
430 state.error_message = None;
431 drop(state);
432 let _ = self
433 .inner
434 .steering_queue
435 .lock()
436 .expect("steer lock")
437 .try_drain();
438 let _ = self
439 .inner
440 .follow_up_queue
441 .lock()
442 .expect("followup lock")
443 .try_drain();
444 Ok(())
445 }
446
447 fn start_active_run(&self) -> Result<(), crate::AgentError> {
450 let mut guard = self.inner.active_run.lock().expect("run lock");
451 if guard.is_some() {
452 return Err(crate::AgentError::State(
453 "Agent is already processing a prompt. Use steer() or followUp() to queue messages, or wait for completion.".into(),
454 ));
455 }
456 let abort = CancellationToken::new();
457 let done = Arc::new(Notify::new());
458 *guard = Some(ActiveRun {
459 abort: abort.clone(),
460 done: Arc::clone(&done),
461 });
462
463 let mut state = self.inner.state.lock().expect("state lock");
464 state.is_streaming = true;
465 state.streaming_message = None;
466 state.error_message = None;
467 drop(state);
468 Ok(())
469 }
470
471 fn finish_run(&self) {
472 {
473 let mut state = self.inner.state.lock().expect("state lock");
474 state.is_streaming = false;
475 state.streaming_message = None;
476 state.pending_tool_calls.clear();
477 }
478 let mut guard = self.inner.active_run.lock().expect("run lock");
479 *guard = None;
480 }
481
482 fn current_done(&self) -> Option<Arc<Notify>> {
483 self.inner
484 .active_run
485 .lock()
486 .expect("run lock")
487 .as_ref()
488 .map(|r| Arc::clone(&r.done))
489 }
490
491 fn abort_token(&self) -> CancellationToken {
492 self.inner
493 .active_run
494 .lock()
495 .expect("run lock")
496 .as_ref()
497 .map(|r| r.abort.clone())
498 .unwrap_or_else(CancellationToken::new)
499 }
500
501 fn context_snapshot(&self) -> AgentContext {
502 let state = self.inner.state.lock().expect("state lock");
503 AgentContext {
504 system_prompt: state.system_prompt.clone(),
505 messages: state.messages.clone(),
506 tools: state.tools.clone(),
507 }
508 }
509
510 fn build_config(&self, signal: CancellationToken) -> AgentLoopConfig {
511 let state = self.inner.state.lock().expect("state lock");
512 let steering = Arc::clone(&self.inner.steering_queue);
513 let follow_up = Arc::clone(&self.inner.follow_up_queue);
514 AgentLoopConfig {
515 model: state.model.clone(),
516 convert_to_llm: Arc::clone(&self.inner.convert_to_llm),
517 transform_context: None,
518 get_api_key: None,
519 should_stop_after_turn: None,
520 prepare_next_turn: None,
521 after_tool_results: None,
522 get_steering_messages: Some(Arc::new(move || {
523 let q = Arc::clone(&steering);
524 Box::pin(async move { q.lock().expect("steer lock").try_drain() })
525 })),
526 get_follow_up_messages: Some(Arc::new(move || {
527 let q = Arc::clone(&follow_up);
528 Box::pin(async move { q.lock().expect("followup lock").try_drain() })
529 })),
530 before_tool_call: None,
531 after_tool_call: None,
532 tool_execution: self.inner.tool_execution,
533 thinking_level: state.thinking_level,
534 api_key: None,
535 timeout: None,
536 max_retries: None,
537 max_retry_delay: None,
538 cache_retention: rpi_ai::provider::CacheRetention::default(),
539 session_id: self.inner.session_id.clone(),
540 signal,
541 }
542 }
543
544 async fn run_prompt(&self, messages: Vec<AgentMessage>) -> Result<(), crate::AgentError> {
545 let signal = self.abort_token();
546 let context = self.context_snapshot();
547 let config = self.build_config(signal);
548 let emit: Arc<dyn AgentEmitter> = Arc::new(StatefulEmitter {
549 state: Arc::clone(&self.inner.state),
550 broadcast: BroadcastEmitter::from_sender(self.inner.event_tx.clone()),
551 });
552 let stream_fn = Arc::clone(&self.inner.stream_fn);
553 run_agent_loop(messages, context, config, emit, stream_fn).await?;
554 Ok(())
555 }
556
557 async fn run_continue(&self) -> Result<(), crate::AgentError> {
558 let signal = self.abort_token();
559 let context = self.context_snapshot();
560 if context.messages.is_empty() {
561 return Err(crate::AgentError::State(
562 "No messages to continue from".into(),
563 ));
564 }
565 if context.messages.last().unwrap().is_assistant() {
566 return Err(crate::AgentError::State(
567 "Cannot continue from message role: assistant".into(),
568 ));
569 }
570 let config = self.build_config(signal);
571 let emit: Arc<dyn AgentEmitter> = Arc::new(StatefulEmitter {
572 state: Arc::clone(&self.inner.state),
573 broadcast: BroadcastEmitter::from_sender(self.inner.event_tx.clone()),
574 });
575 let stream_fn = Arc::clone(&self.inner.stream_fn);
576 run_agent_loop_continue(context, config, emit, stream_fn).await?;
577 Ok(())
578 }
579}
580
581fn default_model() -> Model {
582 rpi_ai::model::Model::new(
583 "unknown",
584 "unknown",
585 rpi_ai::types::Api::Other("unknown".into()),
586 "unknown",
587 "",
588 )
589}
590
591#[cfg(test)]
592mod tests {
593 use super::*;
594 use rpi_ai::event_stream::create_assistant_message_event_stream;
595 use rpi_ai::provider::Provider;
596 use rpi_ai::providers::faux::{FauxProvider, FauxScript};
597
598 fn faux_stream_fn(provider: Arc<FauxProvider>) -> StreamFn {
599 crate::stream_fn::stream_fn(move |model, ctx, opts| {
600 let (mut prod, stream) = create_assistant_message_event_stream();
603 let p = Arc::clone(&provider);
604 let model = model.clone();
605 let ctx = ctx.clone();
606 let opts = opts.clone();
607 tokio::spawn(async move {
608 let mut s = p.stream_simple(&model, &ctx, &opts).await;
609 while let Some(ev) = s.next().await {
611 if !prod.push(ev) {
612 break;
613 }
614 }
615 });
616 stream
617 })
618 }
619
620 #[tokio::test]
621 async fn builder_requires_stream_fn() {
622 let res = AgentBuilder::new().build();
623 assert!(res.is_err(), "build with no stream_fn should error");
624 }
625
626 #[tokio::test]
627 async fn prompt_with_faux_text_collects_events() {
628 let provider = FauxProvider::new(FauxScript::new().with_text("hello"));
629 let sf = faux_stream_fn(provider);
630 let agent = AgentBuilder::new().stream_fn(sf).build().unwrap();
631
632 let mut rx = agent.subscribe();
633 agent.prompt("hi").await.unwrap();
634
635 let mut saw_start = false;
637 let mut saw_end = false;
638 while let Ok(ev) = rx.try_recv() {
639 match ev {
640 AgentEvent::AgentStart => saw_start = true,
641 AgentEvent::AgentEnd { .. } => saw_end = true,
642 _ => {}
643 }
644 }
645 assert!(saw_start, "agent_start observed");
646 assert!(saw_end, "agent_end observed");
647 assert_eq!(agent.state().messages.len(), 2);
649 }
650}