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