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