Skip to main content

oxicode_agent/
agent.rs

1/// Core agent implementation
2use crate::config::AgentConfig;
3use crate::config::ShouldStopAfterTurnContext;
4use crate::events::AgentEvent;
5use crate::state::{AgentState, SharedState};
6use crate::tools::{AgentTool, ToolRegistry};
7use crate::types::{Response, StopReason};
8use anyhow::{Error, Result};
9use oxicode_ai::{
10    CompactionManager, CompactionStrategy, Compactor, LlmCompactor, Model, Provider,
11    transform_for_provider,
12};
13use parking_lot::RwLock;
14use std::sync::Arc;
15use std::sync::atomic::{AtomicBool, Ordering};
16
17// ── ProviderResolver trait ────────────────────────────────────────
18
19/// Trait for resolving providers and models within an Agent.
20///
21/// This abstracts away global static registries, allowing SDK users
22/// to provide isolated provider/model lookups.
23///
24/// When using the SDK (`oxicode-sdk`), the `Oxicode` engine implements this trait.
25/// When using `Agent::new()` directly, a global fallback is used.
26pub trait ProviderResolver: Send + Sync + 'static {
27    /// Resolve a provider by name, returning an Arc handle.
28    fn resolve_provider(&self, name: &str) -> Option<Arc<dyn Provider>>;
29
30    /// Resolve a model ID ("provider/model" or bare "model") to a Model.
31    fn resolve_model(&self, model_id: &str) -> Option<Model>;
32}
33
34/// Global provider resolver — uses `oxicode_ai` global functions.
35///
36/// This is the default resolver when using `Agent::new()`, preserving
37/// backward compatibility with existing CLI usage.
38pub(crate) struct GlobalProviderResolver;
39
40impl ProviderResolver for GlobalProviderResolver {
41    fn resolve_provider(&self, name: &str) -> Option<Arc<dyn Provider>> {
42        oxicode_ai::get_provider(name).map(Arc::from)
43    }
44
45    fn resolve_model(&self, model_id: &str) -> Option<Model> {
46        crate::model_id::resolve_model_from_id(model_id)
47    }
48}
49
50// ── AgentInner ────────────────────────────────────────────────────
51/// Mutable agent internals protected by a read-write lock.
52struct AgentInner {
53    config: AgentConfig,
54    provider: Arc<dyn Provider>,
55    /// Side-dispatch closures invoked for every `AgentEvent` emitted by
56    /// the agent run methods. Used by `oxicode-sdk` to bridge observability
57    /// types (Tracer, CostTracker, ...) into the agent loop without
58    /// leaking SDK types into `oxicode-agent`.
59    ///
60    /// Lock-mutex rather than `RwLock`: dispatch lists mutate rarely
61    /// (only on `add_observability_dispatch`), but reads happen on every
62    /// event (high frequency), so a `Mutex` with cheap poison-free
63    /// acquisition is the right shape.
64    observability_dispatch: parking_lot::Mutex<Vec<EventDispatchFn>>,
65}
66
67/// Type alias for an observability dispatch handler. Each entry is a
68/// closure registered via [`Agent::add_observability_dispatch`] and
69/// invoked on every emitted `AgentEvent`. Named to keep the
70/// [`AgentInner`] field readable without an inline `dyn` route.
71type EventDispatchFn = Arc<dyn Fn(AgentEvent) + Send + Sync>;
72
73impl Clone for AgentInner {
74    fn clone(&self) -> Self {
75        Self {
76            config: self.config.clone(),
77            provider: Arc::clone(&self.provider),
78            // The dispatch list is *not* cloned: each `Agent` instance has
79            // its own observers. Cloning the AgentInner (rare; happens in
80            // `run_with_channel_inner` when sharing config across loops)
81            // gives the new loop an empty observer set, which is correct:
82            // the *Agent* retains the original dispatch list, and the
83            // temporary inner clone is discarded after the run.
84            observability_dispatch: parking_lot::Mutex::new(Vec::new()),
85        }
86    }
87}
88///
89/// Manages provider, tool registry, state, and compaction, providing an
90/// agentic loop for prompt execution, model switching, tool calls, and fallback.
91///
92/// Supports session continuation via [`continue_with`] and tokio-native
93/// event streaming via [`run_tokio_stream`].
94///
95/// [`continue_with`]: Agent::continue_with
96/// [`run_tokio_stream`]: Agent::run_tokio_stream
97/// Deferred model switch request, stored when the agent is running.
98struct PendingModelSwitch {
99    model_id: String,
100    provider: Arc<dyn Provider>,
101    /// Whether messages need cross-provider transformation.
102    needs_transform: bool,
103    old_api: oxicode_ai::Api,
104    new_api: oxicode_ai::Api,
105}
106
107/// Agent runtime.
108///
109/// Manages provider, tool registry, state, and compaction, providing an
110/// agentic loop for prompt execution, model switching, tool calls, and fallback.
111///
112/// Supports session continuation, tokio-native event streaming, and deferred
113/// model switching (changes are queued while a loop is running and applied
114/// after it completes).
115#[allow(missing_docs)]
116pub struct Agent {
117    inner: RwLock<AgentInner>,
118    tools: Arc<ToolRegistry>,
119    state: SharedState,
120    compaction_manager: CompactionManager,
121    /// Custom compactor injected at construction (via `new_with_compactor`).
122    ///
123    /// Replaces the default `LlmCompactor` in the per-run `AgentLoop`
124    /// (threaded into `AgentLoopConfig.compactor`). `None` preserves the
125    /// existing default-LLM-compactor behavior.
126    custom_compactor: Option<Arc<dyn Compactor>>,
127    hooks: parking_lot::RwLock<crate::config::AgentHooks>,
128    /// Guard: true while a run is in progress. Prevents concurrent runs.
129    is_running: Arc<AtomicBool>,
130    /// Provider/model resolver. Uses global functions by default,
131    /// or a custom resolver when created via `new_with_resolver()`.
132    resolver: Arc<dyn ProviderResolver>,
133    /// Shared cancellation flag. Set by `cancel()` (e.g. on Ctrl+C),
134    /// propagated to AgentLoop's `external_stop` during each run.
135    cancel_flag: Arc<AtomicBool>,
136    /// Shared auto-retry enabled flag — runtime-toggleable via `set_auto_retry`,
137    /// injected into each ephemeral AgentLoop via `set_auto_retry_state`.
138    auto_retry_enabled: Arc<AtomicBool>,
139    /// Shared auto-retry cancel flag (RPC `abort_retry`).
140    auto_retry_cancel: Arc<AtomicBool>,
141    /// Shared auto-retry notify for immediate retry-sleep wake-up.
142    auto_retry_notify: Arc<tokio::sync::Notify>,
143    /// Pending model switch — stored when the agent is running,
144    /// applied after the current loop completes.
145    pending_model_switch: RwLock<Option<PendingModelSwitch>>,
146}
147
148impl Agent {
149    /// Create a new agent with the given provider, config, and tool registry.
150    ///
151    /// Uses the global `oxicode_ai::get_provider()` / `resolve_model_from_id()`
152    /// for model switching. For isolated instances, use [`new_with_resolver`].
153    ///
154    /// [`new_with_resolver`]: Agent::new_with_resolver
155    pub fn new(provider: Arc<dyn Provider>, config: AgentConfig, tools: Arc<ToolRegistry>) -> Self {
156        let resolver = Arc::new(GlobalProviderResolver);
157        Self::build_inner(provider, config, tools, resolver, None)
158    }
159
160    /// Create an agent with a custom provider/model resolver.
161    ///
162    /// This is the preferred constructor for SDK usage where provider
163    /// and model registries must be isolated from global state.
164    pub fn new_with_resolver(
165        provider: Arc<dyn Provider>,
166        config: AgentConfig,
167        tools: Arc<ToolRegistry>,
168        resolver: Arc<dyn ProviderResolver>,
169    ) -> Self {
170        Self::build_inner(provider, config, tools, resolver, None)
171    }
172
173    /// Create an agent with a custom provider/model resolver and a custom
174    /// compactor that replaces the default LLM compactor.
175    ///
176    /// The compactor is threaded into every per-run `AgentLoop` (via
177    /// `AgentLoopConfig.compactor`) — see
178    /// [`crate::agent_loop::config::AgentLoopConfig::compactor`] for the
179    /// replace semantics. `oxicode-sdk`'s `AgentBuilder::with_compactor`
180    /// uses this constructor.
181    pub fn new_with_compactor(
182        provider: Arc<dyn Provider>,
183        config: AgentConfig,
184        tools: Arc<ToolRegistry>,
185        resolver: Arc<dyn ProviderResolver>,
186        custom_compactor: Option<Arc<dyn Compactor>>,
187    ) -> Self {
188        Self::build_inner(provider, config, tools, resolver, custom_compactor)
189    }
190
191    /// Create an agent with an empty tool registry.
192    pub fn new_empty(provider: Arc<dyn Provider>, config: AgentConfig) -> Self {
193        Self::new(provider, config, Arc::new(ToolRegistry::new()))
194    }
195
196    /// Get the agent configuration (read guard)
197    fn config(&self) -> parking_lot::RwLockReadGuard<'_, AgentInner> {
198        self.inner.read()
199    }
200
201    /// Get a write guard for the agent inner state
202    fn inner_mut(&self) -> parking_lot::RwLockWriteGuard<'_, AgentInner> {
203        self.inner.write()
204    }
205
206    /// Get the current model ID
207    pub fn model_id(&self) -> String {
208        self.config().config.model_id.clone()
209    }
210
211    /// Get the active model's context window (tokens).
212    ///
213    /// Synced from the resolved model at construction and on every model
214    /// switch — the authoritative value the per-run loop computes
215    /// compaction thresholds against. Hosts use it for CTX displays.
216    pub fn context_window(&self) -> usize {
217        self.config().config.context_window
218    }
219
220    /// Get the agent configuration (full clone)
221    pub fn get_config(&self) -> AgentConfig {
222        self.config().config.clone()
223    }
224
225    /// Get a cheap clone of the configured todo state provider, if any.
226    /// Used by hosts (e.g. the TUI) to observe todo phase changes without
227    /// cloning the full [`AgentConfig`].
228    pub fn todo_provider(&self) -> Option<std::sync::Arc<dyn crate::tools::TodoStateProvider>> {
229        self.config().config.todo.clone()
230    }
231
232    /// Internal constructor shared by `new()`, `new_with_resolver()` and
233    /// `new_with_compactor()`.
234    fn build_inner(
235        provider: Arc<dyn Provider>,
236        config: AgentConfig,
237        tools: Arc<ToolRegistry>,
238        resolver: Arc<dyn ProviderResolver>,
239        custom_compactor: Option<Arc<dyn Compactor>>,
240    ) -> Self {
241        // Resolve the initial model once: its real context window is the
242        // source of truth for compaction thresholds (construction sites
243        // pass a 128_000 placeholder). Overwrite only when positive —
244        // resolvers that cannot resolve leave the configured value alone.
245        let mut config = config;
246        let resolved_model = resolver.resolve_model(&config.model_id);
247        if let Some(model) = &resolved_model
248            && model.context_window > 0
249        {
250            config.context_window = model.context_window;
251        }
252
253        let mut compaction_manager =
254            CompactionManager::new(config.compaction_strategy.clone(), config.context_window);
255
256        // Pre-initialize the LLM compactor if compaction is enabled
257        // (unless a custom compactor replaces it — the Agent's own
258        // manager follows the same replace semantics as the loop).
259        if let Some(compactor) = &custom_compactor {
260            compaction_manager.set_compactor(Arc::clone(compactor));
261        } else if config.compaction_strategy != CompactionStrategy::Disabled
262            && let Some(model) = resolved_model
263        {
264            let llm_compactor = Arc::new(LlmCompactor::new(model.clone(), Arc::clone(&provider)));
265            compaction_manager.set_compactor(llm_compactor);
266        }
267
268        Self {
269            inner: RwLock::new(AgentInner {
270                config,
271                provider,
272                observability_dispatch: parking_lot::Mutex::new(Vec::new()),
273            }),
274            tools,
275            state: SharedState::new(),
276            compaction_manager,
277            custom_compactor,
278            hooks: parking_lot::RwLock::new(crate::config::AgentHooks::default()),
279            is_running: Arc::new(AtomicBool::new(false)),
280            resolver,
281            cancel_flag: Arc::new(AtomicBool::new(false)),
282            auto_retry_enabled: Arc::new(AtomicBool::new(true)),
283            auto_retry_cancel: Arc::new(AtomicBool::new(false)),
284            auto_retry_notify: Arc::new(tokio::sync::Notify::new()),
285            pending_model_switch: RwLock::new(None),
286        }
287    }
288
289    /// Sync the agent's context window from a resolved model.
290    ///
291    /// The per-run loop reads `inner.config.context_window` fresh each run
292    /// (the authoritative value — compaction thresholds, CTX display).
293    /// Construction sites pass a 128_000 placeholder; the resolved model's
294    /// real capacity overwrites it here. Zero-window models (LOCAL
295    /// discovery placeholders) keep the previous value.
296    ///
297    /// The construction-time [`CompactionManager`] field is not updated:
298    /// it only serves manual compaction (`compact_now`, window-independent)
299    /// and is built with the already-synced window at construction.
300    fn apply_context_window(&self, window: usize) {
301        if window == 0 {
302            return;
303        }
304        self.inner_mut().config.context_window = window;
305    }
306
307    /// Get a reference to the provider resolver.
308    pub fn resolver(&self) -> &Arc<dyn ProviderResolver> {
309        &self.resolver
310    }
311    /// Switch the model used for future LLM calls.
312    ///
313    /// Switch model mid-conversation.
314    ///
315    /// If the agent is currently running, the switch is deferred: the new
316    /// model and provider are stored in `pending_model_switch` and applied
317    /// automatically when the current loop finishes. This ensures the
318    /// running loop completes with a consistent provider/model without
319    /// interruption.
320    ///
321    /// If the agent is idle, the switch takes effect immediately.
322    ///
323    /// If the new model uses a different provider API, the conversation
324    /// history is automatically transformed for cross-provider compatibility
325    /// (e.g. thinking blocks are converted to `<thinking>` tags).
326    ///
327    /// # Arguments
328    /// * `model_id` - New model ID in `provider/model` format
329    ///
330    /// # Returns
331    /// `Ok(())` on success, or an error if the model/provider is unknown
332    ///
333    /// # Credentials
334    /// The new provider is constructed via [`ProviderResolver::resolve_provider`],
335    /// which is the single credential authority — the wired `AuthProvider`
336    /// port (sync fast-path) supplies the API key. The old `api_key` parameter
337    /// was removed in 0.55.0; see issues #39 and #40.
338    pub fn switch_model(&self, model_id: &str) -> Result<()> {
339        let new_model = self
340            .resolver
341            .resolve_model(model_id)
342            .ok_or_else(|| Error::msg(format!("Model '{}' not found", model_id)))?;
343
344        // Create the new provider via resolver
345        let new_provider = self
346            .resolver
347            .resolve_provider(&new_model.provider)
348            .ok_or_else(|| Error::msg(format!("Provider '{}' not found", new_model.provider)))?;
349
350        // Detect API change
351        let (old_api, needs_transform) = {
352            let inner = self.config();
353            let old_api = self
354                .resolver
355                .resolve_model(&inner.config.model_id)
356                .map(|m| m.api)
357                .unwrap_or(oxicode_ai::Api::AnthropicMessages);
358            (old_api, old_api != new_model.api)
359        };
360
361        // If the agent is currently running, defer the switch.
362        if self.is_running.load(Ordering::SeqCst) {
363            tracing::info!(
364                "[AGENT] Agent running, deferring model switch to '{}' until loop completes",
365                model_id
366            );
367            *self.pending_model_switch.write() = Some(PendingModelSwitch {
368                model_id: model_id.to_string(),
369                provider: new_provider,
370                needs_transform,
371                old_api,
372                new_api: new_model.api,
373            });
374            // Update config immediately so model_id() returns the new value,
375            // but leave provider unchanged so the running loop keeps its provider.
376            {
377                let mut inner = self.inner_mut();
378                inner.config.model_id = model_id.to_string();
379            }
380            self.apply_context_window(new_model.context_window);
381            return Ok(());
382        }
383
384        // Agent is idle — apply immediately.
385        if needs_transform {
386            let messages = self.state.get_state().messages.clone();
387            let transformed = transform_for_provider(&messages, &old_api, &new_model.api);
388            self.state.update(|s| {
389                s.replace_messages(transformed);
390            });
391        }
392
393        {
394            let mut inner = self.inner_mut();
395            inner.config.model_id = model_id.to_string();
396            inner.provider = new_provider;
397        }
398        self.apply_context_window(new_model.context_window);
399
400        Ok(())
401    }
402
403    /// Switch the model using a pre-resolved `Model` object.
404    ///
405    /// This is useful when the caller has already looked up the model
406    /// and optionally created the provider.
407    ///
408    /// Like [`switch_model`], if the agent is currently running, the switch
409    /// is deferred until the current loop completes.
410    ///
411    /// # Credentials
412    /// The new provider is constructed via [`ProviderResolver::resolve_provider`],
413    /// the single credential authority (sync `AuthProvider` fast-path).
414    /// The old `api_key` parameter was removed in 0.55.0; see issues #39/#40.
415    ///
416    /// [`switch_model`]: Agent::switch_model
417    pub fn switch_to_model(&self, model: &oxicode_ai::Model) -> Result<()> {
418        let model_id = format!("{}/{}", model.provider, model.id);
419        let new_provider = self
420            .resolver
421            .resolve_provider(&model.provider)
422            .ok_or_else(|| Error::msg(format!("Provider '{}' not found", model.provider)))?;
423
424        // Detect API change
425        let (old_api, needs_transform) = {
426            let inner = self.config();
427            let old_api = self
428                .resolver
429                .resolve_model(&inner.config.model_id)
430                .map(|m| m.api)
431                .unwrap_or(oxicode_ai::Api::AnthropicMessages);
432            (old_api, old_api != model.api)
433        };
434
435        // If the agent is currently running, defer the switch.
436        if self.is_running.load(Ordering::SeqCst) {
437            tracing::info!(
438                "[AGENT] Agent running, deferring model switch to '{}' until loop completes",
439                model_id
440            );
441            *self.pending_model_switch.write() = Some(PendingModelSwitch {
442                model_id: model_id.clone(),
443                provider: new_provider,
444                needs_transform,
445                old_api,
446                new_api: model.api,
447            });
448            {
449                let mut inner = self.inner_mut();
450                inner.config.model_id = model_id;
451            }
452            self.apply_context_window(model.context_window);
453            return Ok(());
454        }
455
456        // Agent is idle — apply immediately.
457        if needs_transform {
458            let messages = self.state.get_state().messages.clone();
459            let transformed = transform_for_provider(&messages, &old_api, &model.api);
460            self.state.update(|s| {
461                s.replace_messages(transformed);
462            });
463        }
464
465        {
466            let mut inner = self.inner_mut();
467            inner.config.model_id = model_id;
468            inner.provider = new_provider;
469        }
470        self.apply_context_window(model.context_window);
471
472        Ok(())
473    }
474
475    /// Refresh credentials by re-resolving the current provider via the resolver.
476    ///
477    /// After the resolver-centric credential model (0.55.0), the provider
478    /// instance is the single source of truth for API keys. To pick up
479    /// credential changes — e.g. the user updated their auth store via the
480    /// TUI overlay — call this to re-resolve the current provider and swap
481    /// it in. The resolver consults the wired `AuthProvider` port on every
482    /// call, so updates are reflected without rebuilding the engine.
483    ///
484    /// Returns `Ok(())` if a fresh provider was resolved and swapped, or an
485    /// error if the resolver could not produce a provider (the existing
486    /// provider is left untouched on error). Replaces the deprecated
487    /// `refresh_api_key(&self, api_key)` from pre-0.55.0; see issues #39/#40.
488    pub fn refresh_credentials(&self) -> Result<()> {
489        let provider_name = {
490            let inner = self.config();
491            inner.config.model_id.split('/').next().map(str::to_string)
492        };
493        let name = provider_name.as_deref().unwrap_or("anthropic");
494        let new_provider = self
495            .resolver
496            .resolve_provider(name)
497            .ok_or_else(|| Error::msg(format!("Provider '{}' not found", name)))?;
498        let mut inner = self.inner_mut();
499        inner.provider = new_provider;
500        Ok(())
501    }
502
503    /// Get a handle to the tool registry.
504    pub fn tools(&self) -> Arc<ToolRegistry> {
505        Arc::clone(&self.tools)
506    }
507
508    /// Get a snapshot of the current agent state.
509    pub fn state(&self) -> AgentState {
510        self.state.get_state()
511    }
512
513    /// Update agent state in-place. Used by compaction to replace messages.
514    pub fn update_state(&self, f: impl FnOnce(&mut AgentState)) {
515        self.state.update(f);
516    }
517
518    /// Reset agent state for a new conversation
519    pub fn reset(&self) {
520        self.state.reset();
521    }
522
523    /// Register a tool that the agent can invoke during a run.
524    pub fn add_tool<T: AgentTool + 'static>(&self, tool: T) {
525        self.tools.register(tool);
526    }
527
528    /// Update the system prompt for future interactions.
529    pub fn set_system_prompt(&self, prompt: String) {
530        self.inner_mut().config.system_prompt = Some(prompt);
531    }
532
533    /// Get the compaction manager
534    pub fn compaction_manager(&self) -> &CompactionManager {
535        &self.compaction_manager
536    }
537    /// Update the compaction strategy for future runs.
538    ///
539    /// The strategy is read fresh from the config at the start of each run
540    /// (see `run_with_channel_inner`), so this takes effect on the next
541    /// agent turn — never mid-run. Pair with `compaction_manager()` for
542    /// manual compaction, which is unaffected by the strategy.
543    pub fn set_compaction_strategy(&self, strategy: oxicode_ai::CompactionStrategy) {
544        self.inner.write().config.compaction_strategy = strategy;
545    }
546    /// Get the compaction strategy that will be used on the next run.
547    ///
548    /// This reads from `inner.config` (mutable via `set_compaction_strategy`),
549    /// **not** from the `compaction_manager` field (which retains its
550    /// construction-time strategy). The agent loop reads from config fresh
551    /// each run, so this is the authoritative value.
552    pub fn compaction_strategy(&self) -> oxicode_ai::CompactionStrategy {
553        self.inner.read().config.compaction_strategy.clone()
554    }
555
556    /// Run the agent with a prompt, collecting all events into a vector.
557    ///
558    /// Convenience wrapper around [`run_with_channel`](Self::run_with_channel) that gathers every
559    /// [`AgentEvent`] produced during the run.
560    pub async fn run(&self, prompt: String) -> Result<(Response, Vec<AgentEvent>)> {
561        let mut events = Vec::new();
562        let (tx, rx) = std::sync::mpsc::channel::<AgentEvent>();
563        let result = self.run_with_channel(prompt, tx).await;
564        while let Ok(event) = rx.recv() {
565            events.push(event);
566        }
567        result.map(|r| (r, events))
568    }
569
570    /// Run the agent, delivering events through the provided channel.
571    ///
572    /// Delegates to the agent loop which implements the same 2-level agentic
573    /// loop matching pi-mono's architecture:
574    ///
575    /// ```text
576    /// AgentLoop.run_messages()
577    ///   Outer loop (follow-up messages):
578    ///     Inner loop (tool calls + steering):
579    ///       1. Inject pending messages (steering)
580    ///       2. Compaction check
581    ///       3. Stream LLM response (with accumulated partial messages)
582    ///       4. Execute tool calls if any
583    ///       5. Emit turn_end
584    ///       6. Check shouldStopAfterTurn
585    ///       7. Poll steering messages
586    ///     Check follow-up messages
587    ///     Exit
588    /// ```
589    pub async fn run_with_channel(
590        &self,
591        prompt: String,
592        tx: std::sync::mpsc::Sender<AgentEvent>,
593    ) -> Result<Response> {
594        self.run_with_channel_message(
595            oxicode_ai::Message::User(oxicode_ai::UserMessage::new(prompt)),
596            tx,
597        )
598        .await
599    }
600
601    /// Run with an explicit user `Message` (supports image content blocks).
602    /// Used by RPC `prompt` with images. The running-guard logic lives here;
603    /// [`run_with_channel`](Self::run_with_channel) delegates after converting
604    /// its String prompt into a text-only user message.
605    pub async fn run_with_channel_message(
606        &self,
607        prompt: oxicode_ai::Message,
608        tx: std::sync::mpsc::Sender<AgentEvent>,
609    ) -> Result<Response> {
610        // pi-mono: Agent.prompt() throws if activeRun exists.
611        // Prevent concurrent runs that would corrupt shared state.
612        if self
613            .is_running
614            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
615            .is_err()
616        {
617            return Err(Error::msg("Agent is already running"));
618        }
619
620        // Drop guard ensures is_running is cleared even on panic.
621        struct RunningGuard<'a>(&'a AtomicBool);
622        impl Drop for RunningGuard<'_> {
623            fn drop(&mut self) {
624                self.0.store(false, Ordering::SeqCst);
625            }
626        }
627        let _guard = RunningGuard(&self.is_running);
628        self.reset_cancel();
629
630        self.run_with_channel_inner(prompt, tx).await
631    }
632
633    /// Inner implementation of run_with_channel, called after the running guard is set.
634    async fn run_with_channel_inner(
635        &self,
636        prompt: oxicode_ai::Message,
637        tx: std::sync::mpsc::Sender<AgentEvent>,
638    ) -> Result<Response> {
639        use crate::agent_loop::AgentLoop;
640
641        let (
642            provider,
643            system_prompt,
644            temperature,
645            max_tokens,
646            compaction_strategy,
647            context_window,
648            workspace_dir,
649        ) = {
650            let inner = self.inner.read();
651            (
652                Arc::clone(&inner.provider) as Arc<dyn Provider>,
653                inner.config.system_prompt.clone(),
654                inner.config.temperature,
655                inner.config.max_tokens,
656                inner.config.compaction_strategy.clone(),
657                inner.config.context_window,
658                inner.config.workspace_dir.clone(),
659            )
660        }; // release read lock
661
662        // Build AgentLoopConfig from Agent's config
663        let loop_config = crate::agent_loop::config::AgentLoopConfig {
664            model_id: self.model_id(),
665            system_prompt,
666            temperature: temperature.unwrap_or(1.0) as f32,
667            max_tokens: max_tokens.unwrap_or(4096) as u32,
668            tool_execution: crate::config::ToolExecutionMode::Sequential,
669            compaction_strategy,
670            compaction_instruction: None,
671            compactor: self.custom_compactor.clone(),
672            context_window,
673            session_id: self.config().config.session_id.clone(),
674            transport: None,
675            compact_on_start: false,
676            max_retry_delay_ms: None,
677            auto_retry_enabled: true,
678            auto_retry_max_attempts: 3,
679            auto_retry_base_delay_ms: 1000,
680            workspace_dir,
681            provider_options: self.config().config.provider_options.clone(),
682            on_compaction: None,
683            ttsr_engine: self.config().config.ttsr_engine.clone(),
684            memory: self.config().config.memory.clone(),
685            todo: self.config().config.todo.clone(),
686            todo_reminders_enabled: self.config().config.todo_reminders_enabled,
687            todo_reminders_max: self.config().config.todo_reminders_max,
688            todo_eager_mode: self.config().config.todo_eager_mode,
689            agent_pool: self.config().config.agent_pool.clone(),
690            url_resolver: self.config().config.url_resolver.clone(),
691            lsp: self.config().config.lsp.clone(),
692            snapshot_store: self.config().config.snapshot_store.clone(),
693            max_tool_result_bytes: self.config().config.max_tool_result_bytes,
694            subagent_runner: self.config().config.subagent_runner.clone(),
695            subagent_depth: self.config().config.subagent_depth,
696            mode: self.config().config.mode,
697            ..Default::default()
698        };
699
700        // Create AgentLoop. We give it a NEW SharedState and sync back after.
701        // (SharedState is not Clone, so we create a fresh one from current state)
702        let fresh_state = crate::state::SharedState::new();
703        let current = self.state.get_state();
704        fresh_state.update(|s| {
705            *s = current;
706        });
707
708        let mut agent_loop = AgentLoop::new_with_resolver(
709            provider,
710            loop_config,
711            Arc::clone(&self.tools),
712            fresh_state,
713            Arc::clone(&self.resolver),
714        );
715
716        // Add the user prompt to Agent.state() AFTER fresh_state is created.
717        // fresh_state got a copy of the pre-prompt state, so run_loop will
718        // add the prompt to fresh_state independently via initial_prompts.
719        // But persist_session() reads Agent.state() (not fresh_state), so it
720        // needs the user prompt there to write it to the session file.
721        // Sync happens at AgentEnd (after run_loop completes), where
722        // Agent.state is overwritten with fresh_state (which has all messages).
723        self.state.update(|s| {
724            s.messages.push(prompt.clone());
725        });
726
727        // Pre-populate steering/follow-up from hooks
728        {
729            let hooks = self.hooks.read();
730            if let Some(ref get_steering) = hooks.get_steering_messages {
731                for msg in get_steering() {
732                    agent_loop.steer(msg);
733                }
734            }
735            if let Some(ref get_follow_up) = hooks.get_follow_up_messages {
736                for msg in get_follow_up() {
737                    agent_loop.follow_up(msg);
738                }
739            }
740
741            // Store hooks on AgentLoop so they can be polled each turn
742            // to pick up new messages injected during the run.
743            if let Some(ref get_steering) = hooks.get_steering_messages {
744                agent_loop.set_steering_hook(Arc::clone(get_steering));
745            }
746            if let Some(ref get_follow_up) = hooks.get_follow_up_messages {
747                agent_loop.set_follow_up_hook(Arc::clone(get_follow_up));
748            }
749        }
750        let mut al = agent_loop;
751
752        // Wire should_stop_after_turn hook: share AgentLoop's external_stop
753        // Arc with the emit callback. When the hook fires (Ctrl+C detected),
754        // it sets ext_stop. AgentLoop checks this in should_stop_after_turn()
755        // AND during streaming (streaming.rs checks external_stop each event).
756        //
757        // Arc<dyn Fn> can be cloned, so we read it without consuming.
758        let maybe_hook = {
759            let hooks_r = self.hooks.read();
760            hooks_r.should_stop_after_turn.clone()
761        };
762        let ext_stop = al.external_stop().clone();
763        let cancel_flag = self.cancel_flag.clone();
764
765        // Share cancel_flag with AgentLoop so the streaming loop can check
766        // it directly in the periodic timer — no emit callback required.
767        // This closes the gap where cancel() was ineffective when the
768        // provider stream produced no events.
769        al.set_cancel_signal(self.cancel_flag.clone());
770        let (ar_enabled, ar_cancel, ar_notify) = self.auto_retry_state();
771        al.set_auto_retry_state(ar_enabled, ar_cancel, ar_notify);
772
773        // Create emit callback that sends through the channel.
774        // AgentLoop calls this synchronously. UnboundedSender::send() is
775        // non-blocking and never drops events (unlike try_send on bounded).
776        let tx_emit = tx.clone();
777
778        // Snapshot the observability_dispatch list once per run. This avoids
779        // holding an Agent lock on the emit-fn hot path while still letting
780        // SDK consumers register new dispatchers at any time (registers after
781        // this snapshot will fire on the next run).
782        let dispatch_handlers: Vec<EventDispatchFn> =
783            { self.inner.read().observability_dispatch.lock().clone() };
784        tracing::info!("[AGENT] Starting agent run with channel");
785        let result = al
786            .run_message(prompt.clone(), move |event: AgentEvent| {
787                // Forward event to channel (std::sync::mpsc — send from sync context)
788                tracing::info!("[AGENT-EMIT] Event: {:?}", std::mem::discriminant(&event));
789                if let Err(e) = tx_emit.send(event.clone()) {
790                    tracing::error!(
791                        "[AGENT-EMIT] Failed to send agent event to channel: {:?}",
792                        e
793                    );
794                } else {
795                    tracing::info!("[AGENT-EMIT] Successfully sent event");
796                }
797
798                // Propagate cancellation from Agent::cancel() → external_stop.
799                // This runs on every event, ensuring the streaming loop detects
800                // cancellation promptly.
801                if cancel_flag.load(Ordering::SeqCst) {
802                    ext_stop.store(true, Ordering::SeqCst);
803                }
804
805                // Fan out to SDK-side observability handlers (Tracer,
806                // CostTracker, ...). The dispatch list is snapshotted at
807                // run-start so we hold Arc clones, not a lock. This means
808                // handlers added mid-run do not fire until the next run.
809                for handler in dispatch_handlers.iter() {
810                    handler(event.clone());
811                }
812                // Propagate should_stop → external_stop on every event, not
813                // just TurnEnd. The TUI hook only checks should_stop_flag.load(),
814                // so the context contents are irrelevant for non-TurnEnd events.
815                // This ensures streaming.rs detects cancellation immediately
816                // when the user presses Ctrl+C mid-stream.
817                if let Some(ref hook) = maybe_hook {
818                    let ctx = ShouldStopAfterTurnContext {
819                        message: match &event {
820                            AgentEvent::TurnEnd {
821                                assistant_message: oxicode_ai::Message::Assistant(a),
822                                ..
823                            } => a.clone(),
824                            _ => oxicode_ai::AssistantMessage::new(
825                                oxicode_ai::Api::OpenAiCompletions,
826                                "agent",
827                                "agent-model",
828                            ),
829                        },
830                        tool_results: match &event {
831                            AgentEvent::TurnEnd { tool_results, .. } => tool_results.clone(),
832                            _ => Vec::new(),
833                        },
834                        iteration: 0,
835                    };
836                    if hook(&ctx) {
837                        ext_stop.store(true, Ordering::SeqCst);
838                    }
839                }
840            })
841            .await;
842
843        match result {
844            Ok(_events) => {
845                // Sync state back from AgentLoop
846                let loop_state = al.state().get_state();
847                self.state.update(|s| {
848                    *s = loop_state;
849                });
850
851                // Apply any pending model switch that was deferred during the run.
852                // This transforms messages (if cross-provider) and swaps the provider
853                // so the next run uses the new model.
854                self.apply_pending_model_switch();
855
856                // Extract final response text from state
857                let state = self.state.get_state();
858                let final_text = state
859                    .messages
860                    .iter()
861                    .rev()
862                    .find_map(|m| match m {
863                        oxicode_ai::Message::Assistant(a) => {
864                            a.content.iter().find_map(|b| match b {
865                                oxicode_ai::ContentBlock::Text(t) => Some(t.text.clone()),
866                                _ => None,
867                            })
868                        }
869                        _ => None,
870                    })
871                    .unwrap_or_default();
872
873                let stop_reason = state.stop_reason.unwrap_or(StopReason::Stop);
874
875                Ok(Response {
876                    content: final_text,
877                    stop_reason,
878                })
879            }
880            Err(e) => {
881                // Apply pending model switch even on error so the next run
882                // uses the new model.
883                self.apply_pending_model_switch();
884                Err(e)
885            }
886        }
887    }
888
889    // ── Helper methods for the agentic loop ────────────────────────
890
891    /// Set hooks for the agent loop.
892    pub fn set_hooks(&self, hooks: crate::config::AgentHooks) {
893        let mut h = self.hooks.write();
894        *h = hooks;
895    }
896
897    /// Register a side-dispatch closure called for every `AgentEvent`
898    /// emitted by `run`, `run_with_channel`, `run_streaming`,
899    /// `run_tokio_stream`, and `continue_with`.
900    ///
901    /// Multiple calls stack: every registered closure is invoked on
902    /// every event. Closures run synchronously on the agent-loop emit
903    /// thread, so they must be cheap and non-blocking. Long work
904    /// should be spawned off (e.g. `tokio::spawn`) by the closure
905    /// itself.
906    ///
907    /// Used by `oxicode-sdk` to bridge observability types
908    /// (`Tracer`, `CostTracker`, `AuditLog`, `Authorizer` /
909    /// `AccessGate`) into the runtime without leaking those types
910    /// into `oxicode-agent`.
911    ///
912    /// # Example
913    ///
914    /// ```ignore
915    /// agent.add_observability_dispatch(|event| match event {
916    ///     AgentEvent::TurnStart { turn_number } => {
917    ///         // open a span
918    ///     }
919    ///     AgentEvent::Usage { input_tokens, output_tokens } => {
920    ///         // record cost
921    ///     }
922    ///     _ => {}
923    /// });
924    /// ```
925    pub fn add_observability_dispatch(&self, f: impl Fn(AgentEvent) + Send + Sync + 'static) {
926        let guard = self.inner.write();
927        let mut slot = guard.observability_dispatch.lock();
928        slot.push(Arc::new(f));
929    }
930
931    /// Request cancellation of the current agent run.
932    ///
933    /// Sets a shared `cancel_flag` that is propagated to the `AgentLoop`'s
934    /// `external_stop` on every event AND polled every ~500ms by the
935    /// streaming loop's periodic check. This ensures cancellation is
936    /// detected quickly even when the provider stream is completely hung
937    /// (no events arriving).
938    pub fn cancel(&self) {
939        self.cancel_flag.store(true, Ordering::SeqCst);
940    }
941
942    /// Toggle auto-retry at runtime (affects the next retry decision in an
943    /// active run; does not interrupt an in-progress retry sleep — use
944    /// [`Self::cancel_auto_retry`] for that).
945    pub fn set_auto_retry(&self, enabled: bool) {
946        self.auto_retry_enabled.store(enabled, Ordering::SeqCst);
947    }
948
949    /// Abort any in-progress auto-retry wait immediately. The running turn
950    /// ends without retrying the error.
951    pub fn cancel_auto_retry(&self) {
952        self.auto_retry_cancel.store(true, Ordering::SeqCst);
953        self.auto_retry_notify.notify_waiters();
954    }
955
956    /// Shared auto-retry state (enabled + cancel + notify) for injection
957    /// into an ephemeral `AgentLoop` at run-start.
958    pub(crate) fn auto_retry_state(
959        &self,
960    ) -> (Arc<AtomicBool>, Arc<AtomicBool>, Arc<tokio::sync::Notify>) {
961        (
962            Arc::clone(&self.auto_retry_enabled),
963            Arc::clone(&self.auto_retry_cancel),
964            Arc::clone(&self.auto_retry_notify),
965        )
966    }
967
968    /// Reset the cancellation flag before starting a new run.
969    pub fn reset_cancel(&self) {
970        self.cancel_flag.store(false, Ordering::SeqCst);
971    }
972
973    /// Apply any pending model switch that was deferred during a running loop.
974    ///
975    /// Called after `run_with_channel_inner` completes (success or error).
976    /// Transforms messages for cross-provider switches and swaps the provider
977    /// so the next run uses the new model.
978    fn apply_pending_model_switch(&self) {
979        let pending = self.pending_model_switch.write().take();
980        if let Some(pending) = pending {
981            tracing::info!(
982                "[AGENT] Applying deferred model switch to '{}' (transform={})",
983                pending.model_id,
984                pending.needs_transform
985            );
986
987            // Transform messages if cross-provider
988            if pending.needs_transform {
989                let messages = self.state.get_state().messages.clone();
990                let transformed =
991                    transform_for_provider(&messages, &pending.old_api, &pending.new_api);
992                self.state.update(|s| {
993                    s.replace_messages(transformed);
994                });
995            }
996
997            // Swap the provider
998            let mut inner = self.inner_mut();
999            inner.provider = pending.provider;
1000            // model_id was already updated in switch_model()
1001        }
1002    }
1003
1004    /// Run the agent, invoking `on_event` for each [`AgentEvent`] produced.
1005    ///
1006    /// Blocking convenience wrapper suitable for callers that prefer a
1007    /// callback-based API over a channel.
1008    pub async fn run_streaming<F>(&self, prompt: String, mut on_event: F) -> Result<Response>
1009    where
1010        F: FnMut(AgentEvent) + Send,
1011    {
1012        let (tx, rx) = std::sync::mpsc::channel::<AgentEvent>();
1013        let result = self.run_with_channel(prompt, tx).await;
1014        while let Ok(event) = rx.recv() {
1015            on_event(event);
1016        }
1017        result
1018    }
1019
1020    // ── Session persistence ────────────────────────────────────────
1021
1022    /// Export the agent state as a JSON value.
1023    ///
1024    /// The serialized state includes conversation messages, token counts,
1025    /// iteration progress, and stop reason. Use [`import_state`] to restore.
1026    ///
1027    /// [`import_state`]: Agent::import_state
1028    pub fn export_state(&self) -> Result<serde_json::Value> {
1029        let state = self.state.get_state();
1030        serde_json::to_value(&state).map_err(|e| Error::msg(format!("State export failed: {}", e)))
1031    }
1032
1033    /// Import agent state from a JSON value.
1034    ///
1035    /// Restores conversation history, token counts, and iteration progress.
1036    /// Typically used together with [`export_state`] for session persistence.
1037    ///
1038    /// [`export_state`]: Agent::export_state
1039    pub fn import_state(&self, value: serde_json::Value) -> Result<()> {
1040        let state: AgentState = serde_json::from_value(value)
1041            .map_err(|e| Error::msg(format!("State import failed: {}", e)))?;
1042        self.state.update(|s| *s = state);
1043        Ok(())
1044    }
1045
1046    // ── Session continuation ───────────────────────────────────────
1047
1048    /// Continue the current session with a new prompt.
1049    ///
1050    /// Unlike `run()`, which can be used on a fresh agent, `continue_with`
1051    /// preserves the existing conversation state and appends the new prompt.
1052    /// This enables multi-turn interactions within the same session.
1053    pub async fn continue_with(&self, prompt: String) -> Result<(Response, Vec<AgentEvent>)> {
1054        let mut events = Vec::new();
1055        let (tx, rx) = std::sync::mpsc::channel::<AgentEvent>();
1056        let result = self.run_with_channel(prompt, tx).await;
1057        while let Ok(event) = rx.recv() {
1058            events.push(event);
1059        }
1060        result.map(|r| (r, events))
1061    }
1062
1063    // ── Tokio-native streaming ─────────────────────────────────────
1064
1065    /// Run the agent with tokio-native event streaming.
1066    ///
1067    /// Returns a `tokio::sync::mpsc::Receiver` for events and a
1068    /// `JoinHandle` for the response. This is the preferred API for
1069    /// async runtimes (WebSocket/SSE gateways, tokio-based servers).
1070    ///
1071    /// # Example
1072    ///
1073    /// ```ignore
1074    /// let (rx, handle) = agent.run_tokio_stream("Explain Rust".into()).await?;
1075    /// while let Some(event) = rx.recv().await {
1076    ///     println!("Event: {:?}", event.type_name());
1077    /// }
1078    /// let response = handle.await??;
1079    /// ```
1080    pub async fn run_tokio_stream(
1081        &self,
1082        prompt: String,
1083    ) -> Result<(
1084        tokio::sync::mpsc::Receiver<AgentEvent>,
1085        tokio::task::JoinHandle<Result<Response>>,
1086    )> {
1087        let (tx, rx) = tokio::sync::mpsc::channel::<AgentEvent>(256);
1088
1089        if self
1090            .is_running
1091            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
1092            .is_err()
1093        {
1094            return Err(Error::msg("Agent is already running"));
1095        }
1096
1097        let should_stop_hook = self.hooks.read().should_stop_after_turn.clone();
1098
1099        let inner = self.inner.read().clone();
1100        let tools = Arc::clone(&self.tools);
1101        let resolver = Arc::clone(&self.resolver);
1102
1103        // Build AgentLoopConfig
1104        let loop_config = crate::agent_loop::config::AgentLoopConfig {
1105            model_id: inner.config.model_id.clone(),
1106            system_prompt: inner.config.system_prompt.clone(),
1107            temperature: inner.config.temperature.unwrap_or(1.0) as f32,
1108            max_tokens: inner.config.max_tokens.unwrap_or(4096) as u32,
1109            tool_execution: crate::config::ToolExecutionMode::Sequential,
1110            compaction_strategy: inner.config.compaction_strategy.clone(),
1111            compaction_instruction: None,
1112            compactor: self.custom_compactor.clone(),
1113            context_window: inner.config.context_window,
1114            session_id: inner.config.session_id.clone(),
1115            transport: None,
1116            compact_on_start: false,
1117            max_retry_delay_ms: None,
1118            auto_retry_enabled: true,
1119            auto_retry_max_attempts: 3,
1120            auto_retry_base_delay_ms: 1000,
1121            workspace_dir: inner.config.workspace_dir.clone(),
1122            provider_options: inner.config.provider_options.clone(),
1123            on_compaction: None,
1124            ttsr_engine: inner.config.ttsr_engine.clone(),
1125            max_tool_result_bytes: inner.config.max_tool_result_bytes,
1126            subagent_runner: inner.config.subagent_runner.clone(),
1127            subagent_depth: inner.config.subagent_depth,
1128            memory: inner.config.memory.clone(),
1129            todo: inner.config.todo.clone(),
1130            todo_reminders_enabled: inner.config.todo_reminders_enabled,
1131            todo_eager_mode: inner.config.todo_eager_mode,
1132            todo_reminders_max: inner.config.todo_reminders_max,
1133            agent_pool: inner.config.agent_pool.clone(),
1134            url_resolver: inner.config.url_resolver.clone(),
1135            lsp: inner.config.lsp.clone(),
1136            mode: inner.config.mode,
1137            snapshot_store: inner.config.snapshot_store.clone(),
1138            ..Default::default()
1139        };
1140
1141        let provider: Arc<dyn Provider> = Arc::clone(&inner.provider);
1142
1143        // Share the SAME SharedState (Arc<RwLock<AgentState>>) with the
1144        // agent loop so that state mutations inside the spawned task are
1145        // visible through self.state() without an explicit sync step.
1146        //
1147        // Unlike run_with_channel_inner which creates a fresh SharedState
1148        // and syncs back on completion, the tokio streaming API cannot
1149        // access `self` inside the `'static` spawned task, so we share
1150        // the underlying Arc instead.
1151        //
1152        // Pre-load current state into the shared Arc (in case it was
1153        // modified by a previous run that used a different SharedState).
1154        let shared_state = self.state.clone();
1155
1156        let mut agent_loop = crate::agent_loop::AgentLoop::new_with_resolver(
1157            provider,
1158            loop_config,
1159            tools,
1160            shared_state.clone(),
1161            resolver,
1162        );
1163
1164        let maybe_hook = should_stop_hook;
1165        let ext_stop = agent_loop.external_stop().clone();
1166        let (ar_enabled, ar_cancel, ar_notify) = self.auto_retry_state();
1167        agent_loop.set_auto_retry_state(ar_enabled, ar_cancel, ar_notify);
1168
1169        // Clone the is_running Arc so the spawned task can clear it.
1170        let is_running_flag = Arc::clone(&self.is_running);
1171
1172        // Snapshot the observability_dispatch list before the spawned
1173        // task. The future is `'static` and cannot borrow `&self`,
1174        // so we take the snapshot at run-start on the regular borrow
1175        // stack and move the resulting Arc-clones into the task.
1176        let dispatch_handlers: Vec<EventDispatchFn> = {
1177            let guard = self.inner.read();
1178            guard.observability_dispatch.lock().clone()
1179        };
1180
1181        let handle = tokio::task::spawn(async move {
1182            // Guard ensures is_running is cleared even if the task panics.
1183            // Without this, a panic mid-stream leaves is_running=true and
1184            // blocks all future runs (the compare_exchange at entry fails).
1185            struct RunningGuard(Arc<AtomicBool>);
1186            impl Drop for RunningGuard {
1187                fn drop(&mut self) {
1188                    self.0.store(false, Ordering::SeqCst);
1189                }
1190            }
1191            let _guard = RunningGuard(is_running_flag);
1192
1193            let result = agent_loop
1194                .run(prompt, move |event: AgentEvent| {
1195                    // Forward to tokio channel (non-blocking)
1196                    let _ = tx.try_send(event.clone());
1197
1198                    // Fan out to SDK-side observability handlers
1199                    // (Tracer, CostTracker, ...).
1200                    for handler in dispatch_handlers.iter() {
1201                        handler(event.clone());
1202                    }
1203                    // Propagate should_stop → external_stop on every event,
1204                    // not just TurnEnd. See run_with_channel_inner for rationale.
1205                    if let Some(hook) = &maybe_hook {
1206                        let ctx = ShouldStopAfterTurnContext {
1207                            message: match &event {
1208                                AgentEvent::TurnEnd {
1209                                    assistant_message: oxicode_ai::Message::Assistant(a),
1210                                    ..
1211                                } => a.clone(),
1212                                _ => oxicode_ai::AssistantMessage::new(
1213                                    oxicode_ai::Api::OpenAiCompletions,
1214                                    "agent",
1215                                    "agent-model",
1216                                ),
1217                            },
1218                            tool_results: match &event {
1219                                AgentEvent::TurnEnd { tool_results, .. } => tool_results.clone(),
1220                                _ => Vec::new(),
1221                            },
1222                            iteration: 0,
1223                        };
1224                        if hook(&ctx) {
1225                            ext_stop.store(true, Ordering::SeqCst);
1226                        }
1227                    }
1228                })
1229                .await;
1230
1231            // _guard dropped here: clears is_running on normal exit or panic.
1232
1233            match result {
1234                Ok(_events) => {
1235                    // State is already shared via the same SharedState Arc,
1236                    // so self.state() will reflect all mutations.
1237                    Ok(Response {
1238                        content: String::new(),
1239                        stop_reason: StopReason::Stop,
1240                    })
1241                }
1242                Err(e) => Err(e),
1243            }
1244        });
1245
1246        Ok((rx, handle))
1247    }
1248}