Skip to main content

algocline_core/execution/
service.rs

1//! `ExecutionService` trait — the primary verb surface of the new service layer.
2
3use async_trait::async_trait;
4
5use super::cancel::CancelReason;
6use super::error::{AwaitError, CancelError, ObserveError, ResumeError, SpawnError, StateError};
7use super::progress::ObserverHandle;
8use super::resume::{ResumeOutcome, TerminalOutcome};
9use super::session_id::SessionId;
10use super::spec::SessionSpec;
11use super::state::ExecutionState;
12
13/// Pure service trait for managing execution sessions.
14///
15/// Implementors must ensure the following invariants hold:
16///
17/// 1. **Wire-concept exclusion**: no MCP/rmcp types, `progressToken`, `_meta` fields,
18///    `notifications/*` paths, or `mcp_`-prefixed identifiers are referenced by this
19///    trait or its supporting types.
20/// 2. **`SessionId` is the sole handle**: all verbs after `spawn` accept only a
21///    `&SessionId`; no session-internal handles leak through the API.
22/// 3. **Pure value types**: all inputs and outputs are value types (no callbacks,
23///    no `Arc`-leaked internals).
24/// 4. **Cooperative cancellation only**: `cancel` signals the session via a
25///    `CancellationToken`; no `JoinHandle::abort()` or process kill may be invoked.
26/// 5. **Sink-free progress fan-out**: `observe` returns a `broadcast::Receiver`
27///    wrapper that is valid without any pre-registered observer.  Multiple concurrent
28///    subscribers each receive the full event stream independently.
29/// 6. **Immediate `SessionId` return**: `spawn` returns the `SessionId` before
30///    execution completes.
31/// 7. **Single trait, all verbs**: CLI, Server, and programmatic callers all use
32///    this single trait.
33///
34/// # Async vs sync verbs
35/// All verbs are `async fn` except `observe`, which is a synchronous `fn`.
36/// `observe` is sync because `broadcast::Sender::subscribe()` is itself synchronous
37/// and does not perform I/O — making it `async` would force callers to `.await`
38/// without reason and obscure the sink-free subscription semantics.
39#[async_trait]
40pub trait ExecutionService: Send + Sync {
41    /// Spawn a new execution session from the given specification.
42    ///
43    /// Returns the `SessionId` immediately; execution proceeds in the background.
44    ///
45    /// # Errors
46    /// - [`SpawnError::Engine`] — the engine failed to initialize the session.
47    /// - [`SpawnError::InvalidSpec`] — the provided `SessionSpec` is malformed.
48    async fn spawn(&self, spec: SessionSpec) -> Result<SessionId, SpawnError>;
49
50    /// Query the current state of a session.
51    ///
52    /// # Errors
53    /// - [`StateError::NotFound`] — no session with the given id exists.
54    async fn state(&self, id: &SessionId) -> Result<ExecutionState, StateError>;
55
56    /// Resume a paused session by providing LLM responses.
57    ///
58    /// The `payload` kind must match the pause kind of the session (single vs batch).
59    ///
60    /// # Errors
61    /// - [`ResumeError::NotFound`] — no session with the given id exists.
62    /// - [`ResumeError::NotPaused`] — the session is not in the `Paused` state.
63    /// - [`ResumeError::AlreadyCancelled`] — the session is already cancelled.
64    /// - [`ResumeError::FeedError`] — the underlying feed operation failed.
65    async fn resume(
66        &self,
67        id: &SessionId,
68        payload: super::resume::ResumePayload,
69    ) -> Result<ResumeOutcome, ResumeError>;
70
71    /// Request cooperative cancellation of a session.
72    ///
73    /// `cancel` is idempotent: calling it on a session already in a terminal state
74    /// (`Done`, `Failed`, `Cancelled`) returns `Ok(())`.  The only error case is when
75    /// no session with the given id exists.
76    ///
77    /// Cancellation is delivered via a `CancellationToken`; the engine checks at
78    /// exactly four checkpoints (A/B/C/D) and transitions cooperatively to `Cancelled`.
79    /// No `JoinHandle::abort()` or process kill path is introduced.
80    ///
81    /// # Errors
82    /// - [`CancelError::NotFound`] — no session with the given id exists.
83    async fn cancel(&self, id: &SessionId, reason: CancelReason) -> Result<(), CancelError>;
84
85    /// Subscribe to the progress event stream for a session.
86    ///
87    /// Returns a `Box<dyn ObserverHandle>` that delivers [`super::progress::ProgressEvent`]
88    /// events from the session's broadcast channel.  Multiple concurrent subscribers
89    /// each receive the full event stream independently (sink-free fan-out).
90    ///
91    /// This is a **synchronous** `fn` (not `async`) because
92    /// `broadcast::Sender::subscribe()` is synchronous and performs no I/O.
93    ///
94    /// # Errors
95    /// - [`ObserveError::NotFound`] — no session with the given id exists.
96    fn observe(&self, id: &SessionId) -> Result<Box<dyn ObserverHandle>, ObserveError>;
97
98    /// Await the terminal state of a session.
99    ///
100    /// Blocks (asynchronously) until the session transitions to `Done`, `Cancelled`,
101    /// or `Failed`.  Returns the terminal outcome.
102    ///
103    /// # Errors
104    /// - [`AwaitError::NotFound`] — no session with the given id exists.
105    /// - [`AwaitError::Joined`] — the background task failed unexpectedly.
106    async fn await_terminal(&self, id: &SessionId) -> Result<TerminalOutcome, AwaitError>;
107}