Skip to main content

pi/core/agent_session/
mod.rs

1//! `AgentSession` — mode-agnostic orchestration over `pi-agent` + product services.
2//!
3//! This module owns the event/persistence foundation:
4//! - [`AgentSession`] / [`AgentSessionConfig`] / [`AgentSessionInner`]
5//! - raw-serde [`AgentSessionEvent`] superset
6//! - [`ExtensionRunner`] seam + [`NullExtensionRunner`]
7//! - [`SessionHooks`] shared with agent tool / next-turn closures
8//! - exactly one lossless event pump (extension-before-public)
9//!
10//! Sibling modules (prompt, retry, compaction, model, tools, bash, tree,
11//! extension) add `impl AgentSession` blocks in later slices. Only
12//! `pub(super)` invariants needed by those slices are exposed here.
13//!
14//! # Lock order
15//!
16//! Never hold a sync lock across `.await`. Never nest locks out of order:
17//!
18//! 0. `bind_lock` (`tokio::sync::Mutex`) — serializes the entire
19//!    `bind_extensions` lifecycle; held across `.await`. Acquire before any
20//!    `lock_inner()`.
21//! 1. `AgentSessionInner` (`std::sync::Mutex`) — flags, mirror queues, retry
22//!    counters, listener list, pump handle, cancellation slots.
23//! 2. `session_manager` (`tokio::sync::Mutex<SessionManager>`) — single-writer
24//!    async mutex; event pump and public mutators share it. Documented as the
25//!    sole writer of the append-only session tree for this session.
26//! 3. `SessionHooks` `RwLocks` (`runner` → `system_prompt` → `tools`) — only one
27//!    at a time, never nested with (1) or (2).
28//!
29//! Public listeners are invoked without holding any lock.
30
31pub 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/// Optional model + thinking pair for `--models` scoped cycling.
75#[derive(Clone, Debug)]
76pub struct ScopedModel {
77    /// Model entry.
78    pub model: Model,
79    /// Optional thinking level override for this model.
80    pub thinking_level: Option<ModelThinkingLevel>,
81}
82
83/// Construction inputs for [`AgentSession`].
84///
85/// The services factory builds this and passes it to [`AgentSession::new`].
86/// Product dependencies remain concrete: `model_runtime` is a typed handle,
87/// while compaction test overrides use their own typed seam.
88pub struct AgentSessionConfig {
89    /// Pre-built agent. When `None`, [`AgentSession::new`] builds one from
90    /// `provider` + defaults and installs `SessionHooks` closures.
91    pub agent: Option<Agent>,
92    /// Provider used when `agent` is `None`.
93    pub provider: Option<Arc<dyn Provider>>,
94    /// Session persistence manager (moved into an async mutex).
95    pub session_manager: SessionManager,
96    /// Settings manager (owned; later slices mutate retry/compaction flags).
97    pub settings_manager: SettingsManager,
98    /// Working directory.
99    pub cwd: String,
100    /// Scoped models from `--models`.
101    pub scoped_models: Vec<ScopedModel>,
102    /// Initial active built-in tool names.
103    pub initial_active_tool_names: Option<Vec<String>>,
104    /// Optional tool allowlist.
105    pub allowed_tool_names: Option<Vec<String>>,
106    /// Optional tool denylist.
107    pub excluded_tool_names: Option<Vec<String>>,
108    /// Initial tools installed on the agent when building from provider.
109    pub tools: Vec<Arc<dyn AgentTool>>,
110    /// Initial system prompt.
111    pub system_prompt: String,
112    /// Initial model (when building agent).
113    pub model: Option<Model>,
114    /// Initial thinking level.
115    pub thinking_level: ModelThinkingLevel,
116    /// Initial transcript messages.
117    pub messages: Vec<AgentMessage>,
118    /// Extension runner (defaults to [`NullExtensionRunner`]).
119    pub extension_runner: Option<Arc<dyn ExtensionRunner>>,
120    /// Concrete host runner retained for reload/restart (no trait downcast).
121    pub host_extension_runner: Option<Arc<crate::core::extension_host::HostExtensionRunner>>,
122    /// Typed model/auth runtime used by model selection and compaction.
123    pub model_runtime: Option<Arc<ModelRuntime>>,
124    /// Optional compaction stream override for tests and headless integrations.
125    pub compaction_stream_override: Option<compaction::CompactionStreamHandle>,
126    /// Skills for `/skill:name` expansion (populated by resources slice).
127    pub skills: Vec<crate::core::resources::skills::Skill>,
128    /// Prompt templates for `/template` expansion.
129    pub prompt_templates: Vec<crate::core::resources::prompts::PromptTemplate>,
130    /// Resource loader retained for extension-driven resource refreshes.
131    pub resource_loader: Option<crate::core::resources::DefaultResourceLoader>,
132    /// Session-start metadata emitted to extensions on first bind
133    /// (`None` = default startup).
134    pub session_start_event: Option<SessionStartEvent>,
135    /// Base agent loop config overrides (hooks are always installed by session).
136    pub base_config: Option<AgentLoopConfig>,
137}
138
139impl AgentSessionConfig {
140    /// Minimal config for tests: in-memory session + null extensions.
141    ///
142    /// `SessionManager::in_memory` with no custom id only fails if validation
143    /// is introduced later; the `None` options path is the documented default.
144    ///
145    /// # Errors
146    ///
147    /// Returns [`AgentSessionError::Session`] if both in-memory session
148    /// construction attempts fail.
149    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
189/// Mutable session state shared by the event pump and public methods.
190///
191/// Guarded by `std::sync::Mutex`. Never hold across `.await`.
192pub(super) struct AgentSessionInner {
193    /// Session lifecycle and automatic-action flags.
194    lifecycle: SessionLifecycle,
195    /// Pending steering message texts for UI (mirror of agent queue).
196    pub(super) steering_messages: Vec<String>,
197    /// Pending follow-up message texts for UI.
198    pub(super) follow_up_messages: Vec<String>,
199    /// Current auto-retry attempt (0 = not retrying).
200    pub(super) retry_attempt: u32,
201    /// Max retries from settings (cached for `will_retry` checks).
202    pub(super) max_retries: u32,
203    /// Last assistant message observed on `message_end`.
204    pub(super) last_assistant_message: Option<AssistantMessage>,
205    /// Public event listeners with stable ids for unsubscribe.
206    pub(super) listeners: Vec<(u64, AgentSessionEventListener)>,
207    /// Monotonic listener id allocator.
208    pub(super) next_listener_id: u64,
209    /// Awaited event backpressure hooks with stable ids.
210    pub(super) backpressure_hooks: Vec<(u64, EventBackpressureHook)>,
211    /// Monotonic backpressure-hook id allocator.
212    pub(super) next_backpressure_hook_id: u64,
213    /// Typed persistence failure awaiting prompt completion.
214    pub(super) pending_session_error: Option<SessionError>,
215    /// Active event pump (at most one), encapsulated within this module.
216    pump: Option<EventPump>,
217    /// Idle waiters for session-level idle.
218    pub(super) idle_notify: Arc<Notify>,
219    /// Completed `agent_end` events processed by the session event pump.
220    pub(super) processed_agent_ends: u64,
221    /// Wakes prompt lifecycle barriers after a complete `agent_end`.
222    pub(super) agent_end_notify: Arc<Notify>,
223    /// Cancels prompt lifecycle barriers when the event pump disconnects.
224    pub(super) agent_end_wait_cancel: CancellationToken,
225    /// Scoped models list.
226    pub(super) scoped_models: Vec<ScopedModel>,
227    /// Active tool names.
228    pub(super) active_tool_names: Vec<String>,
229    /// Base system prompt (mirrored into `SessionHooks`).
230    pub(super) base_system_prompt: String,
231    /// Cancellation: retry sleep.
232    pub(super) retry_abort: Option<CancellationToken>,
233    /// Cancellation: manual compaction.
234    pub(super) compaction_abort: Option<CancellationToken>,
235    /// Cancellation: auto compaction.
236    pub(super) auto_compaction_abort: Option<CancellationToken>,
237    /// Cancellation: branch summary.
238    pub(super) branch_summary_abort: Option<CancellationToken>,
239    /// Cancellation: bash execution.
240    pub(super) bash_abort: Option<CancellationToken>,
241    /// Pending nextTurn custom messages injected into the next prompt.
242    pub(super) pending_next_turn_messages: Vec<AgentMessage>,
243    /// Bound extension mode (set by `bind_extensions`).
244    pub(super) extension_mode: Option<crate::core::agent_session::extension::ExtensionMode>,
245    /// Bound extension UI context tag (interactive mode only).
246    pub(super) extension_ui_tag: Option<String>,
247    /// Bound extension shutdown handler.
248    pub(super) extension_shutdown_handler: Option<std::sync::Arc<dyn Fn() + Send + Sync>>,
249    /// Bound extension error listener.
250    pub(super) extension_error_listener: Option<ExtensionErrorListener>,
251    /// Bound extension command-context actions (opaque JSON).
252    pub(super) extension_command_context: Option<serde_json::Value>,
253    /// Session-start event stored at construction, consumed by the first
254    /// `bind_extensions` call (take-guard against duplicate emission).
255    pub(super) pending_session_start: Option<SessionStartEvent>,
256    /// Whether bind-time startup resource discovery already ran
257    /// (same-session rediscovery guard).
258    pub(super) initial_resources_discovered: bool,
259    /// Base built-in tool definitions (insertion-ordered, first-wins on dupes).
260    pub(super) base_tool_definitions: Vec<std::sync::Arc<dyn AgentTool>>,
261    /// Active tool registry (built-in + extension + custom, insertion-ordered).
262    pub(super) tool_registry: Vec<std::sync::Arc<dyn AgentTool>>,
263    /// Optional tool allowlist.
264    pub(super) allowed_tool_names: Option<std::collections::HashSet<String>>,
265    /// Optional tool denylist.
266    pub(super) excluded_tool_names: Option<std::collections::HashSet<String>>,
267    /// Pending bash messages awaiting flush after `agent_end`.
268    pub(super) pending_bash_messages: Vec<crate::core::messages::BashExecutionMessage>,
269    /// Test-only reload restart factory. When set, [`AgentSession::reload`]
270    /// uses it instead of the production host spawn path so cutover can be
271    /// exercised against in-memory transports.
272    #[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
333/// Awaited barrier invoked after a public event has reached synchronous listeners.
334pub 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
382/// Mode-agnostic agent session.
383///
384/// Not `Clone`. Modes hold it behind their own reference (`Arc` at the runtime
385/// layer if needed). Interior mutability covers pump/listener/queue state.
386pub struct AgentSession {
387    /// Underlying agent turn loop.
388    pub agent: Agent,
389    /// Session tree (single-writer async mutex).
390    pub(super) session_manager: Arc<AsyncMutex<SessionManager>>,
391    /// Serializes pending-bash flushes without owning queue data or nesting locks.
392    pub(super) bash_flush_lock: AsyncMutex<()>,
393    /// Settings manager (interior-mutable so every accessor / mutator on
394    /// `AgentSession` can operate through `&self`). Lock briefly and drop
395    /// before any `.await` — see [`AgentSession::lock_settings`].
396    pub(super) settings_manager: std::sync::Mutex<SettingsManager>,
397    /// Working directory.
398    pub cwd: String,
399    /// Shared hooks for agent closures + extension runner.
400    pub(super) hooks: Arc<SessionHooks>,
401    /// Mutable inner state.
402    pub(super) inner: Mutex<AgentSessionInner>,
403    /// Concrete host runner for reload (optional; no trait downcast).
404    pub(super) host_extension_runner:
405        std::sync::RwLock<Option<Arc<crate::core::extension_host::HostExtensionRunner>>>,
406    /// Typed model runtime shared across product-owned session boundaries.
407    pub(super) model_runtime: Option<Arc<ModelRuntime>>,
408    /// Optional compaction-only stream override.
409    pub(super) compaction_stream_override: Option<compaction::CompactionStreamHandle>,
410    /// Skills for `/skill:name` expansion.
411    pub(super) skills: Mutex<Vec<crate::core::resources::skills::Skill>>,
412    /// Prompt templates for `/template` expansion.
413    pub(super) prompt_templates: Mutex<Vec<crate::core::resources::prompts::PromptTemplate>>,
414    /// Resource loader for extension-discovered skills, prompts, and themes.
415    pub(super) resource_loader: Option<AsyncMutex<crate::core::resources::DefaultResourceLoader>>,
416    /// Self handle for pump (set after construction).
417    pub(super) self_handle: Mutex<Option<std::sync::Weak<AgentSession>>>,
418    /// Serializes the whole `bind_extensions` lifecycle (record → emit →
419    /// discover). Lives on the session (not `AgentSessionInner`) because it
420    /// is held across `.await`; acquire it before any `lock_inner()`, never
421    /// hold `lock_inner` across an await.
422    pub(super) bind_lock: AsyncMutex<()>,
423}
424
425/// Errors from [`AgentSession::new`].
426#[derive(Debug, thiserror::Error)]
427pub enum AgentSessionError {
428    /// Missing both a pre-built agent and a provider.
429    #[error("AgentSessionConfig requires `agent` or `provider`")]
430    MissingAgentOrProvider,
431    /// Session manager error during setup.
432    #[error(transparent)]
433    Session(#[from] crate::core::sessions::SessionError),
434}
435
436impl AgentSession {
437    /// Construct a session, install hooks, and spawn the event pump.
438    ///
439    /// # Errors
440    ///
441    /// Returns [`AgentSessionError::MissingAgentOrProvider`] when neither an
442    /// agent nor a provider is supplied.
443    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        // Build the initial tool registry from configured base tools and active
533        // names. Extension tools will be picked up on the first reload.
534        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        // Store weak self and spawn pump.
542        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    // -------------------------------------------------------------------------
552    // Accessors (public surface for modes / later slices)
553    // -------------------------------------------------------------------------
554
555    /// Underlying agent.
556    #[must_use]
557    pub fn agent(&self) -> &Agent {
558        &self.agent
559    }
560
561    /// Extension runner snapshot.
562    #[must_use]
563    pub fn extension_runner(&self) -> Arc<dyn ExtensionRunner> {
564        self.hooks.runner()
565    }
566
567    /// [`SessionHooks`] handle (for reload / sibling modules).
568    #[must_use]
569    pub fn hooks(&self) -> Arc<SessionHooks> {
570        Arc::clone(&self.hooks)
571    }
572
573    /// Session manager async mutex (single-writer).
574    #[must_use]
575    pub fn session_manager(&self) -> Arc<AsyncMutex<SessionManager>> {
576        Arc::clone(&self.session_manager)
577    }
578
579    /// Current model from agent state.
580    #[must_use]
581    pub fn model(&self) -> Model {
582        self.agent.state().model
583    }
584
585    /// Current thinking level.
586    #[must_use]
587    pub fn thinking_level(&self) -> ModelThinkingLevel {
588        self.agent.state().thinking_level
589    }
590
591    /// Whether a session run has been admitted, including preflight,
592    /// retry, compaction, and post-run continuation phases.
593    #[must_use]
594    pub fn is_admission_active(&self) -> bool {
595        self.lock_inner().is_agent_run_active
596    }
597
598    /// Whether the agent is currently streaming.
599    #[must_use]
600    pub fn is_streaming(&self) -> bool {
601        self.agent.state().is_streaming
602    }
603
604    /// Whether the agent has no active run.
605    #[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    /// Whether session-level auto-compaction is in progress.
612    #[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    /// Whether auto-retry sleep is in progress.
619    #[must_use]
620    pub fn is_retrying(&self) -> bool {
621        self.lock_inner().retry_abort.is_some()
622    }
623
624    /// Whether bash is running.
625    #[must_use]
626    pub fn is_bash_running(&self) -> bool {
627        self.lock_inner().bash_abort.is_some()
628    }
629    /// Whether branch summarization is in progress.
630    #[must_use]
631    pub fn is_summarizing(&self) -> bool {
632        self.lock_inner().branch_summary_abort.is_some()
633    }
634
635    /// Session file path, if any.
636    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    /// Session id.
645    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    /// Session display name.
654    pub async fn session_name(&self) -> Option<String> {
655        self.session_manager.lock().await.get_session_name()
656    }
657
658    /// Scoped models list.
659    #[must_use]
660    pub fn scoped_models(&self) -> Vec<ScopedModel> {
661        self.lock_inner().scoped_models.clone()
662    }
663
664    /// Pending steering + follow-up count.
665    #[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    /// Pending steering and follow-up message mirrors.
674    #[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    /// Active tool names.
684    #[must_use]
685    pub fn active_tool_names(&self) -> Vec<String> {
686        self.lock_inner().active_tool_names.clone()
687    }
688
689    /// Transcript message count.
690    #[must_use]
691    pub fn message_count(&self) -> usize {
692        self.agent.transcript().len()
693    }
694
695    /// Clone of current transcript.
696    #[must_use]
697    pub fn messages(&self) -> Vec<AgentMessage> {
698        self.agent.transcript()
699    }
700
701    /// Steering queue mode.
702    #[must_use]
703    pub fn steering_mode(&self) -> QueueMode {
704        self.agent.steering_mode()
705    }
706
707    /// Follow-up queue mode.
708    #[must_use]
709    pub fn follow_up_mode(&self) -> QueueMode {
710        self.agent.follow_up_mode()
711    }
712
713    /// Auto-compaction enabled flag.
714    #[must_use]
715    pub fn auto_compaction_enabled(&self) -> bool {
716        self.lock_inner().auto_compaction_enabled
717    }
718
719    /// Auto-retry enabled flag.
720    #[must_use]
721    pub fn auto_retry_enabled(&self) -> bool {
722        self.lock_inner().auto_retry_enabled
723    }
724
725    /// Typed model-runtime handle.
726    #[must_use]
727    pub fn model_runtime_handle(&self) -> Option<Arc<ModelRuntime>> {
728        self.model_runtime.clone()
729    }
730
731    // -------------------------------------------------------------------------
732    // Subscribe / emit
733    // -------------------------------------------------------------------------
734
735    /// Subscribe to public session events. Returns an unsubscribe token.
736    ///
737    /// Listeners are invoked without holding the inner mutex.
738    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    /// Register an awaited event-production barrier.
764    ///
765    /// The event pump and compaction paths invoke these hooks after synchronous
766    /// public listeners. The returned closure removes the hook by stable id.
767    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    /// Emit a `queue_update` snapshot from current mirrors.
839    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    /// Push a steering mirror entry and emit `queue_update`.
854    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    /// Push a follow-up mirror entry and emit `queue_update`.
863    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    /// Clear both mirror queues and emit `queue_update`.
872    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    // -------------------------------------------------------------------------
883    // Cancellation slots
884    // -------------------------------------------------------------------------
885
886    /// Replace the retry abort token; returns the new token.
887    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    /// Clear the retry abort token.
898    pub(super) fn clear_retry_abort(&self) {
899        let mut inner = self.lock_inner();
900        inner.retry_abort = None;
901    }
902
903    /// Abort in-flight retry sleep.
904    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    /// Begin compaction abort slot.
912    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    /// Clear compaction abort.
923    pub(super) fn clear_compaction_abort(&self) {
924        self.lock_inner().compaction_abort = None;
925    }
926
927    /// Abort manual compaction.
928    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    /// Begin bash abort slot.
939    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    /// Clear bash abort.
950    pub(super) fn clear_bash_abort(&self) {
951        self.lock_inner().bash_abort = None;
952    }
953
954    /// Abort bash.
955    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    /// Abort every active session operation, then wait for session idle.
963    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    /// Wait until the session-level run is idle (no active retries/continuations).
973    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            // Also wait for agent idle so we don't spin.
983            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    /// Dispose this session's local resources.
996    ///
997    /// Cancels session-owned operations, disconnects the event pump, aborts
998    /// and drains the agent, invalidates the extension context, then awaits
999    /// host process reap exactly once when a concrete host is present — even
1000    /// if no `session_shutdown` handlers were registered. The runtime
1001    /// replacement layer owns the single reason-specific extension shutdown
1002    /// event and must emit it before calling this method when handlers exist.
1003    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        // Always await process reap exactly once when a host was bound.
1031        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    // -------------------------------------------------------------------------
1044    // pub(super) helpers for sibling modules / pump
1045    // -------------------------------------------------------------------------
1046
1047    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    /// Lock the settings manager with poison recovery.
1054    ///
1055    /// Callers must drop the guard before any `.await`. Read what you need
1056    /// into locals first when the surrounding function is async.
1057    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    /// Set auto-retry enabled.
1092    ///
1093    /// Updates the runtime cache used by `prepare_retry` / `will_retry` and the
1094    /// settings document (TypeScript `setAutoRetryEnabled` writes settings).
1095    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    /// Set auto-compaction enabled.
1101    ///
1102    /// Updates both the runtime cache and the persisted settings document.
1103    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    /// Set steering mode on the agent.
1109    pub fn set_steering_mode(&self, mode: QueueMode) {
1110        self.agent.set_steering_mode(mode);
1111    }
1112
1113    /// Set follow-up mode on the agent.
1114    pub fn set_follow_up_mode(&self, mode: QueueMode) {
1115        self.agent.set_follow_up_mode(mode);
1116    }
1117}
1118
1119// Silence unused Duration until retry sleep lands.
1120#[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    /// Extension runner that records emit order and can delay `message_end`.
1202    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        // Allow the lossless event pump to drain agent_end/turn_end after the
1388        // agent run token is released.
1389        sleep(Duration::from_millis(50)).await;
1390        session.emit_agent_settled().await;
1391
1392        let types = collect_types(&mut rx, 16).await;
1393        // agent_start -> turn_start -> message_start -> message_end (user)
1394        // -> message_start -> message_end (assistant) -> turn_end -> agent_end -> agent_settled
1395        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            // Block-free: try_lock; if busy push via blocking.
1437            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        // Drain any spawned public pushes.
1455        sleep(Duration::from_millis(50)).await;
1456
1457        let recorded = order.lock().await.clone();
1458        // For each event type that has both, ext must appear before pub.
1459        // Check agent_start specifically.
1460        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        // First prompt to establish assistant tail, then steer, then continue.
1494        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        // pendingMessageCount is already decremented by the time message_start is observed.
1512        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        // Replace assistant text with "replaced".
1531        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        // Live agent transcript tail should be the replacement.
1557        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        // Persistence: session entries should include the replacement text.
1571        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        // Simulate retry/continuation without settling yet.
1608        session
1609            .agent
1610            .prompt(vec![user_text("again", std::iter::empty())])
1611            .await?;
1612        session.agent.wait_for_idle().await;
1613
1614        // Exactly one settle after the full session-level lifecycle.
1615        session.emit_agent_settled().await;
1616        // Second call must not double-emit.
1617        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        // message_end (user) must come before message_start (assistant), etc.
1651        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        // Without a real tool-using provider fixture, verify that manually
1751        // injected agent events through the pump preserve tool_* types.
1752        // Full tool interleave is covered once tools slices land; here we
1753        // assert the session event mapping for tool variants.
1754        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}