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