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}