1pub mod bash;
32pub mod compaction;
33pub mod events;
34pub mod extension;
35pub mod extension_runner;
36pub mod model;
37pub mod persistence;
38pub mod prompt;
39pub mod retry;
40pub mod stats;
41pub mod subscribe;
42pub mod tools;
43pub mod tree;
44
45pub use events::{
46 AgentSessionEvent, AgentSessionEventListener, CompactionReason, ModelSelectSource,
47 SessionBeforeForkPosition, SessionBeforeSwitchReason, SessionShutdownReason, SessionStartEvent,
48 SessionStartReason,
49};
50pub use extension::{
51 ExtensionBindError, ExtensionBindings, ExtensionMode, ExtensionUiContext,
52 ReplacedSessionContext,
53};
54pub use extension_runner::{
55 BeforeAgentStartResult, CancelResult, ExtensionRunner, ExtensionRunnerError,
56 InputTransformResult, NullExtensionRunner, SessionHooks, SystemPromptState,
57};
58
59use std::sync::{Arc, Mutex};
60use std::time::Duration;
61
62use futures::future::BoxFuture;
63use pi_agent::{Agent, AgentLoopConfig, AgentMessage, AgentOptions, AgentTool, QueueMode};
64use pi_ai::{AssistantMessage, Model, ModelThinkingLevel, Provider};
65use tokio::sync::{Mutex as AsyncMutex, Notify};
66use tokio_util::sync::CancellationToken;
67
68use crate::core::model_runtime::ModelRuntime;
69use crate::core::sessions::{SessionError, SessionManager};
70use crate::core::settings::SettingsManager;
71use events::AgentSessionEvent as Event;
72use subscribe::EventPump;
73
74#[derive(Clone, Debug)]
76pub struct ScopedModel {
77 pub model: Model,
79 pub thinking_level: Option<ModelThinkingLevel>,
81}
82
83pub struct AgentSessionConfig {
89 pub agent: Option<Agent>,
92 pub provider: Option<Arc<dyn Provider>>,
94 pub session_manager: SessionManager,
96 pub settings_manager: SettingsManager,
98 pub cwd: String,
100 pub scoped_models: Vec<ScopedModel>,
102 pub initial_active_tool_names: Option<Vec<String>>,
104 pub allowed_tool_names: Option<Vec<String>>,
106 pub excluded_tool_names: Option<Vec<String>>,
108 pub tools: Vec<Arc<dyn AgentTool>>,
110 pub system_prompt: String,
112 pub model: Option<Model>,
114 pub thinking_level: ModelThinkingLevel,
116 pub messages: Vec<AgentMessage>,
118 pub extension_runner: Option<Arc<dyn ExtensionRunner>>,
120 pub host_extension_runner: Option<Arc<crate::core::extension_host::HostExtensionRunner>>,
122 pub model_runtime: Option<Arc<ModelRuntime>>,
124 pub compaction_stream_override: Option<compaction::CompactionStreamHandle>,
126 pub skills: Vec<crate::core::resources::skills::Skill>,
128 pub prompt_templates: Vec<crate::core::resources::prompts::PromptTemplate>,
130 pub resource_loader: Option<crate::core::resources::DefaultResourceLoader>,
132 pub session_start_event: Option<SessionStartEvent>,
135 pub base_config: Option<AgentLoopConfig>,
137}
138
139impl AgentSessionConfig {
140 pub fn test_config(
150 provider: Arc<dyn Provider>,
151 model: Model,
152 ) -> Result<Self, AgentSessionError> {
153 let session_manager = SessionManager::in_memory(Some("."), None)
154 .or_else(|_| SessionManager::in_memory(None, None))?;
155 let settings_manager = SettingsManager::in_memory(
156 &crate::core::settings::Settings::default(),
157 crate::core::settings::SettingsManagerCreateOptions {
158 project_trusted: true,
159 },
160 );
161 Ok(Self {
162 agent: None,
163 provider: Some(provider),
164 session_manager,
165 settings_manager,
166 cwd: ".".to_owned(),
167 scoped_models: Vec::new(),
168 initial_active_tool_names: None,
169 allowed_tool_names: None,
170 excluded_tool_names: None,
171 tools: Vec::new(),
172 system_prompt: String::new(),
173 model: Some(model),
174 thinking_level: ModelThinkingLevel::Off,
175 messages: Vec::new(),
176 extension_runner: None,
177 host_extension_runner: None,
178 model_runtime: None,
179 compaction_stream_override: None,
180 skills: Vec::new(),
181 prompt_templates: Vec::new(),
182 resource_loader: None,
183 session_start_event: None,
184 base_config: None,
185 })
186 }
187}
188
189pub(super) struct AgentSessionInner {
193 lifecycle: SessionLifecycle,
195 pub(super) steering_messages: Vec<String>,
197 pub(super) follow_up_messages: Vec<String>,
199 pub(super) retry_attempt: u32,
201 pub(super) max_retries: u32,
203 pub(super) last_assistant_message: Option<AssistantMessage>,
205 pub(super) listeners: Vec<(u64, AgentSessionEventListener)>,
207 pub(super) next_listener_id: u64,
209 pub(super) backpressure_hooks: Vec<(u64, EventBackpressureHook)>,
211 pub(super) next_backpressure_hook_id: u64,
213 pub(super) pending_session_error: Option<SessionError>,
215 pump: Option<EventPump>,
217 pub(super) idle_notify: Arc<Notify>,
219 pub(super) processed_agent_ends: u64,
221 pub(super) agent_end_notify: Arc<Notify>,
223 pub(super) agent_end_wait_cancel: CancellationToken,
225 pub(super) scoped_models: Vec<ScopedModel>,
227 pub(super) active_tool_names: Vec<String>,
229 pub(super) base_system_prompt: String,
231 pub(super) retry_abort: Option<CancellationToken>,
233 pub(super) compaction_abort: Option<CancellationToken>,
235 pub(super) auto_compaction_abort: Option<CancellationToken>,
237 pub(super) branch_summary_abort: Option<CancellationToken>,
239 pub(super) bash_abort: Option<CancellationToken>,
241 pub(super) pending_next_turn_messages: Vec<AgentMessage>,
243 pub(super) extension_mode: Option<crate::core::agent_session::extension::ExtensionMode>,
245 pub(super) extension_ui_tag: Option<String>,
247 pub(super) extension_shutdown_handler: Option<std::sync::Arc<dyn Fn() + Send + Sync>>,
249 pub(super) extension_error_listener: Option<ExtensionErrorListener>,
251 pub(super) extension_command_context: Option<serde_json::Value>,
253 pub(super) pending_session_start: Option<SessionStartEvent>,
256 pub(super) initial_resources_discovered: bool,
259 pub(super) base_tool_definitions: Vec<std::sync::Arc<dyn AgentTool>>,
261 pub(super) tool_registry: Vec<std::sync::Arc<dyn AgentTool>>,
263 pub(super) allowed_tool_names: Option<std::collections::HashSet<String>>,
265 pub(super) excluded_tool_names: Option<std::collections::HashSet<String>>,
267 pub(super) pending_bash_messages: Vec<crate::core::messages::BashExecutionMessage>,
269 #[cfg(test)]
273 pub(super) reload_restart_factory: Option<extension::ReloadRestartFactory>,
274}
275
276#[derive(Default)]
277pub(super) struct AutomaticActionFlags {
278 auto_retry_enabled: bool,
279 auto_compaction_enabled: bool,
280}
281
282pub(super) struct SessionLifecycle {
283 automatic_actions: AutomaticActionFlags,
284 is_agent_run_active: bool,
285 overflow_recovery_attempted: bool,
286 disposed: bool,
287}
288
289impl Default for SessionLifecycle {
290 fn default() -> Self {
291 Self {
292 automatic_actions: AutomaticActionFlags {
293 auto_retry_enabled: true,
294 auto_compaction_enabled: true,
295 },
296 is_agent_run_active: false,
297 overflow_recovery_attempted: false,
298 disposed: false,
299 }
300 }
301}
302
303impl std::ops::Deref for SessionLifecycle {
304 type Target = AutomaticActionFlags;
305
306 fn deref(&self) -> &Self::Target {
307 &self.automatic_actions
308 }
309}
310
311impl std::ops::DerefMut for SessionLifecycle {
312 fn deref_mut(&mut self) -> &mut Self::Target {
313 &mut self.automatic_actions
314 }
315}
316
317impl std::ops::Deref for AgentSessionInner {
318 type Target = SessionLifecycle;
319
320 fn deref(&self) -> &Self::Target {
321 &self.lifecycle
322 }
323}
324
325impl std::ops::DerefMut for AgentSessionInner {
326 fn deref_mut(&mut self) -> &mut Self::Target {
327 &mut self.lifecycle
328 }
329}
330
331type ExtensionErrorListener = extension::ExtensionErrorListener;
332
333pub type EventBackpressureHook = Arc<dyn Fn() -> BoxFuture<'static, ()> + Send + Sync>;
335
336impl AgentSessionInner {
337 fn new(scoped_models: Vec<ScopedModel>, base_system_prompt: String) -> Self {
338 Self {
339 lifecycle: SessionLifecycle::default(),
340 steering_messages: Vec::new(),
341 follow_up_messages: Vec::new(),
342 retry_attempt: 0,
343 max_retries: 3,
344 last_assistant_message: None,
345 listeners: Vec::new(),
346 next_listener_id: 1,
347 backpressure_hooks: Vec::new(),
348 next_backpressure_hook_id: 1,
349 pending_session_error: None,
350 pump: None,
351 idle_notify: Arc::new(Notify::new()),
352 processed_agent_ends: 0,
353 agent_end_notify: Arc::new(Notify::new()),
354 agent_end_wait_cancel: CancellationToken::new(),
355 scoped_models,
356 active_tool_names: Vec::new(),
357 base_system_prompt,
358 retry_abort: None,
359 compaction_abort: None,
360 auto_compaction_abort: None,
361 branch_summary_abort: None,
362 bash_abort: None,
363 pending_next_turn_messages: Vec::new(),
364 extension_mode: None,
365 extension_ui_tag: None,
366 extension_shutdown_handler: None,
367 extension_error_listener: None,
368 extension_command_context: None,
369 pending_session_start: None,
370 initial_resources_discovered: false,
371 base_tool_definitions: Vec::new(),
372 tool_registry: Vec::new(),
373 allowed_tool_names: None,
374 excluded_tool_names: None,
375 pending_bash_messages: Vec::new(),
376 #[cfg(test)]
377 reload_restart_factory: None,
378 }
379 }
380}
381
382pub struct AgentSession {
387 pub agent: Agent,
389 pub(super) session_manager: Arc<AsyncMutex<SessionManager>>,
391 pub(super) bash_flush_lock: AsyncMutex<()>,
393 pub(super) settings_manager: std::sync::Mutex<SettingsManager>,
397 pub cwd: String,
399 pub(super) hooks: Arc<SessionHooks>,
401 pub(super) inner: Mutex<AgentSessionInner>,
403 pub(super) host_extension_runner:
405 std::sync::RwLock<Option<Arc<crate::core::extension_host::HostExtensionRunner>>>,
406 pub(super) model_runtime: Option<Arc<ModelRuntime>>,
408 pub(super) compaction_stream_override: Option<compaction::CompactionStreamHandle>,
410 pub(super) skills: Mutex<Vec<crate::core::resources::skills::Skill>>,
412 pub(super) prompt_templates: Mutex<Vec<crate::core::resources::prompts::PromptTemplate>>,
414 pub(super) resource_loader: Option<AsyncMutex<crate::core::resources::DefaultResourceLoader>>,
416 pub(super) self_handle: Mutex<Option<std::sync::Weak<AgentSession>>>,
418 pub(super) bind_lock: AsyncMutex<()>,
423}
424
425#[derive(Debug, thiserror::Error)]
427pub enum AgentSessionError {
428 #[error("AgentSessionConfig requires `agent` or `provider`")]
430 MissingAgentOrProvider,
431 #[error(transparent)]
433 Session(#[from] crate::core::sessions::SessionError),
434}
435
436impl AgentSession {
437 pub fn new(config: AgentSessionConfig) -> Result<Arc<Self>, AgentSessionError> {
444 let runner = config
445 .extension_runner
446 .unwrap_or_else(|| Arc::new(NullExtensionRunner));
447 let hooks = Arc::new(SessionHooks::new(runner));
448 hooks.set_base_system_prompt(config.system_prompt.clone());
449 hooks.set_tools(config.tools.clone());
450
451 let agent = if let Some(agent) = config.agent {
452 agent
453 } else {
454 let provider = config
455 .provider
456 .ok_or(AgentSessionError::MissingAgentOrProvider)?;
457 let model = config
458 .model
459 .clone()
460 .unwrap_or_else(pi_agent::state::default_model);
461 let mut base = config.base_config.unwrap_or_else(|| AgentLoopConfig {
462 model: model.clone(),
463 reasoning: None,
464 temperature: None,
465 max_tokens: None,
466 session_id: None,
467 transport: None,
468 cache_retention: None,
469 thinking_budgets: None,
470 max_retry_delay_ms: None,
471 metadata: None,
472 headers: None,
473 env: None,
474 stream_extra: serde_json::Map::new(),
475 tool_execution: pi_agent::ToolExecutionMode::Parallel,
476 convert_to_llm: pi_agent::default_convert_to_llm_hook(),
477 transform_context: None,
478 get_api_key: None,
479 should_stop_after_turn: None,
480 prepare_next_turn: None,
481 get_steering_messages: None,
482 get_follow_up_messages: None,
483 before_tool_call: None,
484 after_tool_call: None,
485 on_payload: None,
486 on_response: None,
487 });
488 base.before_tool_call = Some(hooks.before_tool_call_hook());
489 base.after_tool_call = Some(hooks.after_tool_call_hook());
490 base.prepare_next_turn = Some(hooks.prepare_next_turn_hook());
491 Agent::new(AgentOptions {
492 system_prompt: config.system_prompt.clone(),
493 model,
494 thinking_level: config.thinking_level,
495 tools: config.tools.clone(),
496 messages: config.messages,
497 config: base,
498 provider,
499 })
500 };
501
502 let retry = config.settings_manager.get_retry_settings();
503 let compaction = config.settings_manager.get_compaction_settings();
504
505 let mut inner = AgentSessionInner::new(config.scoped_models, config.system_prompt.clone());
506 inner.pending_session_start = Some(config.session_start_event.unwrap_or_default());
507 inner.auto_retry_enabled = retry.enabled;
508 inner.max_retries = u32::try_from(retry.max_retries).unwrap_or(u32::MAX);
509 inner.auto_compaction_enabled = compaction.enabled;
510 if let Some(ref names) = config.initial_active_tool_names {
511 inner.active_tool_names.clone_from(names);
512 }
513
514 let session = Arc::new(Self {
515 agent,
516 session_manager: Arc::new(AsyncMutex::new(config.session_manager)),
517 bash_flush_lock: AsyncMutex::new(()),
518 settings_manager: std::sync::Mutex::new(config.settings_manager),
519 cwd: config.cwd,
520 hooks,
521 inner: Mutex::new(inner),
522 host_extension_runner: std::sync::RwLock::new(config.host_extension_runner),
523 model_runtime: config.model_runtime,
524 compaction_stream_override: config.compaction_stream_override,
525 skills: Mutex::new(config.skills),
526 prompt_templates: Mutex::new(config.prompt_templates),
527 resource_loader: config.resource_loader.map(AsyncMutex::new),
528 self_handle: Mutex::new(None),
529 bind_lock: AsyncMutex::new(()),
530 });
531
532 session.build_initial_tool_registry(
535 config.tools.clone(),
536 config.initial_active_tool_names.clone(),
537 config.allowed_tool_names.clone(),
538 config.excluded_tool_names.clone(),
539 );
540
541 if let Ok(mut guard) = session.self_handle.lock() {
543 *guard = Some(Arc::downgrade(&session));
544 }
545 let pump = session.spawn_event_pump();
546 session.store_pump(pump);
547
548 Ok(session)
549 }
550
551 #[must_use]
557 pub fn agent(&self) -> &Agent {
558 &self.agent
559 }
560
561 #[must_use]
563 pub fn extension_runner(&self) -> Arc<dyn ExtensionRunner> {
564 self.hooks.runner()
565 }
566
567 #[must_use]
569 pub fn hooks(&self) -> Arc<SessionHooks> {
570 Arc::clone(&self.hooks)
571 }
572
573 #[must_use]
575 pub fn session_manager(&self) -> Arc<AsyncMutex<SessionManager>> {
576 Arc::clone(&self.session_manager)
577 }
578
579 #[must_use]
581 pub fn model(&self) -> Model {
582 self.agent.state().model
583 }
584
585 #[must_use]
587 pub fn thinking_level(&self) -> ModelThinkingLevel {
588 self.agent.state().thinking_level
589 }
590
591 #[must_use]
594 pub fn is_admission_active(&self) -> bool {
595 self.lock_inner().is_agent_run_active
596 }
597
598 #[must_use]
600 pub fn is_streaming(&self) -> bool {
601 self.agent.state().is_streaming
602 }
603
604 #[must_use]
606 pub fn is_idle(&self) -> bool {
607 let inner = self.lock_inner();
608 !inner.is_agent_run_active && !self.agent.state().is_streaming
609 }
610
611 #[must_use]
613 pub fn is_compacting(&self) -> bool {
614 let inner = self.lock_inner();
615 inner.compaction_abort.is_some() || inner.auto_compaction_abort.is_some()
616 }
617
618 #[must_use]
620 pub fn is_retrying(&self) -> bool {
621 self.lock_inner().retry_abort.is_some()
622 }
623
624 #[must_use]
626 pub fn is_bash_running(&self) -> bool {
627 self.lock_inner().bash_abort.is_some()
628 }
629 #[must_use]
631 pub fn is_summarizing(&self) -> bool {
632 self.lock_inner().branch_summary_abort.is_some()
633 }
634
635 pub async fn session_file(&self) -> Option<String> {
637 self.session_manager
638 .lock()
639 .await
640 .get_session_file()
641 .map(str::to_owned)
642 }
643
644 pub async fn session_id(&self) -> String {
646 self.session_manager
647 .lock()
648 .await
649 .get_session_id()
650 .to_owned()
651 }
652
653 pub async fn session_name(&self) -> Option<String> {
655 self.session_manager.lock().await.get_session_name()
656 }
657
658 #[must_use]
660 pub fn scoped_models(&self) -> Vec<ScopedModel> {
661 self.lock_inner().scoped_models.clone()
662 }
663
664 #[must_use]
666 pub fn pending_message_count(&self) -> usize {
667 let inner = self.lock_inner();
668 inner
669 .steering_messages
670 .len()
671 .saturating_add(inner.follow_up_messages.len())
672 }
673 #[must_use]
675 pub fn pending_messages(&self) -> (Vec<String>, Vec<String>) {
676 let inner = self.lock_inner();
677 (
678 inner.steering_messages.clone(),
679 inner.follow_up_messages.clone(),
680 )
681 }
682
683 #[must_use]
685 pub fn active_tool_names(&self) -> Vec<String> {
686 self.lock_inner().active_tool_names.clone()
687 }
688
689 #[must_use]
691 pub fn message_count(&self) -> usize {
692 self.agent.transcript().len()
693 }
694
695 #[must_use]
697 pub fn messages(&self) -> Vec<AgentMessage> {
698 self.agent.transcript()
699 }
700
701 #[must_use]
703 pub fn steering_mode(&self) -> QueueMode {
704 self.agent.steering_mode()
705 }
706
707 #[must_use]
709 pub fn follow_up_mode(&self) -> QueueMode {
710 self.agent.follow_up_mode()
711 }
712
713 #[must_use]
715 pub fn auto_compaction_enabled(&self) -> bool {
716 self.lock_inner().auto_compaction_enabled
717 }
718
719 #[must_use]
721 pub fn auto_retry_enabled(&self) -> bool {
722 self.lock_inner().auto_retry_enabled
723 }
724
725 #[must_use]
727 pub fn model_runtime_handle(&self) -> Option<Arc<ModelRuntime>> {
728 self.model_runtime.clone()
729 }
730
731 pub fn subscribe<F>(&self, listener: F) -> impl Fn() + Send + Sync + 'static
739 where
740 F: Fn(&AgentSessionEvent) + Send + Sync + 'static,
741 {
742 let listener: AgentSessionEventListener = Arc::new(listener);
743 let listener_id = {
744 let mut inner = self.lock_inner();
745 let id = inner.next_listener_id;
746 inner.next_listener_id = inner.next_listener_id.saturating_add(1);
747 inner.listeners.push((id, Arc::clone(&listener)));
748 id
749 };
750
751 let session_for_unsub = self.upgrade_self();
752 move || {
753 if let Some(session) = session_for_unsub
754 .as_ref()
755 .and_then(std::sync::Weak::upgrade)
756 {
757 let mut guard = session.lock_inner();
758 guard.listeners.retain(|(id, _)| *id != listener_id);
759 }
760 }
761 }
762
763 pub fn register_event_backpressure_hook(
768 &self,
769 hook: EventBackpressureHook,
770 ) -> Box<dyn Fn() + Send + Sync> {
771 let hook_id = {
772 let mut inner = self.lock_inner();
773 let id = inner.next_backpressure_hook_id;
774 inner.next_backpressure_hook_id = inner.next_backpressure_hook_id.saturating_add(1);
775 inner.backpressure_hooks.push((id, hook));
776 id
777 };
778 let session_for_unsub = self.upgrade_self();
779 Box::new(move || {
780 if let Some(session) = session_for_unsub
781 .as_ref()
782 .and_then(std::sync::Weak::upgrade)
783 {
784 session
785 .lock_inner()
786 .backpressure_hooks
787 .retain(|(id, _)| *id != hook_id);
788 }
789 })
790 }
791
792 pub(super) async fn await_event_backpressure(&self) {
793 let hooks = self
794 .lock_inner()
795 .backpressure_hooks
796 .iter()
797 .map(|(_, hook)| Arc::clone(hook))
798 .collect::<Vec<_>>();
799 for hook in hooks {
800 hook().await;
801 }
802 }
803
804 pub(super) async fn emit_public_awaited(&self, event: &Event) {
805 self.emit_public(event);
806 self.await_event_backpressure().await;
807 }
808
809 pub(super) fn record_session_error(&self, error: SessionError) {
810 let mut inner = self.lock_inner();
811 if inner.pending_session_error.is_none() {
812 inner.pending_session_error = Some(error);
813 }
814 }
815
816 pub(super) fn take_session_error(&self) -> Option<SessionError> {
817 self.lock_inner().pending_session_error.take()
818 }
819
820 pub(super) fn emit_public<E>(&self, event: E)
821 where
822 E: std::borrow::Borrow<Event>,
823 {
824 let event = event.borrow();
825 let listeners = {
826 let inner = self.lock_inner();
827 inner
828 .listeners
829 .iter()
830 .map(|(_, listener)| Arc::clone(listener))
831 .collect::<Vec<_>>()
832 };
833 for listener in listeners {
834 listener(event);
835 }
836 }
837
838 pub(super) fn emit_queue_update(&self) {
840 let (steering, follow_up) = {
841 let inner = self.lock_inner();
842 (
843 inner.steering_messages.clone(),
844 inner.follow_up_messages.clone(),
845 )
846 };
847 self.emit_public(&Event::QueueUpdate {
848 steering,
849 follow_up,
850 });
851 }
852
853 pub(super) fn mirror_steering_push(&self, text: String) {
855 {
856 let mut inner = self.lock_inner();
857 inner.steering_messages.push(text);
858 }
859 self.emit_queue_update();
860 }
861
862 pub(super) fn mirror_follow_up_push(&self, text: String) {
864 {
865 let mut inner = self.lock_inner();
866 inner.follow_up_messages.push(text);
867 }
868 self.emit_queue_update();
869 }
870
871 pub fn clear_queue(&self) {
873 {
874 let mut inner = self.lock_inner();
875 inner.steering_messages.clear();
876 inner.follow_up_messages.clear();
877 }
878 self.agent.clear_queues();
879 self.emit_queue_update();
880 }
881
882 pub(super) fn begin_retry_abort(&self) -> CancellationToken {
888 let token = CancellationToken::new();
889 let mut inner = self.lock_inner();
890 if let Some(prev) = inner.retry_abort.take() {
891 prev.cancel();
892 }
893 inner.retry_abort = Some(token.clone());
894 token
895 }
896
897 pub(super) fn clear_retry_abort(&self) {
899 let mut inner = self.lock_inner();
900 inner.retry_abort = None;
901 }
902
903 pub fn abort_retry(&self) {
905 let mut inner = self.lock_inner();
906 if let Some(token) = inner.retry_abort.take() {
907 token.cancel();
908 }
909 }
910
911 pub(super) fn begin_compaction_abort(&self) -> CancellationToken {
913 let token = CancellationToken::new();
914 let mut inner = self.lock_inner();
915 if let Some(prev) = inner.compaction_abort.take() {
916 prev.cancel();
917 }
918 inner.compaction_abort = Some(token.clone());
919 token
920 }
921
922 pub(super) fn clear_compaction_abort(&self) {
924 self.lock_inner().compaction_abort = None;
925 }
926
927 pub fn abort_compaction(&self) {
929 let mut inner = self.lock_inner();
930 if let Some(token) = inner.compaction_abort.take() {
931 token.cancel();
932 }
933 if let Some(token) = inner.auto_compaction_abort.take() {
934 token.cancel();
935 }
936 }
937
938 pub(super) fn begin_bash_abort(&self) -> CancellationToken {
940 let token = CancellationToken::new();
941 let mut inner = self.lock_inner();
942 if let Some(prev) = inner.bash_abort.take() {
943 prev.cancel();
944 }
945 inner.bash_abort = Some(token.clone());
946 token
947 }
948
949 pub(super) fn clear_bash_abort(&self) {
951 self.lock_inner().bash_abort = None;
952 }
953
954 pub fn abort_bash(&self) {
956 let mut inner = self.lock_inner();
957 if let Some(token) = inner.bash_abort.take() {
958 token.cancel();
959 }
960 }
961
962 pub async fn abort(&self) {
964 self.abort_retry();
965 self.abort_compaction();
966 self.abort_branch_summary();
967 self.abort_bash();
968 self.agent.abort();
969 self.wait_for_idle().await;
970 }
971
972 pub async fn wait_for_idle(&self) {
974 loop {
975 let notified = {
976 let inner = self.lock_inner();
977 if !inner.is_agent_run_active && !self.agent.state().is_streaming {
978 return;
979 }
980 Arc::clone(&inner.idle_notify)
981 };
982 tokio::select! {
984 () = notified.notified() => {}
985 () = self.agent.wait_for_idle() => {
986 let inner = self.lock_inner();
987 if !inner.is_agent_run_active {
988 return;
989 }
990 }
991 }
992 }
993 }
994
995 pub async fn dispose(&self) {
1004 {
1005 let mut inner = self.lock_inner();
1006 if inner.disposed {
1007 return;
1008 }
1009 inner.disposed = true;
1010 if let Some(token) = inner.retry_abort.take() {
1011 token.cancel();
1012 }
1013 if let Some(token) = inner.compaction_abort.take() {
1014 token.cancel();
1015 }
1016 if let Some(token) = inner.auto_compaction_abort.take() {
1017 token.cancel();
1018 }
1019 if let Some(token) = inner.branch_summary_abort.take() {
1020 token.cancel();
1021 }
1022 if let Some(token) = inner.bash_abort.take() {
1023 token.cancel();
1024 }
1025 }
1026 self.disconnect_from_agent();
1027 self.agent.abort();
1028 self.agent.wait_for_idle().await;
1029 self.hooks.runner().invalidate();
1030 let host = {
1032 if let Ok(mut guard) = self.host_extension_runner.write() {
1033 guard.take()
1034 } else {
1035 None
1036 }
1037 };
1038 if let Some(host) = host {
1039 host.shutdown_once().await;
1040 }
1041 }
1042
1043 pub(super) fn lock_inner(&self) -> std::sync::MutexGuard<'_, AgentSessionInner> {
1048 self.inner
1049 .lock()
1050 .unwrap_or_else(std::sync::PoisonError::into_inner)
1051 }
1052
1053 pub fn lock_settings(&self) -> std::sync::MutexGuard<'_, SettingsManager> {
1058 self.settings_manager
1059 .lock()
1060 .unwrap_or_else(std::sync::PoisonError::into_inner)
1061 }
1062
1063 fn store_pump(&self, pump: EventPump) {
1064 let mut inner = self.lock_inner();
1065 if let Some(prev) = inner.pump.take() {
1066 prev.cancel.cancel();
1067 prev.join.abort();
1068 }
1069 inner.pump = Some(pump);
1070 }
1071
1072 fn take_pump(&self) -> Option<EventPump> {
1073 self.lock_inner().pump.take()
1074 }
1075
1076 fn pump_is_active(&self) -> bool {
1077 let inner = self.lock_inner();
1078 inner
1079 .pump
1080 .as_ref()
1081 .is_some_and(|p| p.active.load(std::sync::atomic::Ordering::SeqCst))
1082 }
1083
1084 fn upgrade_self(&self) -> Option<std::sync::Weak<AgentSession>> {
1085 self.self_handle
1086 .lock()
1087 .unwrap_or_else(std::sync::PoisonError::into_inner)
1088 .clone()
1089 }
1090
1091 pub fn set_auto_retry_enabled(&self, enabled: bool) {
1096 self.lock_inner().auto_retry_enabled = enabled;
1097 self.lock_settings().set_retry_enabled(enabled);
1098 }
1099
1100 pub fn set_auto_compaction_enabled(&self, enabled: bool) {
1104 self.lock_inner().auto_compaction_enabled = enabled;
1105 self.lock_settings().set_compaction_enabled(enabled);
1106 }
1107
1108 pub fn set_steering_mode(&self, mode: QueueMode) {
1110 self.agent.set_steering_mode(mode);
1111 }
1112
1113 pub fn set_follow_up_mode(&self, mode: QueueMode) {
1115 self.agent.set_follow_up_mode(mode);
1116 }
1117}
1118
1119#[allow(dead_code)]
1121fn _duration_keep() -> Duration {
1122 Duration::from_millis(0)
1123}
1124
1125#[cfg(test)]
1126mod tests {
1127 use super::*;
1128 use std::sync::atomic::{AtomicUsize, Ordering};
1129 use std::time::Duration;
1130
1131 use futures::stream::{self, BoxStream, StreamExt};
1132 use pi_agent::{AgentEvent, user_text};
1133 use pi_ai::{
1134 AssistantContent, AssistantMessage, AssistantMessageEvent, Context, DoneReason, Model,
1135 ModelCost, ModelInput, Provider, ProviderError, StopReason, StreamOptions, TextContent,
1136 };
1137 use tokio::sync::{Mutex as TokioMutex, mpsc};
1138 use tokio::time::{sleep, timeout};
1139
1140 fn test_model() -> Model {
1141 Model {
1142 id: "m".to_owned(),
1143 name: "m".to_owned(),
1144 api: "test-api".to_owned(),
1145 provider: "test-provider".to_owned(),
1146 base_url: String::new(),
1147 reasoning: false,
1148 thinking_level_map: None,
1149 input: vec![ModelInput::Text],
1150 cost: ModelCost::default(),
1151 context_window: 8_192,
1152 max_tokens: 1_024,
1153 headers: None,
1154 compat: None,
1155 extra: std::collections::BTreeMap::new(),
1156 }
1157 }
1158
1159 fn assistant(text: &str) -> AssistantMessage {
1160 let mut message =
1161 AssistantMessage::new("test-api", "test-provider", "m", pi_agent::now_millis());
1162 message
1163 .content
1164 .push(AssistantContent::Text(TextContent::new(text)));
1165 message.stop_reason = StopReason::Stop;
1166 message
1167 }
1168
1169 fn start_event() -> AssistantMessageEvent {
1170 AssistantMessageEvent::Start {
1171 partial: AssistantMessage::new(
1172 "test-api",
1173 "test-provider",
1174 "m",
1175 pi_agent::now_millis(),
1176 ),
1177 }
1178 }
1179
1180 fn done_event(text: &str) -> AssistantMessageEvent {
1181 AssistantMessageEvent::Done {
1182 reason: DoneReason::Stop,
1183 message: assistant(text),
1184 }
1185 }
1186
1187 #[derive(Clone)]
1188 struct MockProvider(Vec<Result<AssistantMessageEvent, ProviderError>>);
1189
1190 impl Provider for MockProvider {
1191 fn stream(
1192 &self,
1193 _model: &Model,
1194 _context: Context,
1195 _options: StreamOptions,
1196 ) -> BoxStream<'static, Result<AssistantMessageEvent, ProviderError>> {
1197 stream::iter(self.0.clone()).boxed()
1198 }
1199 }
1200
1201 struct RecordingRunner {
1203 order: Arc<TokioMutex<Vec<String>>>,
1204 delay_message_end: Duration,
1205 replace_with: Mutex<Option<AgentMessage>>,
1206 }
1207
1208 impl RecordingRunner {
1209 fn new(order: Arc<TokioMutex<Vec<String>>>) -> Self {
1210 Self {
1211 order,
1212 delay_message_end: Duration::ZERO,
1213 replace_with: Mutex::new(None),
1214 }
1215 }
1216 }
1217
1218 impl ExtensionRunner for RecordingRunner {
1219 fn has_handlers(&self, _event: &str) -> bool {
1220 true
1221 }
1222
1223 fn emit(
1224 &self,
1225 event: AgentSessionEvent,
1226 ) -> futures::future::BoxFuture<'_, Result<Option<CancelResult>, ExtensionRunnerError>>
1227 {
1228 let label = format!("ext:{}", event.type_name());
1229 Box::pin(async move {
1230 self.order.lock().await.push(label);
1231 Ok(None)
1232 })
1233 }
1234
1235 fn emit_message_end(
1236 &self,
1237 message: AgentMessage,
1238 ) -> futures::future::BoxFuture<'_, Result<Option<AgentMessage>, ExtensionRunnerError>>
1239 {
1240 let delay = self.delay_message_end;
1241 Box::pin(async move {
1242 if !delay.is_zero() {
1243 sleep(delay).await;
1244 }
1245 self.order.lock().await.push("ext:message_end".into());
1246 let replacement = self
1247 .replace_with
1248 .lock()
1249 .unwrap_or_else(std::sync::PoisonError::into_inner)
1250 .clone();
1251 Ok(replacement.or(Some(message)))
1252 })
1253 }
1254
1255 fn emit_tool_call(
1256 &self,
1257 _tool_name: &str,
1258 _tool_call_id: &str,
1259 _input: serde_json::Map<String, serde_json::Value>,
1260 ) -> futures::future::BoxFuture<
1261 '_,
1262 Result<Option<pi_agent::BeforeToolCallResult>, ExtensionRunnerError>,
1263 > {
1264 Box::pin(async { Ok(None) })
1265 }
1266
1267 fn emit_tool_result(
1268 &self,
1269 _tool_name: &str,
1270 _tool_call_id: &str,
1271 _input: serde_json::Map<String, serde_json::Value>,
1272 _content: Vec<pi_ai::ToolResultContent>,
1273 _details: serde_json::Value,
1274 _is_error: bool,
1275 ) -> futures::future::BoxFuture<
1276 '_,
1277 Result<Option<pi_agent::AfterToolCallResult>, ExtensionRunnerError>,
1278 > {
1279 Box::pin(async { Ok(None) })
1280 }
1281
1282 fn emit_input(
1283 &self,
1284 _text: &str,
1285 _images: Option<serde_json::Value>,
1286 _source: &str,
1287 _streaming_behavior: Option<&str>,
1288 ) -> futures::future::BoxFuture<'_, Result<InputTransformResult, ExtensionRunnerError>>
1289 {
1290 Box::pin(async { Ok(InputTransformResult::default()) })
1291 }
1292
1293 fn emit_before_agent_start(
1294 &self,
1295 _prompt: &str,
1296 _images: Option<serde_json::Value>,
1297 ) -> futures::future::BoxFuture<
1298 '_,
1299 Result<Option<BeforeAgentStartResult>, ExtensionRunnerError>,
1300 > {
1301 Box::pin(async { Ok(None) })
1302 }
1303
1304 fn emit_resources_discover(
1305 &self,
1306 _cwd: &str,
1307 _reason: &str,
1308 ) -> futures::future::BoxFuture<
1309 '_,
1310 Result<crate::core::resources::ResourceExtensionPaths, ExtensionRunnerError>,
1311 > {
1312 Box::pin(async { Ok(crate::core::resources::ResourceExtensionPaths::default()) })
1313 }
1314
1315 fn execute_command<'a>(
1316 &'a self,
1317 _name: &'a str,
1318 _args: &'a str,
1319 ) -> futures::future::BoxFuture<'a, Result<bool, ExtensionRunnerError>> {
1320 Box::pin(async { Ok(false) })
1321 }
1322
1323 fn get_registered_commands(&self) -> Vec<String> {
1324 Vec::new()
1325 }
1326
1327 fn get_all_registered_tools(
1328 &self,
1329 ) -> std::collections::HashMap<String, Arc<dyn AgentTool>> {
1330 std::collections::HashMap::new()
1331 }
1332
1333 fn get_flag_values(&self) -> std::collections::HashMap<String, serde_json::Value> {
1334 std::collections::HashMap::new()
1335 }
1336
1337 fn invalidate(&self) {}
1338
1339 fn emit_error(&self, _message: String) {}
1340 }
1341
1342 async fn collect_types(rx: &mut mpsc::UnboundedReceiver<String>, n: usize) -> Vec<String> {
1343 let mut out = Vec::new();
1344 while out.len() < n {
1345 match timeout(Duration::from_secs(2), rx.recv()).await {
1346 Ok(Some(v)) => out.push(v),
1347 _ => break,
1348 }
1349 }
1350 out
1351 }
1352
1353 #[tokio::test]
1354 async fn admission_active_precedes_agent_streaming() -> Result<(), Box<dyn std::error::Error>> {
1355 let provider = Arc::new(MockProvider(Vec::new()));
1356 let session = AgentSession::new(AgentSessionConfig::test_config(provider, test_model())?)?;
1357
1358 assert!(!session.agent.state().is_streaming);
1359 assert!(!session.is_admission_active());
1360 session.mark_agent_run_active();
1361 assert!(!session.agent.state().is_streaming);
1362 assert!(session.is_admission_active());
1363 Ok(())
1364 }
1365
1366 #[tokio::test]
1367 async fn single_prompt_event_order() -> Result<(), Box<dyn std::error::Error>> {
1368 let provider = Arc::new(MockProvider(vec![
1369 Ok(start_event()),
1370 Ok(done_event("hello")),
1371 ]));
1372 let mut config = AgentSessionConfig::test_config(provider, test_model())?;
1373 config.system_prompt = "sys".into();
1374 let session = AgentSession::new(config)?;
1375
1376 let (tx, mut rx) = mpsc::unbounded_channel();
1377 let _unsub = session.subscribe(move |event| {
1378 let _ = tx.send(event.type_name().to_owned());
1379 });
1380
1381 session.mark_agent_run_active();
1382 session
1383 .agent
1384 .prompt(vec![user_text("hi", std::iter::empty())])
1385 .await?;
1386 session.agent.wait_for_idle().await;
1387 sleep(Duration::from_millis(50)).await;
1390 session.emit_agent_settled().await;
1391
1392 let types = collect_types(&mut rx, 16).await;
1393 assert!(
1396 types.iter().any(|t| t == "agent_start"),
1397 "missing agent_start in {types:?}"
1398 );
1399 assert!(
1400 types.iter().any(|t| t == "agent_end"),
1401 "missing agent_end in {types:?}"
1402 );
1403 assert_eq!(
1404 types.iter().filter(|t| *t == "agent_settled").count(),
1405 1,
1406 "exactly one agent_settled: {types:?}"
1407 );
1408 let start = types
1409 .iter()
1410 .position(|t| t == "agent_start")
1411 .ok_or_else(|| std::io::Error::other("missing agent_start"))?;
1412 let end = types
1413 .iter()
1414 .position(|t| t == "agent_end")
1415 .ok_or_else(|| std::io::Error::other("missing agent_end"))?;
1416 let settled = types
1417 .iter()
1418 .position(|t| t == "agent_settled")
1419 .ok_or_else(|| std::io::Error::other("missing agent_settled"))?;
1420 assert!(start < end && end < settled, "order {types:?}");
1421 Ok(())
1422 }
1423
1424 #[tokio::test]
1425 async fn extension_before_public_ordering() -> Result<(), Box<dyn std::error::Error>> {
1426 let order = Arc::new(TokioMutex::new(Vec::new()));
1427 let runner = Arc::new(RecordingRunner::new(Arc::clone(&order)));
1428 let provider = Arc::new(MockProvider(vec![Ok(start_event()), Ok(done_event("ok"))]));
1429 let mut config = AgentSessionConfig::test_config(provider, test_model())?;
1430 config.extension_runner = Some(runner);
1431 let session = AgentSession::new(config)?;
1432
1433 let public_order = Arc::clone(&order);
1434 let _unsub = session.subscribe(move |event| {
1435 let label = format!("pub:{}", event.type_name());
1436 if let Ok(mut g) = public_order.try_lock() {
1438 g.push(label);
1439 } else {
1440 let order = Arc::clone(&public_order);
1441 let label = label.clone();
1442 tokio::spawn(async move {
1443 order.lock().await.push(label);
1444 });
1445 }
1446 });
1447
1448 session.mark_agent_run_active();
1449 session
1450 .agent
1451 .prompt(vec![user_text("hi", std::iter::empty())])
1452 .await?;
1453 session.agent.wait_for_idle().await;
1454 sleep(Duration::from_millis(50)).await;
1456
1457 let recorded = order.lock().await.clone();
1458 let ext_start = recorded.iter().position(|s| s == "ext:agent_start");
1461 let pub_start = recorded.iter().position(|s| s == "pub:agent_start");
1462 if let (Some(e), Some(p)) = (ext_start, pub_start) {
1463 assert!(e < p, "extension before public: {recorded:?}");
1464 }
1465 Ok(())
1466 }
1467
1468 #[tokio::test]
1469 async fn queue_update_before_message_start_public() -> Result<(), Box<dyn std::error::Error>> {
1470 let provider = Arc::new(MockProvider(vec![
1471 Ok(start_event()),
1472 Ok(done_event("ok")),
1473 Ok(start_event()),
1474 Ok(done_event("ok2")),
1475 ]));
1476 let session = AgentSession::new(AgentSessionConfig::test_config(provider, test_model())?)?;
1477
1478 let (tx, _rx) = mpsc::unbounded_channel::<String>();
1479 let pending_at_user_start = Arc::new(AtomicUsize::new(usize::MAX));
1480 let pending_flag = Arc::clone(&pending_at_user_start);
1481 let session_for_count = Arc::clone(&session);
1482 let _unsub = session.subscribe(move |event| match event {
1483 AgentSessionEvent::MessageStart { message } if message.role() == "user" => {
1484 pending_flag.store(session_for_count.pending_message_count(), Ordering::SeqCst);
1485 let _ = tx.send("message_start:user".into());
1486 }
1487 AgentSessionEvent::QueueUpdate { .. } => {
1488 let _ = tx.send("queue_update".into());
1489 }
1490 _ => {}
1491 });
1492
1493 session.mark_agent_run_active();
1495 session
1496 .agent
1497 .prompt(vec![user_text("first", std::iter::empty())])
1498 .await?;
1499 session.agent.wait_for_idle().await;
1500
1501 session.mirror_steering_push("steer-text".into());
1502 session
1503 .agent
1504 .steer(user_text("steer-text", std::iter::empty()));
1505 assert_eq!(session.pending_message_count(), 1);
1506
1507 session.agent.continue_run().await?;
1508 session.agent.wait_for_idle().await;
1509 sleep(Duration::from_millis(50)).await;
1510
1511 let pending = pending_at_user_start.load(Ordering::SeqCst);
1513 assert_eq!(
1514 pending, 0,
1515 "pending must be 0 at message_start:user, got {pending}"
1516 );
1517 Ok(())
1518 }
1519
1520 #[tokio::test]
1521 async fn message_end_replacement_updates_live_and_persists()
1522 -> Result<(), Box<dyn std::error::Error>> {
1523 let order = Arc::new(TokioMutex::new(Vec::new()));
1524 let runner = Arc::new(RecordingRunner {
1525 order: Arc::clone(&order),
1526 delay_message_end: Duration::ZERO,
1527 replace_with: Mutex::new(None),
1528 });
1529
1530 let mut replaced = assistant("replaced");
1532 replaced.stop_reason = StopReason::Stop;
1533 *runner
1534 .replace_with
1535 .lock()
1536 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(AgentMessage::Llm(
1537 Box::new(pi_ai::Message::Assistant(replaced)),
1538 ));
1539
1540 let provider = Arc::new(MockProvider(vec![
1541 Ok(start_event()),
1542 Ok(done_event("original")),
1543 ]));
1544 let mut config = AgentSessionConfig::test_config(provider, test_model())?;
1545 config.extension_runner = Some(runner);
1546 let session = AgentSession::new(config)?;
1547
1548 session.mark_agent_run_active();
1549 session
1550 .agent
1551 .prompt(vec![user_text("hi", std::iter::empty())])
1552 .await?;
1553 session.agent.wait_for_idle().await;
1554 sleep(Duration::from_millis(50)).await;
1555
1556 let last = session.agent.last_assistant();
1558 assert!(last.is_some(), "expected last assistant");
1559 let text = last
1560 .as_ref()
1561 .and_then(|m| {
1562 m.content.iter().find_map(|c| match c {
1563 AssistantContent::Text(t) => Some(t.text.to_string()),
1564 _ => None,
1565 })
1566 })
1567 .unwrap_or_default();
1568 assert_eq!(text, "replaced");
1569
1570 let sm = session.session_manager.lock().await;
1572 let entries = sm.get_entries();
1573 let encoded = serde_json::to_string(&entries).unwrap_or_default();
1574 assert!(
1575 encoded.contains("replaced"),
1576 "persisted entries should contain replacement: {encoded}"
1577 );
1578 Ok(())
1579 }
1580
1581 #[tokio::test]
1582 async fn single_settled_across_simulated_continuation() -> Result<(), Box<dyn std::error::Error>>
1583 {
1584 let provider = Arc::new(MockProvider(vec![
1585 Ok(start_event()),
1586 Ok(done_event("a")),
1587 Ok(start_event()),
1588 Ok(done_event("b")),
1589 ]));
1590 let session = AgentSession::new(AgentSessionConfig::test_config(provider, test_model())?)?;
1591
1592 let settled = Arc::new(AtomicUsize::new(0));
1593 let settled_c = Arc::clone(&settled);
1594 let _unsub = session.subscribe(move |event| {
1595 if matches!(event, AgentSessionEvent::AgentSettled) {
1596 settled_c.fetch_add(1, Ordering::SeqCst);
1597 }
1598 });
1599
1600 session.mark_agent_run_active();
1601 session
1602 .agent
1603 .prompt(vec![user_text("hi", std::iter::empty())])
1604 .await?;
1605 session.agent.wait_for_idle().await;
1606
1607 session
1609 .agent
1610 .prompt(vec![user_text("again", std::iter::empty())])
1611 .await?;
1612 session.agent.wait_for_idle().await;
1613
1614 session.emit_agent_settled().await;
1616 session.emit_agent_settled().await;
1618
1619 assert_eq!(settled.load(Ordering::SeqCst), 1);
1620 Ok(())
1621 }
1622
1623 #[tokio::test]
1624 async fn slow_extension_does_not_reorder() -> Result<(), Box<dyn std::error::Error>> {
1625 let order = Arc::new(TokioMutex::new(Vec::new()));
1626 let runner = Arc::new(RecordingRunner {
1627 order: Arc::clone(&order),
1628 delay_message_end: Duration::from_millis(30),
1629 replace_with: Mutex::new(None),
1630 });
1631 let provider = Arc::new(MockProvider(vec![Ok(start_event()), Ok(done_event("ok"))]));
1632 let mut config = AgentSessionConfig::test_config(provider, test_model())?;
1633 config.extension_runner = Some(runner);
1634 let session = AgentSession::new(config)?;
1635
1636 let (tx, mut rx) = mpsc::unbounded_channel();
1637 let _unsub = session.subscribe(move |event| {
1638 let _ = tx.send(event.type_name().to_owned());
1639 });
1640
1641 session.mark_agent_run_active();
1642 session
1643 .agent
1644 .prompt(vec![user_text("hi", std::iter::empty())])
1645 .await?;
1646 session.agent.wait_for_idle().await;
1647 sleep(Duration::from_millis(80)).await;
1648
1649 let types = collect_types(&mut rx, 16).await;
1650 let user_end = types
1652 .iter()
1653 .enumerate()
1654 .filter(|(_, t)| *t == "message_end")
1655 .map(|(i, _)| i)
1656 .collect::<Vec<_>>();
1657 assert!(
1658 user_end.len() >= 2,
1659 "expected user+assistant message_end: {types:?}"
1660 );
1661 assert!(user_end[0] < user_end[1], "order preserved: {types:?}");
1662 Ok(())
1663 }
1664
1665 #[tokio::test]
1666 async fn listener_unsubscribe() -> Result<(), Box<dyn std::error::Error>> {
1667 let provider = Arc::new(MockProvider(vec![Ok(start_event()), Ok(done_event("ok"))]));
1668 let session = AgentSession::new(AgentSessionConfig::test_config(provider, test_model())?)?;
1669
1670 let count = Arc::new(AtomicUsize::new(0));
1671 let c1 = Arc::clone(&count);
1672 let unsub = session.subscribe(move |_e| {
1673 c1.fetch_add(1, Ordering::SeqCst);
1674 });
1675 unsub();
1676
1677 session.mark_agent_run_active();
1678 session
1679 .agent
1680 .prompt(vec![user_text("hi", std::iter::empty())])
1681 .await?;
1682 session.agent.wait_for_idle().await;
1683 sleep(Duration::from_millis(30)).await;
1684
1685 assert_eq!(
1686 count.load(Ordering::SeqCst),
1687 0,
1688 "unsubscribed listener silent"
1689 );
1690 Ok(())
1691 }
1692
1693 #[tokio::test]
1694 async fn dispose_cancels_pump_without_emitting_shutdown()
1695 -> Result<(), Box<dyn std::error::Error>> {
1696 let order = Arc::new(TokioMutex::new(Vec::new()));
1697 let runner = Arc::new(RecordingRunner::new(Arc::clone(&order)));
1698 let provider = Arc::new(MockProvider(vec![Ok(start_event()), Ok(done_event("ok"))]));
1699 let mut config = AgentSessionConfig::test_config(provider, test_model())?;
1700 config.extension_runner = Some(runner);
1701 let session = AgentSession::new(config)?;
1702 assert!(session.pump_is_active());
1703 session.dispose().await;
1704 sleep(Duration::from_millis(20)).await;
1705 assert!(!session.pump_is_active());
1706 assert!(
1707 order
1708 .lock()
1709 .await
1710 .iter()
1711 .all(|entry| !entry.starts_with("shutdown:")),
1712 "runtime teardown owns the single reason-specific shutdown event"
1713 );
1714 Ok(())
1715 }
1716
1717 #[cfg(unix)]
1718 #[tokio::test]
1719 async fn abort_stops_running_bash() -> Result<(), Box<dyn std::error::Error>> {
1720 let provider = Arc::new(MockProvider(Vec::new()));
1721 let session = AgentSession::new(AgentSessionConfig::test_config(provider, test_model())?)?;
1722 let running = tokio::spawn({
1723 let session = Arc::clone(&session);
1724 async move {
1725 session
1726 .execute_bash(
1727 "sleep 30",
1728 None::<fn(&str)>,
1729 super::bash::ExecuteBashOptions::default(),
1730 )
1731 .await
1732 }
1733 });
1734 for _ in 0..100 {
1735 if session.is_bash_running() {
1736 break;
1737 }
1738 tokio::time::sleep(Duration::from_millis(5)).await;
1739 }
1740 assert!(session.is_bash_running());
1741
1742 session.abort().await;
1743 let _ = tokio::time::timeout(Duration::from_secs(2), running).await??;
1744 assert!(!session.is_bash_running());
1745 Ok(())
1746 }
1747
1748 #[tokio::test]
1749 async fn tool_turn_order_includes_tool_events() -> Result<(), Box<dyn std::error::Error>> {
1750 let event = AgentSessionEvent::from_agent_event(
1755 AgentEvent::ToolExecutionStart {
1756 tool_call_id: "1".into(),
1757 tool_name: "read".into(),
1758 args: serde_json::Map::new(),
1759 },
1760 false,
1761 );
1762 assert_eq!(event.type_name(), "tool_execution_start");
1763 let encoded = serde_json::to_value(&event)?;
1764 assert_eq!(encoded["toolCallId"], "1");
1765 assert_eq!(encoded["toolName"], "read");
1766 Ok(())
1767 }
1768}