1use std::any::{Any, TypeId};
90use std::collections::HashMap;
91use std::fmt;
92use std::future::Future;
93use std::path::PathBuf;
94use std::sync::atomic::{AtomicBool, Ordering};
95use std::sync::{Arc, Mutex};
96use std::time::Duration;
97
98use async_trait::async_trait;
99use serde_json::Value;
100use thiserror::Error;
101use tokio::sync::Semaphore;
102use tokio::sync::broadcast;
103use tokio::task::JoinHandle;
104use uuid::Uuid;
105
106use super::agent::{AgentRuntime, RunOutput};
107use super::error::RuntimeError;
108use super::event::AgentEvent;
109use super::run::{RunId, RunRequest};
110use super::stream::RuntimeEventEnvelope;
111use behest_provider::{ChatStreamEvent, ModelName, ProviderId, ToolChoice};
112
113#[derive(Debug, Clone)]
120pub struct EmitRequest {
121 pub provider: ProviderId,
123 pub model: ModelName,
125 pub input: String,
127 pub session_id: Option<Uuid>,
129 pub client_request_id: Option<String>,
131 pub metadata: Value,
133}
134
135impl EmitRequest {
136 #[must_use]
138 pub fn new(provider: ProviderId, model: ModelName, input: impl Into<String>) -> Self {
139 Self {
140 provider,
141 model,
142 input: input.into(),
143 session_id: None,
144 client_request_id: None,
145 metadata: Value::Null,
146 }
147 }
148
149 #[must_use]
151 pub fn with_session_id(mut self, session_id: Uuid) -> Self {
152 self.session_id = Some(session_id);
153 self
154 }
155
156 #[must_use]
158 pub fn with_client_request_id(mut self, id: impl Into<String>) -> Self {
159 self.client_request_id = Some(id.into());
160 self
161 }
162
163 #[must_use]
165 pub fn with_metadata(mut self, metadata: Value) -> Self {
166 self.metadata = metadata;
167 self
168 }
169
170 #[must_use]
175 pub fn into_run_request(self) -> RunRequest {
176 RunRequest {
177 session_id: self.session_id,
178 run_id: None,
179 provider: self.provider,
180 model: self.model,
181 input: self.input,
182 metadata: self.metadata,
183 tool_choice: ToolChoice::Auto,
184 client_request_id: self.client_request_id,
185 }
186 }
187}
188
189#[derive(Debug, Error)]
191pub enum InvocationError {
192 #[error(transparent)]
194 Runtime(#[from] RuntimeError),
195
196 #[error("invocation task failed: {message}")]
198 TaskFailed {
199 message: String,
201 },
202
203 #[error("invalid invocation request: {message}")]
205 InvalidRequest {
206 message: String,
208 },
209}
210
211#[derive(Debug, Error)]
213pub enum SessionDataError {
214 #[error("session data key not found: {session_id}/{key}")]
216 NotFound {
217 session_id: Uuid,
219 key: String,
221 },
222 #[error("session data storage error: {message}")]
224 Storage {
225 message: String,
227 },
228}
229
230#[async_trait]
241pub trait SessionDataStore: Send + Sync {
242 async fn set(
248 &self,
249 session_id: Uuid,
250 key: String,
251 value: Value,
252 ) -> Result<(), SessionDataError>;
253
254 async fn get(&self, session_id: Uuid, key: &str) -> Result<Option<Value>, SessionDataError>;
262
263 async fn delete(&self, session_id: Uuid, key: &str) -> Result<(), SessionDataError>;
271}
272
273#[derive(Clone)]
278pub struct MemorySessionDataStore {
279 data: Arc<Mutex<HashMap<(Uuid, String), Value>>>,
280}
281
282impl Default for MemorySessionDataStore {
283 fn default() -> Self {
284 Self::new()
285 }
286}
287
288impl MemorySessionDataStore {
289 #[must_use]
291 pub fn new() -> Self {
292 Self {
293 data: Arc::new(Mutex::new(HashMap::new())),
294 }
295 }
296}
297
298impl fmt::Debug for MemorySessionDataStore {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 f.debug_struct("MemorySessionDataStore")
301 .finish_non_exhaustive()
302 }
303}
304
305#[async_trait]
306impl SessionDataStore for MemorySessionDataStore {
307 async fn set(
308 &self,
309 session_id: Uuid,
310 key: String,
311 value: Value,
312 ) -> Result<(), SessionDataError> {
313 let mut map = self
314 .data
315 .lock()
316 .unwrap_or_else(std::sync::PoisonError::into_inner);
317 map.insert((session_id, key), value);
318 Ok(())
319 }
320
321 async fn get(&self, session_id: Uuid, key: &str) -> Result<Option<Value>, SessionDataError> {
322 let map = self
323 .data
324 .lock()
325 .unwrap_or_else(std::sync::PoisonError::into_inner);
326 Ok(map.get(&(session_id, key.to_string())).cloned())
327 }
328
329 async fn delete(&self, session_id: Uuid, key: &str) -> Result<(), SessionDataError> {
330 let mut map = self
331 .data
332 .lock()
333 .unwrap_or_else(std::sync::PoisonError::into_inner);
334 map.remove(&(session_id, key.to_string()));
335 Ok(())
336 }
337}
338
339pub struct FileSessionDataStore {
346 base_dir: PathBuf,
347 locks: Arc<Mutex<HashMap<Uuid, Arc<Mutex<()>>>>>,
348}
349
350impl FileSessionDataStore {
351 #[must_use]
355 pub fn new(base_dir: impl Into<PathBuf>) -> Self {
356 Self {
357 base_dir: base_dir.into(),
358 locks: Arc::new(Mutex::new(HashMap::new())),
359 }
360 }
361
362 fn session_path(&self, session_id: Uuid) -> PathBuf {
363 self.base_dir.join(format!("{session_id}.json"))
364 }
365
366 fn session_lock(&self, session_id: Uuid) -> Arc<Mutex<()>> {
367 let mut map = self
368 .locks
369 .lock()
370 .unwrap_or_else(std::sync::PoisonError::into_inner);
371 map.entry(session_id)
372 .or_insert_with(|| Arc::new(Mutex::new(())))
373 .clone()
374 }
375}
376
377impl fmt::Debug for FileSessionDataStore {
378 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379 f.debug_struct("FileSessionDataStore")
380 .field("base_dir", &self.base_dir)
381 .finish_non_exhaustive()
382 }
383}
384
385#[async_trait]
386impl SessionDataStore for FileSessionDataStore {
387 async fn set(
388 &self,
389 session_id: Uuid,
390 key: String,
391 value: Value,
392 ) -> Result<(), SessionDataError> {
393 let path = self.session_path(session_id);
394 let lock = self.session_lock(session_id);
395 let base = self.base_dir.clone();
396
397 tokio::task::spawn_blocking(move || {
398 let _guard = lock
399 .lock()
400 .unwrap_or_else(std::sync::PoisonError::into_inner);
401 std::fs::create_dir_all(&base).map_err(|e| SessionDataError::Storage {
402 message: format!("failed to create base dir: {e}"),
403 })?;
404
405 let mut map: HashMap<String, Value> = if path.exists() {
406 let data =
407 std::fs::read_to_string(&path).map_err(|e| SessionDataError::Storage {
408 message: format!("failed to read session file: {e}"),
409 })?;
410 serde_json::from_str(&data).map_err(|e| SessionDataError::Storage {
411 message: format!("failed to parse session file: {e}"),
412 })?
413 } else {
414 HashMap::new()
415 };
416
417 map.insert(key, value);
418 let json =
419 serde_json::to_string_pretty(&map).map_err(|e| SessionDataError::Storage {
420 message: format!("failed to serialize session data: {e}"),
421 })?;
422 std::fs::write(&path, json).map_err(|e| SessionDataError::Storage {
423 message: format!("failed to write session file: {e}"),
424 })?;
425 Ok(())
426 })
427 .await
428 .map_err(|e| SessionDataError::Storage {
429 message: format!("spawn_blocking error: {e}"),
430 })?
431 }
432
433 async fn get(&self, session_id: Uuid, key: &str) -> Result<Option<Value>, SessionDataError> {
434 let path = self.session_path(session_id);
435 let lock = self.session_lock(session_id);
436 let key = key.to_string();
437
438 tokio::task::spawn_blocking(move || {
439 let _guard = lock
440 .lock()
441 .unwrap_or_else(std::sync::PoisonError::into_inner);
442 if !path.exists() {
443 return Ok(None);
444 }
445 let data = std::fs::read_to_string(&path).map_err(|e| SessionDataError::Storage {
446 message: format!("failed to read session file: {e}"),
447 })?;
448 let map: HashMap<String, Value> =
449 serde_json::from_str(&data).map_err(|e| SessionDataError::Storage {
450 message: format!("failed to parse session file: {e}"),
451 })?;
452 Ok(map.get(&key).cloned())
453 })
454 .await
455 .map_err(|e| SessionDataError::Storage {
456 message: format!("spawn_blocking error: {e}"),
457 })?
458 }
459
460 async fn delete(&self, session_id: Uuid, key: &str) -> Result<(), SessionDataError> {
461 let path = self.session_path(session_id);
462 let lock = self.session_lock(session_id);
463 let key = key.to_string();
464
465 tokio::task::spawn_blocking(move || {
466 let _guard = lock
467 .lock()
468 .unwrap_or_else(std::sync::PoisonError::into_inner);
469 if !path.exists() {
470 return Ok(());
471 }
472 let data = std::fs::read_to_string(&path).map_err(|e| SessionDataError::Storage {
473 message: format!("failed to read session file: {e}"),
474 })?;
475 let mut map: HashMap<String, Value> =
476 serde_json::from_str(&data).map_err(|e| SessionDataError::Storage {
477 message: format!("failed to parse session file: {e}"),
478 })?;
479 map.remove(&key);
480 let json =
481 serde_json::to_string_pretty(&map).map_err(|e| SessionDataError::Storage {
482 message: format!("failed to serialize session data: {e}"),
483 })?;
484 std::fs::write(&path, json).map_err(|e| SessionDataError::Storage {
485 message: format!("failed to write session file: {e}"),
486 })?;
487 Ok(())
488 })
489 .await
490 .map_err(|e| SessionDataError::Storage {
491 message: format!("spawn_blocking error: {e}"),
492 })?
493 }
494}
495
496#[derive(Debug, Clone)]
502pub enum InvocationEvent {
503 Agent(AgentEvent),
505 Chat(ChatStreamEvent),
507}
508
509impl InvocationEvent {
510 #[must_use]
514 pub fn run_id(&self) -> Option<RunId> {
515 match self {
516 InvocationEvent::Agent(e) => Some(e.run_id()),
517 InvocationEvent::Chat(_) => None,
518 }
519 }
520
521 #[must_use]
523 pub fn as_agent(&self) -> Option<&AgentEvent> {
524 match self {
525 InvocationEvent::Agent(e) => Some(e),
526 InvocationEvent::Chat(_) => None,
527 }
528 }
529}
530
531#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
537pub enum EventKind {
538 Any,
540 RunStarted,
542 ContextBuilt,
544 ModelStarted,
546 TextDelta,
548 ToolCallStarted,
550 ToolCallDelta,
552 ToolCallCompleted,
554 ToolExecutionStarted,
556 ToolExecutionFinished,
558 AssistantMessageCommitted,
560 ToolMessageCommitted,
562 UsageRecorded,
564 CacheMetrics,
566 RunCompleted,
568 RunFailed,
570 RunCancelled,
572 DoomLoopDetected,
574 CompactionCircuitOpened,
576 ChatStarted,
578 ChatTextDelta,
580 ChatToolCallStarted,
582 ChatToolCallArgumentsDelta,
584 ChatToolCallCompleted,
586 ChatFinished,
588}
589
590impl EventKind {
591 #[must_use]
593 #[allow(clippy::too_many_lines)]
594 pub fn matches_agent(self, event: &AgentEvent) -> bool {
595 match self {
596 Self::Any => true,
597 Self::RunStarted => matches!(event, AgentEvent::RunStarted(_)),
598 Self::ContextBuilt => matches!(event, AgentEvent::ContextBuilt(_)),
599 Self::ModelStarted => matches!(event, AgentEvent::ModelStarted(_)),
600 Self::TextDelta => matches!(event, AgentEvent::TextDelta(_)),
601 Self::ToolCallStarted => matches!(event, AgentEvent::ToolCallStarted(_)),
602 Self::ToolCallDelta => matches!(event, AgentEvent::ToolCallDelta(_)),
603 Self::ToolCallCompleted => matches!(event, AgentEvent::ToolCallCompleted(_)),
604 Self::ToolExecutionStarted => matches!(event, AgentEvent::ToolExecutionStarted(_)),
605 Self::ToolExecutionFinished => matches!(event, AgentEvent::ToolExecutionFinished(_)),
606 Self::AssistantMessageCommitted => {
607 matches!(event, AgentEvent::AssistantMessageCommitted(_))
608 }
609 Self::ToolMessageCommitted => matches!(event, AgentEvent::ToolMessageCommitted(_)),
610 Self::UsageRecorded => matches!(event, AgentEvent::UsageRecorded(_)),
611 Self::CacheMetrics => matches!(event, AgentEvent::CacheMetrics(_)),
612 Self::RunCompleted => matches!(event, AgentEvent::RunCompleted(_)),
613 Self::RunFailed => matches!(event, AgentEvent::RunFailed(_)),
614 Self::RunCancelled => matches!(event, AgentEvent::RunCancelled(_)),
615 Self::DoomLoopDetected => matches!(event, AgentEvent::DoomLoopDetected(_)),
616 Self::CompactionCircuitOpened => {
617 matches!(event, AgentEvent::CompactionCircuitOpened(_))
618 }
619 Self::ChatStarted
621 | Self::ChatTextDelta
622 | Self::ChatToolCallStarted
623 | Self::ChatToolCallArgumentsDelta
624 | Self::ChatToolCallCompleted
625 | Self::ChatFinished => false,
626 }
627 }
628
629 #[must_use]
631 pub fn matches(self, event: &InvocationEvent) -> bool {
632 match event {
633 InvocationEvent::Agent(e) => self.matches_agent(e),
634 InvocationEvent::Chat(e) => self.matches_chat(e),
635 }
636 }
637
638 #[must_use]
640 fn matches_chat(self, event: &ChatStreamEvent) -> bool {
641 match self {
642 Self::Any => true,
643 Self::ChatStarted => matches!(event, ChatStreamEvent::Started { .. }),
644 Self::ChatTextDelta => matches!(event, ChatStreamEvent::TextDelta { .. }),
645 Self::ChatToolCallStarted => matches!(event, ChatStreamEvent::ToolCallStarted { .. }),
646 Self::ChatToolCallArgumentsDelta => {
647 matches!(event, ChatStreamEvent::ToolCallArgumentsDelta { .. })
648 }
649 Self::ChatToolCallCompleted => {
650 matches!(event, ChatStreamEvent::ToolCallCompleted { .. })
651 }
652 Self::ChatFinished => matches!(event, ChatStreamEvent::Finished { .. }),
653 _ => false,
654 }
655 }
656}
657
658pub struct InvocationSession {
664 pub session_id: Option<Uuid>,
666 pub run_id: Option<RunId>,
668 store: Arc<dyn SessionDataStore>,
669}
670
671impl InvocationSession {
672 pub async fn set_data(
679 &self,
680 key: impl Into<String>,
681 value: Value,
682 ) -> Result<(), SessionDataError> {
683 let session_id = self.session_id.ok_or(SessionDataError::Storage {
684 message: "session_id not available".into(),
685 })?;
686 self.store.set(session_id, key.into(), value).await
687 }
688
689 pub async fn get_data(&self, key: &str) -> Result<Option<Value>, SessionDataError> {
698 let session_id = self.session_id.ok_or(SessionDataError::Storage {
699 message: "session_id not available".into(),
700 })?;
701 self.store.get(session_id, key).await
702 }
703
704 pub async fn delete_data(&self, key: &str) -> Result<(), SessionDataError> {
713 let session_id = self.session_id.ok_or(SessionDataError::Storage {
714 message: "session_id not available".into(),
715 })?;
716 self.store.delete(session_id, key).await
717 }
718}
719
720impl fmt::Debug for InvocationSession {
721 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
722 f.debug_struct("InvocationSession")
723 .field("session_id", &self.session_id)
724 .field("run_id", &self.run_id)
725 .finish_non_exhaustive()
726 }
727}
728
729#[derive(Debug)]
730struct ControlInner {
731 cancelled: AtomicBool,
732 timeout: Mutex<Option<Duration>>,
733 concurrency_limit: Mutex<Option<usize>>,
734 extensions: Mutex<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
735}
736
737#[derive(Debug, Clone)]
752pub struct Control {
753 inner: Arc<ControlInner>,
754}
755
756impl Default for Control {
757 fn default() -> Self {
758 Self::new()
759 }
760}
761
762impl Control {
763 #[must_use]
765 pub fn new() -> Self {
766 Self {
767 inner: Arc::new(ControlInner {
768 cancelled: AtomicBool::new(false),
769 timeout: Mutex::new(None),
770 concurrency_limit: Mutex::new(None),
771 extensions: Mutex::new(HashMap::new()),
772 }),
773 }
774 }
775
776 pub fn cancel(&self) {
778 self.inner.cancelled.store(true, Ordering::Release);
779 }
780
781 #[must_use]
783 pub fn is_cancelled(&self) -> bool {
784 self.inner.cancelled.load(Ordering::Acquire)
785 }
786
787 pub fn set_timeout(&self, timeout: Duration) {
789 *lock_or_recover(&self.inner.timeout) = Some(timeout);
790 }
791
792 #[must_use]
794 pub fn timeout(&self) -> Option<Duration> {
795 *lock_or_recover(&self.inner.timeout)
796 }
797
798 pub fn set_concurrency_limit(&self, limit: usize) {
800 *lock_or_recover(&self.inner.concurrency_limit) = Some(limit.max(1));
801 }
802
803 #[must_use]
805 pub fn concurrency_limit(&self) -> Option<usize> {
806 *lock_or_recover(&self.inner.concurrency_limit)
807 }
808
809 pub fn set_data<T: Send + Sync + 'static>(&self, val: T) {
814 let mut ext = lock_or_recover(&self.inner.extensions);
815 ext.insert(TypeId::of::<T>(), Arc::new(val));
816 }
817
818 #[must_use]
822 pub fn data<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
823 let ext = lock_or_recover(&self.inner.extensions);
824 ext.get(&TypeId::of::<T>())
825 .and_then(|arc| Arc::clone(arc).downcast::<T>().ok())
826 }
827}
828
829fn lock_or_recover<T>(lock: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
831 lock.lock()
832 .unwrap_or_else(std::sync::PoisonError::into_inner)
833}
834
835#[derive(Debug)]
840pub struct InvocationHandle {
841 task: JoinHandle<()>,
842}
843
844impl InvocationHandle {
845 pub fn abort(&self) {
847 self.task.abort();
848 }
849
850 #[must_use]
852 pub fn is_finished(&self) -> bool {
853 self.task.is_finished()
854 }
855}
856
857impl Drop for InvocationHandle {
858 fn drop(&mut self) {
859 self.task.abort();
860 }
861}
862
863#[derive(Clone)]
868pub struct RuntimeInvocation {
869 runtime: Arc<AgentRuntime>,
870 session_map: Arc<Mutex<HashMap<RunId, Uuid>>>,
871 initial_data: Arc<Mutex<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>>,
872 session_data_store: Arc<dyn SessionDataStore>,
873}
874
875impl RuntimeInvocation {
876 #[must_use]
880 pub fn new(runtime: Arc<AgentRuntime>) -> Self {
881 Self {
882 runtime,
883 session_map: Arc::new(Mutex::new(HashMap::new())),
884 initial_data: Arc::new(Mutex::new(HashMap::new())),
885 session_data_store: Arc::new(MemorySessionDataStore::new()),
886 }
887 }
888
889 #[must_use]
891 pub fn with_session_store(
892 runtime: Arc<AgentRuntime>,
893 store: Arc<dyn SessionDataStore>,
894 ) -> Self {
895 Self {
896 runtime,
897 session_map: Arc::new(Mutex::new(HashMap::new())),
898 initial_data: Arc::new(Mutex::new(HashMap::new())),
899 session_data_store: store,
900 }
901 }
902
903 #[must_use]
905 pub fn runtime(&self) -> &AgentRuntime {
906 &self.runtime
907 }
908
909 pub fn set_data<T: Send + Sync + 'static>(&self, val: T) {
915 let mut map = lock_or_recover(&self.initial_data);
916 map.insert(TypeId::of::<T>(), Arc::new(val));
917 }
918
919 fn make_control(&self) -> Control {
921 let control = Control::new();
922 let extensions = lock_or_recover(&self.initial_data);
923 let mut target = lock_or_recover(&control.inner.extensions);
924 for (type_id, arc) in extensions.iter() {
925 target.insert(*type_id, Arc::clone(arc));
926 }
927 drop(target);
928 drop(extensions);
929 control
930 }
931
932 pub async fn emit<F, Fut>(&self, f: F) -> Result<RunOutput, InvocationError>
949 where
950 F: FnOnce(InvocationSession, Control) -> Fut + Send,
951 Fut: Future<Output = EmitRequest> + Send,
952 {
953 let control = self.make_control();
954 let session = InvocationSession {
955 session_id: None,
956 run_id: None,
957 store: Arc::clone(&self.session_data_store),
958 };
959 if control.is_cancelled() {
960 return Err(InvocationError::TaskFailed {
961 message: "cancelled".into(),
962 });
963 }
964 let request = f(session, control.clone()).await;
965 if control.is_cancelled() {
966 return Err(InvocationError::TaskFailed {
967 message: "cancelled".into(),
968 });
969 }
970 let run_request = request.into_run_request();
971 let output = self.runtime.run(run_request).await?;
972 Ok(output)
973 }
974
975 #[allow(clippy::unused_async)]
987 pub async fn on<F, Fut>(
988 &self,
989 kind: EventKind,
990 f: F,
991 ) -> Result<InvocationHandle, InvocationError>
992 where
993 F: Fn(RuntimeEventEnvelope, InvocationSession, Control) -> Fut + Send + Sync + 'static,
994 Fut: Future<Output = ()> + Send + 'static,
995 {
996 let receiver = self.runtime.subscribe();
997 let control = self.make_control();
998 let session_map = Arc::clone(&self.session_map);
999 let store = Arc::clone(&self.session_data_store);
1000 Ok(spawn_listener(
1001 receiver,
1002 kind,
1003 control,
1004 session_map,
1005 store,
1006 f,
1007 ))
1008 }
1009}
1010
1011fn spawn_listener<F, Fut>(
1016 mut receiver: broadcast::Receiver<AgentEvent>,
1017 kind: EventKind,
1018 control: Control,
1019 session_map: Arc<Mutex<HashMap<RunId, Uuid>>>,
1020 store: Arc<dyn SessionDataStore>,
1021 handler: F,
1022) -> InvocationHandle
1023where
1024 F: Fn(RuntimeEventEnvelope, InvocationSession, Control) -> Fut + Send + Sync + 'static,
1025 Fut: Future<Output = ()> + Send + 'static,
1026{
1027 let handler = Arc::new(handler);
1028 let concurrency_limit = control.concurrency_limit();
1029 let semaphore = concurrency_limit.map(|limit| Arc::new(Semaphore::new(limit)));
1030 let task = tokio::spawn(async move {
1031 loop {
1032 match receiver.recv().await {
1033 Ok(event) => {
1034 if control.is_cancelled() {
1035 break;
1036 }
1037 if !kind.matches_agent(&event) {
1038 continue;
1039 }
1040 let run_id = event.run_id();
1041 let session_id = {
1042 let mut map = lock_or_recover(&session_map);
1043 if let AgentEvent::RunStarted(started) = &event {
1044 map.insert(run_id, started.session_id);
1045 Some(started.session_id)
1046 } else {
1047 map.get(&run_id).copied()
1048 }
1049 };
1050 let envelope = RuntimeEventEnvelope {
1051 event_id: super::stream::RuntimeEventId::new(),
1052 seq: 0,
1053 run_id,
1054 session_id,
1055 event,
1056 emitted_at: chrono::Utc::now(),
1057 };
1058 let session = InvocationSession {
1059 session_id,
1060 run_id: Some(run_id),
1061 store: Arc::clone(&store),
1062 };
1063 let permit = if let Some(sem) = semaphore.clone() {
1064 match sem.acquire_owned().await {
1065 Ok(permit) => Some(permit),
1066 Err(_) => break,
1067 }
1068 } else {
1069 None
1070 };
1071 let h = Arc::clone(&handler);
1072 let c = control.clone();
1073 tokio::spawn(async move {
1074 let _permit = permit;
1075 h(envelope, session, c).await;
1076 });
1077 }
1078 Err(broadcast::error::RecvError::Closed) => break,
1079 Err(broadcast::error::RecvError::Lagged(_)) => {}
1080 }
1081 }
1082 });
1083 InvocationHandle { task }
1084}
1085
1086#[cfg(test)]
1087#[allow(clippy::unwrap_used, clippy::expect_used)]
1088mod tests {
1089 use super::*;
1090 use crate::event::{RunCompleted, RunStarted, TextDelta};
1091 use behest_provider::{ChatStreamEvent, FinishReason, ModelName, ProviderId, ToolChoice};
1092 use chrono::Utc;
1093 use serde_json::json;
1094 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1095 use std::time::Duration;
1096 use tokio::sync::{Notify, broadcast};
1097 use uuid::Uuid;
1098
1099 #[test]
1100 fn emit_request_converts_to_run_request() {
1101 let sid = Uuid::new_v4();
1102 let req = EmitRequest::new(ProviderId::new("p"), ModelName::new("m"), "hi")
1103 .with_session_id(sid)
1104 .with_client_request_id("cid")
1105 .with_metadata(json!({"k": "v"}));
1106 let run = req.into_run_request();
1107 assert_eq!(run.provider, ProviderId::new("p"));
1108 assert_eq!(run.model, ModelName::new("m"));
1109 assert_eq!(run.input, "hi");
1110 assert_eq!(run.session_id, Some(sid));
1111 assert_eq!(run.client_request_id.as_deref(), Some("cid"));
1112 assert_eq!(run.metadata, json!({"k": "v"}));
1113 assert!(matches!(run.tool_choice, ToolChoice::Auto));
1114 assert!(run.run_id.is_none());
1115 }
1116
1117 #[test]
1118 fn emit_request_default_metadata_is_null() {
1119 let req = EmitRequest::new(ProviderId::new("p"), ModelName::new("m"), "hi");
1120 assert!(req.metadata.is_null());
1121 let run = req.clone().into_run_request();
1122 assert!(run.metadata.is_null());
1123 }
1124
1125 #[test]
1126 fn event_kind_matches_agent_text_delta() {
1127 let ev = InvocationEvent::Agent(AgentEvent::TextDelta(TextDelta {
1128 run_id: RunId::new(),
1129 delta: "x".into(),
1130 timestamp: Utc::now(),
1131 }));
1132 assert!(EventKind::TextDelta.matches(&ev));
1133 assert!(EventKind::Any.matches(&ev));
1134 assert!(!EventKind::RunCompleted.matches(&ev));
1135 assert!(!EventKind::ChatTextDelta.matches(&ev));
1136 }
1137
1138 #[test]
1139 fn event_kind_matches_agent_run_completed() {
1140 let ev = InvocationEvent::Agent(AgentEvent::RunCompleted(RunCompleted {
1141 run_id: RunId::new(),
1142 finish_reason: FinishReason::Stop,
1143 iterations: 1,
1144 timestamp: Utc::now(),
1145 }));
1146 assert!(EventKind::RunCompleted.matches(&ev));
1147 assert!(EventKind::Any.matches(&ev));
1148 assert!(!EventKind::TextDelta.matches(&ev));
1149 }
1150
1151 #[test]
1152 fn event_kind_matches_chat_text_delta() {
1153 let ev = InvocationEvent::Chat(ChatStreamEvent::TextDelta { delta: "x".into() });
1154 assert!(EventKind::ChatTextDelta.matches(&ev));
1155 assert!(EventKind::Any.matches(&ev));
1156 assert!(!EventKind::TextDelta.matches(&ev));
1157 }
1158
1159 #[test]
1160 fn control_cancel_sets_flag_and_shares_state() {
1161 let c = Control::new();
1162 assert!(!c.is_cancelled());
1163 c.cancel();
1164 assert!(c.is_cancelled());
1165 let cloned = c.clone();
1166 assert!(cloned.is_cancelled(), "clone must share cancel state");
1167 }
1168
1169 #[test]
1170 fn control_set_data_and_data() {
1171 let c = Control::new();
1172 c.set_data(42_u32);
1173 c.set_data(String::from("hello"));
1174 assert_eq!(*c.data::<u32>().unwrap(), 42);
1175 assert_eq!(*c.data::<String>().unwrap(), "hello");
1176 assert!(c.data::<f64>().is_none());
1177 }
1178
1179 #[test]
1180 fn control_data_shared_across_clones() {
1181 let c = Control::new();
1182 c.set_data(99_u64);
1183 let cloned = c.clone();
1184 assert_eq!(*cloned.data::<u64>().unwrap(), 99);
1185 }
1186
1187 #[tokio::test]
1188 async fn memory_session_data_store_round_trip() {
1189 let store = MemorySessionDataStore::new();
1190 let sid = Uuid::new_v4();
1191 store.set(sid, "k".into(), json!({"x": 1})).await.unwrap();
1192 let val = store.get(sid, "k").await.unwrap();
1193 assert_eq!(val, Some(json!({"x": 1})));
1194 store.delete(sid, "k").await.unwrap();
1195 assert!(store.get(sid, "k").await.unwrap().is_none());
1196 }
1197
1198 #[tokio::test]
1199 async fn file_session_data_store_round_trip() {
1200 let dir = std::env::temp_dir().join(format!("behest_test_{}", Uuid::new_v4()));
1201 let store = FileSessionDataStore::new(&dir);
1202 let sid = Uuid::new_v4();
1203 store.set(sid, "name".into(), json!("alice")).await.unwrap();
1204 let val = store.get(sid, "name").await.unwrap();
1205 assert_eq!(val, Some(json!("alice")));
1206 store.delete(sid, "name").await.unwrap();
1207 assert!(store.get(sid, "name").await.unwrap().is_none());
1208 let _ = std::fs::remove_dir_all(&dir);
1209 }
1210
1211 #[tokio::test]
1212 async fn invocation_session_set_get_delete() {
1213 let session = InvocationSession {
1214 session_id: Some(Uuid::new_v4()),
1215 run_id: None,
1216 store: Arc::new(MemorySessionDataStore::new()),
1217 };
1218 session.set_data("key", json!(42)).await.unwrap();
1219 let val = session.get_data("key").await.unwrap();
1220 assert_eq!(val, Some(json!(42)));
1221 session.delete_data("key").await.unwrap();
1222 assert!(session.get_data("key").await.unwrap().is_none());
1223 }
1224
1225 #[tokio::test]
1226 async fn invocation_session_no_session_id_errors() {
1227 let session = InvocationSession {
1228 session_id: None,
1229 run_id: None,
1230 store: Arc::new(MemorySessionDataStore::new()),
1231 };
1232 let result = session.set_data("key", json!(1)).await;
1233 assert!(result.is_err());
1234 }
1235
1236 #[tokio::test]
1237 async fn on_only_handles_matching_events() {
1238 let (tx, rx) = broadcast::channel::<AgentEvent>(16);
1239 let counter = Arc::new(AtomicUsize::new(0));
1240 let c = counter.clone();
1241 let handler = move |_, _, _| {
1242 let c = c.clone();
1243 async move {
1244 c.fetch_add(1, Ordering::SeqCst);
1245 }
1246 };
1247 let session_map = Arc::new(Mutex::new(HashMap::new()));
1248 let store: Arc<dyn SessionDataStore> = Arc::new(MemorySessionDataStore::new());
1249 let handle = spawn_listener(
1250 rx,
1251 EventKind::TextDelta,
1252 Control::new(),
1253 session_map,
1254 store,
1255 handler,
1256 );
1257
1258 let _ = tx.send(AgentEvent::TextDelta(TextDelta {
1259 run_id: RunId::new(),
1260 delta: "a".into(),
1261 timestamp: Utc::now(),
1262 }));
1263 let _ = tx.send(AgentEvent::RunStarted(RunStarted {
1264 run_id: RunId::new(),
1265 session_id: Uuid::new_v4(),
1266 provider: ProviderId::new("p"),
1267 model: ModelName::new("m"),
1268 timestamp: Utc::now(),
1269 }));
1270
1271 tokio::time::sleep(Duration::from_millis(100)).await;
1272 assert_eq!(
1273 counter.load(Ordering::SeqCst),
1274 1,
1275 "only the matching TextDelta event should be handled"
1276 );
1277 handle.abort();
1278 }
1279
1280 #[test]
1281 fn control_set_concurrency_limit_clamps_zero_to_one() {
1282 let control = Control::new();
1283
1284 control.set_concurrency_limit(0);
1285
1286 assert_eq!(control.concurrency_limit(), Some(1));
1287 }
1288
1289 #[tokio::test]
1290 async fn listener_applies_concurrency_backpressure_before_spawning_handlers() {
1291 let (tx, rx) = broadcast::channel::<AgentEvent>(1);
1292 let handled = Arc::new(AtomicUsize::new(0));
1293 let first_started = Arc::new(Notify::new());
1294 let release_first = Arc::new(Notify::new());
1295 let released = Arc::new(AtomicBool::new(false));
1296
1297 let h_handled = Arc::clone(&handled);
1298 let h_first_started = Arc::clone(&first_started);
1299 let h_release_first = Arc::clone(&release_first);
1300 let h_released = Arc::clone(&released);
1301 let handler = move |_, _, _| {
1302 let handled = Arc::clone(&h_handled);
1303 let first_started = Arc::clone(&h_first_started);
1304 let release_first = Arc::clone(&h_release_first);
1305 let released = Arc::clone(&h_released);
1306 async move {
1307 let current = handled.fetch_add(1, Ordering::SeqCst) + 1;
1308 if current == 1 {
1309 first_started.notify_waiters();
1310 release_first.notified().await;
1311 released.store(true, Ordering::SeqCst);
1312 return;
1313 }
1314
1315 if !released.load(Ordering::SeqCst) {
1316 release_first.notified().await;
1317 }
1318 }
1319 };
1320 let session_map = Arc::new(Mutex::new(HashMap::new()));
1321 let store: Arc<dyn SessionDataStore> = Arc::new(MemorySessionDataStore::new());
1322 let control = Control::new();
1323 control.set_concurrency_limit(1);
1324 let handle = spawn_listener(
1325 rx,
1326 EventKind::TextDelta,
1327 control,
1328 session_map,
1329 store,
1330 handler,
1331 );
1332
1333 let _ = tx.send(AgentEvent::TextDelta(TextDelta {
1334 run_id: RunId::new(),
1335 delta: "first".into(),
1336 timestamp: Utc::now(),
1337 }));
1338 tokio::time::timeout(Duration::from_millis(100), first_started.notified())
1339 .await
1340 .expect("first handler should start");
1341
1342 for idx in 0..20 {
1343 let _ = tx.send(AgentEvent::TextDelta(TextDelta {
1344 run_id: RunId::new(),
1345 delta: format!("queued-{idx}"),
1346 timestamp: Utc::now(),
1347 }));
1348 tokio::task::yield_now().await;
1349 }
1350
1351 assert_eq!(handled.load(Ordering::SeqCst), 1);
1352 release_first.notify_waiters();
1353 tokio::time::sleep(Duration::from_millis(100)).await;
1354
1355 assert!(
1356 handled.load(Ordering::SeqCst) <= 3,
1357 "listener should not pre-spawn handlers while the limit is saturated"
1358 );
1359 handle.abort();
1360 }
1361
1362 #[tokio::test]
1363 async fn invocation_handle_abort_stops_listener() {
1364 let (tx, rx) = broadcast::channel::<AgentEvent>(16);
1365 let counter = Arc::new(AtomicUsize::new(0));
1366 let c = counter.clone();
1367 let handler = move |_, _, _| {
1368 let c = c.clone();
1369 async move {
1370 c.fetch_add(1, Ordering::SeqCst);
1371 }
1372 };
1373 let session_map = Arc::new(Mutex::new(HashMap::new()));
1374 let store: Arc<dyn SessionDataStore> = Arc::new(MemorySessionDataStore::new());
1375 let handle = spawn_listener(
1376 rx,
1377 EventKind::Any,
1378 Control::new(),
1379 session_map,
1380 store,
1381 handler,
1382 );
1383
1384 let _ = tx.send(AgentEvent::RunStarted(RunStarted {
1385 run_id: RunId::new(),
1386 session_id: Uuid::new_v4(),
1387 provider: ProviderId::new("p"),
1388 model: ModelName::new("m"),
1389 timestamp: Utc::now(),
1390 }));
1391 tokio::time::sleep(Duration::from_millis(100)).await;
1392 let before = counter.load(Ordering::SeqCst);
1393 assert!(before >= 1, "first event should be handled");
1394
1395 handle.abort();
1396 tokio::time::sleep(Duration::from_millis(50)).await;
1397 assert!(handle.is_finished(), "listener should finish after abort");
1398
1399 let _ = tx.send(AgentEvent::RunStarted(RunStarted {
1400 run_id: RunId::new(),
1401 session_id: Uuid::new_v4(),
1402 provider: ProviderId::new("p"),
1403 model: ModelName::new("m"),
1404 timestamp: Utc::now(),
1405 }));
1406 tokio::time::sleep(Duration::from_millis(100)).await;
1407 assert_eq!(
1408 counter.load(Ordering::SeqCst),
1409 before,
1410 "no events should be handled after abort"
1411 );
1412 }
1413}