Skip to main content

ag_agent/channel/
contract.rs

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