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    /// Current personality body included during a full prompt bootstrap.
58    pub personality_prompt: Option<&'a str>,
59    /// User prompt to send.
60    pub prompt: &'a str,
61    /// Reasoning effort preference for this turn.
62    ///
63    /// Ignored by backends/models that do not support reasoning effort.
64    pub reasoning_level: ReasoningLevel,
65    /// Canonical request kind that drives execution and protocol semantics.
66    pub request_kind: &'a AgentRequestKind,
67    /// Replayable transcript text captured when the turn was queued.
68    pub replay_transcript: Option<&'a str>,
69}
70
71/// Error type for backend setup and command construction failures.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum AgentBackendError {
74    /// One-time backend setup failure.
75    Setup(String),
76    /// Per-command build failure.
77    CommandBuild(String),
78}
79
80impl fmt::Display for AgentBackendError {
81    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82        match self {
83            Self::Setup(message) | Self::CommandBuild(message) => {
84                write!(formatter, "{message}")
85            }
86        }
87    }
88}
89
90impl Error for AgentBackendError {}
91
92/// Builds and configures external agent CLI commands.
93#[cfg_attr(any(test, feature = "test-utils"), mockall::automock)]
94pub trait AgentBackend: Send + Sync {
95    /// Performs one-time setup in an agent folder before first run.
96    ///
97    /// # Errors
98    /// Returns an error when one-time backend setup cannot be completed.
99    fn setup(&self, folder: &Path) -> Result<(), AgentBackendError>;
100
101    /// Builds one provider transport command.
102    ///
103    /// CLI-backed providers return the per-turn subprocess command. App-server
104    /// providers return the long-lived runtime command that owns later RPC
105    /// turn execution.
106    ///
107    /// # Errors
108    /// Returns an error when prompt rendering or provider argument
109    /// construction fails.
110    fn build_command<'request>(
111        &'request self,
112        request: BuildCommandRequest<'request>,
113    ) -> Result<Command, AgentBackendError>;
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_agent_transport_app_server_uses_app_server() {
122        // Arrange
123        let transport = AgentTransport::AppServer;
124
125        // Act
126        let result = transport.uses_app_server();
127
128        // Assert
129        assert!(result);
130    }
131
132    #[test]
133    fn test_agent_transport_cli_does_not_use_app_server() {
134        // Arrange
135        let transport = AgentTransport::Cli;
136
137        // Act
138        let result = transport.uses_app_server();
139
140        // Assert
141        assert!(!result);
142    }
143
144    #[test]
145    fn test_agent_backend_error_setup_displays_message() {
146        // Arrange
147        let error = AgentBackendError::Setup("setup failed".to_string());
148
149        // Act
150        let display = format!("{error}");
151
152        // Assert
153        assert_eq!(display, "setup failed");
154    }
155
156    #[test]
157    fn test_agent_backend_error_command_build_displays_message() {
158        // Arrange
159        let error = AgentBackendError::CommandBuild("build failed".to_string());
160
161        // Act
162        let display = format!("{error}");
163
164        // Assert
165        assert_eq!(display, "build failed");
166    }
167
168    #[test]
169    fn test_agent_backend_error_implements_std_error() {
170        // Arrange
171        let error = AgentBackendError::Setup("test error".to_string());
172
173        // Act
174        let std_error: &dyn Error = &error;
175
176        // Assert
177        assert_eq!(std_error.to_string(), "test error");
178        assert!(std_error.source().is_none());
179    }
180
181    #[test]
182    fn test_agent_backend_error_setup_and_command_build_are_distinct() {
183        // Arrange
184        let setup_error = AgentBackendError::Setup("failure".to_string());
185        let build_error = AgentBackendError::CommandBuild("failure".to_string());
186
187        // Act / Assert
188        assert_ne!(setup_error, build_error);
189    }
190}