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