Skip to main content

ag_agent/channel/
contract.rs

1//! Shared channel trait and provider turn request/result contracts.
2
3use std::future::Future;
4use std::path::PathBuf;
5use std::pin::Pin;
6use std::sync::{Arc, Mutex};
7
8use tokio::sync::mpsc;
9
10use crate::agent::AgentResponse;
11use crate::model::agent::ReasoningLevel;
12use crate::model::turn_prompt::TurnPrompt;
13
14/// Boxed async result used by [`AgentChannel`] trait methods.
15pub type AgentFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
16
17/// Turn initiation mode for [`TurnRequest`].
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum AgentRequestKind {
20    /// Starts a fresh interactive session turn with no prior context.
21    SessionStart,
22    /// Resumes an interactive session turn, optionally replaying transcript
23    /// output into the next prompt.
24    SessionResume {
25        /// Prior session output used for history replay when present.
26        session_output: Option<String>,
27    },
28    /// Runs one utility prompt with utility protocol requirements.
29    ///
30    /// Callers may route this through an isolated one-shot channel or through
31    /// an existing session channel when the utility work needs provider
32    /// conversation continuity without normal post-turn auto-commit handling.
33    UtilityPrompt,
34    /// Reads provider account metadata without creating an agent turn.
35    AccountRead,
36}
37
38impl AgentRequestKind {
39    /// Returns the protocol request profile derived from this request kind.
40    #[must_use]
41    pub fn protocol_profile(&self) -> crate::agent::ProtocolRequestProfile {
42        match self {
43            Self::SessionStart | Self::SessionResume { .. } => {
44                crate::agent::ProtocolRequestProfile::SessionTurn
45            }
46            Self::UtilityPrompt | Self::AccountRead => {
47                crate::agent::ProtocolRequestProfile::UtilityPrompt
48            }
49        }
50    }
51
52    /// Returns whether this request resumes a prior interactive session turn.
53    #[must_use]
54    pub fn is_resume(&self) -> bool {
55        matches!(self, Self::SessionResume { .. })
56    }
57
58    /// Returns transcript output used for history replay, when present.
59    #[must_use]
60    pub fn session_output(&self) -> Option<&str> {
61        match self {
62            Self::SessionStart | Self::UtilityPrompt | Self::AccountRead => None,
63            Self::SessionResume { session_output } => session_output.as_deref(),
64        }
65    }
66}
67
68/// Input payload for one provider-agnostic agent turn.
69#[derive(Debug, Clone)]
70pub struct TurnRequest {
71    /// Session worktree folder where the agent runs.
72    pub folder: PathBuf,
73    /// Live session output buffer for app-server context reconstruction.
74    ///
75    /// App-server clients may read this buffer during a turn to access content
76    /// that was streamed before a prior crash, providing a more complete
77    /// transcript than the snapshot captured at enqueue time. CLI channels
78    /// ignore this field.
79    pub live_session_output: Option<Arc<Mutex<String>>>,
80    /// Provider-specific model identifier.
81    pub model: String,
82    /// Canonical request kind that drives transport behavior and protocol
83    /// semantics for this turn.
84    pub request_kind: AgentRequestKind,
85    /// Structured prompt payload for the turn.
86    pub prompt: TurnPrompt,
87    /// Provider-native conversation identifier loaded from persistence.
88    ///
89    /// When present, app-server channels forward this to the provider runtime
90    /// so it can attempt native context resume. CLI channels ignore this field.
91    pub provider_conversation_id: Option<String>,
92    /// Persisted provider-native conversation id that already received the
93    /// full instruction bootstrap.
94    ///
95    /// App-server channels use this to choose between a full bootstrap and a
96    /// compact reminder for the active provider context. CLI channels ignore
97    /// this field.
98    pub persisted_instruction_conversation_id: Option<String>,
99    /// Reasoning effort preference for the turn.
100    ///
101    /// Ignored by providers/models that do not support reasoning effort.
102    pub reasoning_level: ReasoningLevel,
103}
104
105/// Incremental event emitted during one agent turn.
106///
107/// Events are sent through an [`mpsc::UnboundedSender`] as the turn
108/// progresses, enabling transient loader updates without appending partial turn
109/// output into the persisted transcript.
110#[derive(Clone, Debug, PartialEq)]
111pub enum TurnEvent {
112    /// A streamed thinking/planning or tool-status fragment shown in the
113    /// transient loader.
114    ThoughtDelta(String),
115    /// The turn completed successfully with final token counts.
116    Completed {
117        /// Whether the provider reset its context for this turn.
118        context_reset: bool,
119        /// Input token count for the turn.
120        input_tokens: u64,
121        /// Output token count for the turn.
122        output_tokens: u64,
123    },
124    /// The turn failed with an error description.
125    Failed(String),
126    /// A child process PID update.
127    ///
128    /// Sent by CLI channels immediately after spawning the child process
129    /// (`Some(pid)`) and again after the child exits (`None`). Consumers
130    /// update the shared PID slot used by cancellation signals.
131    PidUpdate(Option<u32>),
132}
133
134/// Normalized result returned when one agent turn completes successfully.
135#[derive(Debug)]
136pub struct TurnResult {
137    /// Parsed agent response containing structured protocol messages.
138    pub assistant_message: AgentResponse,
139    /// Whether the provider reset its context to complete this turn.
140    pub context_reset: bool,
141    /// Input token count for the turn.
142    pub input_tokens: u64,
143    /// Output token count for the turn.
144    pub output_tokens: u64,
145    /// Provider-native conversation identifier observed after the turn.
146    ///
147    /// App-server providers return this so the worker can persist it for
148    /// future runtime restarts. CLI channels always return `None`.
149    pub provider_conversation_id: Option<String>,
150}
151
152/// Opaque reference to an active agent session.
153pub struct SessionRef {
154    /// Stable session identifier.
155    pub session_id: String,
156}
157
158/// Input payload for initiating a new agent session.
159pub struct StartSessionRequest {
160    /// Session worktree folder.
161    pub folder: PathBuf,
162    /// Stable session identifier.
163    pub session_id: String,
164}
165
166/// Typed error returned by [`AgentChannel`] operations.
167///
168/// Discriminates failure causes so the app layer can route errors without
169/// parsing formatted messages.
170#[derive(Debug, thiserror::Error)]
171pub enum AgentError {
172    /// An app-server infrastructure failure propagated from a persistent
173    /// provider runtime.
174    #[error(transparent)]
175    AppServer(#[from] crate::app_server::AppServerError),
176
177    /// A CLI backend command or process execution failure.
178    #[error("{0}")]
179    Backend(String),
180
181    /// The user explicitly interrupted the active turn.
182    #[error("{0}")]
183    InterruptedByUser(String),
184
185    /// A subprocess IO error such as a spawn failure or unavailable pipe.
186    #[error("{0}")]
187    Io(String),
188}
189
190/// Provider-agnostic session channel for executing agent turns.
191///
192/// Implementations bridge a specific transport - CLI subprocess or app-server
193/// RPC - to the unified [`TurnEvent`] stream consumed by session workers. The
194/// trait is object-safe so it can be held as `Arc<dyn AgentChannel>`.
195#[cfg_attr(any(test, feature = "test-utils"), mockall::automock)]
196pub trait AgentChannel: Send + Sync {
197    /// Initialises a provider session for the given session identifier.
198    ///
199    /// Implementations that do not maintain persistent sessions return
200    /// immediately with a [`SessionRef`] wrapping the supplied identifier.
201    fn start_session(
202        &self,
203        req: StartSessionRequest,
204    ) -> AgentFuture<Result<SessionRef, AgentError>>;
205
206    /// Executes one prompt turn and streams incremental events to `events`.
207    ///
208    /// Implementations may emit [`TurnEvent::ThoughtDelta`] values for
209    /// transient loader updates. Final transcript output is derived from the
210    /// returned [`TurnResult`] after the turn finishes.
211    ///
212    /// # Errors
213    /// Returns [`AgentError`] when the turn cannot be executed (spawn failure,
214    /// transport error) or is interrupted by a signal.
215    fn run_turn(
216        &self,
217        session_id: String,
218        req: TurnRequest,
219        events: mpsc::UnboundedSender<TurnEvent>,
220    ) -> AgentFuture<Result<TurnResult, AgentError>>;
221
222    /// Tears down the provider session associated with `session_id`.
223    ///
224    /// Implementations that do not maintain persistent sessions treat this as
225    /// a no-op and always return `Ok(())`.
226    fn shutdown_session(&self, session_id: String) -> AgentFuture<Result<(), AgentError>>;
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    /// Ensures session request kinds derive the session-turn protocol
235    /// profile.
236    fn test_agent_request_kind_session_variants_use_session_protocol_profile() {
237        // Arrange
238        let start = AgentRequestKind::SessionStart;
239        let resume = AgentRequestKind::SessionResume {
240            session_output: Some("prior output".to_string()),
241        };
242
243        // Act
244        let start_profile = start.protocol_profile();
245        let resume_profile = resume.protocol_profile();
246
247        // Assert
248        assert_eq!(
249            start_profile,
250            crate::agent::ProtocolRequestProfile::SessionTurn
251        );
252        assert_eq!(
253            resume_profile,
254            crate::agent::ProtocolRequestProfile::SessionTurn
255        );
256    }
257
258    #[test]
259    /// Ensures utility prompts derive the utility protocol profile and never
260    /// expose replay output.
261    fn test_agent_request_kind_utility_prompt_uses_utility_protocol_profile() {
262        // Arrange
263        let request_kind = AgentRequestKind::UtilityPrompt;
264
265        // Act
266        let protocol_profile = request_kind.protocol_profile();
267        let session_output = request_kind.session_output();
268
269        // Assert
270        assert_eq!(
271            protocol_profile,
272            crate::agent::ProtocolRequestProfile::UtilityPrompt
273        );
274        assert_eq!(session_output, None);
275    }
276
277    #[test]
278    /// Ensures account-read requests are non-session utility requests without
279    /// replay output.
280    fn test_agent_request_kind_account_read_uses_utility_protocol_profile() {
281        // Arrange
282        let request_kind = AgentRequestKind::AccountRead;
283
284        // Act
285        let protocol_profile = request_kind.protocol_profile();
286        let session_output = request_kind.session_output();
287
288        // Assert
289        assert_eq!(
290            protocol_profile,
291            crate::agent::ProtocolRequestProfile::UtilityPrompt
292        );
293        assert_eq!(session_output, None);
294    }
295}