Skip to main content

ag_agent/agent/
backend.rs

1use std::error::Error;
2use std::fmt;
3use std::path::Path;
4use std::process::Command;
5
6use ag_protocol::TurnPromptAttachment;
7
8use crate::channel::AgentRequestKind;
9use crate::model::agent::ReasoningLevel;
10use crate::model::session::SpeedMode;
11
12/// Transport runtime used to execute turns for one backend.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum AgentTransport {
15    /// Provider runs through persistent app-server sessions.
16    AppServer,
17    /// Provider runs as direct CLI subprocess commands.
18    Cli,
19}
20
21impl AgentTransport {
22    /// Returns whether this transport uses app-server sessions.
23    pub fn uses_app_server(self) -> bool {
24        matches!(self, Self::AppServer)
25    }
26}
27
28/// Prompt delivery mode used by one provider backend.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub(crate) enum AgentPromptTransport {
31    /// Prompt is passed inline through argv.
32    Argv,
33    /// Prompt is streamed through stdin.
34    Stdin,
35}
36
37/// App-server thought-stream classification policy for one provider.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub(crate) enum AppServerThoughtPolicy {
40    /// Provider does not expose dedicated thought phases.
41    None,
42    /// Provider uses phase labels to distinguish thought chunks.
43    PhaseLabel,
44}
45
46/// Request payload used to build provider transport commands.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct BuildCommandRequest<'a> {
49    /// Ordered local image attachments referenced from the prompt body.
50    pub attachments: &'a [TurnPromptAttachment],
51    /// Working directory where the command will run.
52    pub folder: &'a Path,
53    /// Main repository checkout that must remain read-only during the turn,
54    /// when Agentty can resolve it.
55    pub main_checkout_root: Option<&'a Path>,
56    /// Provider-specific model identifier.
57    pub model: &'a str,
58    /// Current personality body included during a full prompt bootstrap.
59    pub personality_prompt: Option<&'a str>,
60    /// User prompt to send.
61    pub prompt: &'a str,
62    /// Reasoning effort preference for this turn.
63    ///
64    /// Ignored by backends/models that do not support reasoning effort.
65    pub reasoning_level: ReasoningLevel,
66    /// Canonical request kind that drives execution and protocol semantics.
67    pub request_kind: &'a AgentRequestKind,
68    /// Replayable transcript text captured when the turn was queued.
69    pub replay_transcript: Option<&'a str>,
70    /// Response-speed preference for this turn.
71    pub speed_mode: SpeedMode,
72}
73
74/// Error type for backend setup and command construction failures.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum AgentBackendError {
77    /// One-time backend setup failure.
78    Setup(String),
79    /// Per-command build failure.
80    CommandBuild(String),
81}
82
83impl fmt::Display for AgentBackendError {
84    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85        match self {
86            Self::Setup(message) | Self::CommandBuild(message) => {
87                write!(formatter, "{message}")
88            }
89        }
90    }
91}
92
93impl Error for AgentBackendError {}
94
95/// Builds and configures external agent CLI commands.
96#[cfg_attr(any(test, feature = "test-utils"), mockall::automock)]
97pub trait AgentBackend: Send + Sync {
98    /// Performs one-time setup in an agent folder before first run.
99    ///
100    /// # Errors
101    /// Returns an error when one-time backend setup cannot be completed.
102    fn setup(&self, folder: &Path) -> Result<(), AgentBackendError>;
103
104    /// Builds one provider transport command.
105    ///
106    /// CLI-backed providers return the per-turn subprocess command. App-server
107    /// providers return the long-lived runtime command that owns later RPC
108    /// turn execution.
109    ///
110    /// # Errors
111    /// Returns an error when prompt rendering or provider argument
112    /// construction fails.
113    fn build_command<'request>(
114        &'request self,
115        request: BuildCommandRequest<'request>,
116    ) -> Result<Command, AgentBackendError>;
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_agent_transport_app_server_uses_app_server() {
125        // Arrange
126        let transport = AgentTransport::AppServer;
127
128        // Act
129        let result = transport.uses_app_server();
130
131        // Assert
132        assert!(result);
133    }
134
135    #[test]
136    fn test_agent_transport_cli_does_not_use_app_server() {
137        // Arrange
138        let transport = AgentTransport::Cli;
139
140        // Act
141        let result = transport.uses_app_server();
142
143        // Assert
144        assert!(!result);
145    }
146
147    #[test]
148    fn test_agent_backend_error_setup_displays_message() {
149        // Arrange
150        let error = AgentBackendError::Setup("setup failed".to_string());
151
152        // Act
153        let display = format!("{error}");
154
155        // Assert
156        assert_eq!(display, "setup failed");
157    }
158
159    #[test]
160    fn test_agent_backend_error_command_build_displays_message() {
161        // Arrange
162        let error = AgentBackendError::CommandBuild("build failed".to_string());
163
164        // Act
165        let display = format!("{error}");
166
167        // Assert
168        assert_eq!(display, "build failed");
169    }
170
171    #[test]
172    fn test_agent_backend_error_implements_std_error() {
173        // Arrange
174        let error = AgentBackendError::Setup("test error".to_string());
175
176        // Act
177        let std_error: &dyn Error = &error;
178
179        // Assert
180        assert_eq!(std_error.to_string(), "test error");
181        assert!(std_error.source().is_none());
182    }
183
184    #[test]
185    fn test_agent_backend_error_setup_and_command_build_are_distinct() {
186        // Arrange
187        let setup_error = AgentBackendError::Setup("failure".to_string());
188        let build_error = AgentBackendError::CommandBuild("failure".to_string());
189
190        // Act / Assert
191        assert_ne!(setup_error, build_error);
192    }
193}