1use crate::agent_api::{Agent, SessionOptions};
8use crate::agent_protocol::{
9 AgentProtocolChangeSetRequestV1, AgentProtocolChangeSetV1, AgentProtocolCommandReceiptV1,
10 AgentProtocolCommandV1, AgentProtocolError, AgentProtocolEventPageRequestV1,
11 AgentProtocolEventPageV1, AgentProtocolRunIdentityV1,
12};
13use crate::agent_protocol_host::{AgentProtocolHost, AgentProtocolHostError};
14use crate::error::CodeError;
15use crate::release::{
16 agent_harness_compatibility_v1, AgentReleaseError, AgentReleaseManifest, AGENT_PROTOCOL_V1,
17};
18use std::collections::HashMap;
19use std::path::{Path, PathBuf};
20use std::sync::atomic::{AtomicBool, Ordering};
21use std::sync::Arc;
22use thiserror::Error;
23use tokio::sync::{Mutex, RwLock};
24
25pub const AGENT_PROTOCOL_HARNESS_MAX_SESSIONS: usize = 1_024;
27
28#[derive(Debug, Error)]
30pub enum AgentProtocolHarnessError {
31 #[error(transparent)]
32 Protocol(#[from] AgentProtocolError),
33 #[error(transparent)]
34 Release(#[from] AgentReleaseError),
35 #[error(transparent)]
36 Host(#[from] AgentProtocolHostError),
37 #[error(transparent)]
38 Code(#[from] CodeError),
39 #[error("A3S Code Harness session was not found")]
40 SessionNotFound,
41 #[error("A3S Code Harness session capacity is exhausted")]
42 SessionCapacity,
43 #[error("A3S Code Harness is draining or stopped")]
44 Closed,
45 #[error("A3S Code Harness workspace isolation failed: {0}")]
46 Workspace(String),
47}
48
49impl AgentProtocolHarnessError {
50 pub const fn code(&self) -> &'static str {
51 match self {
52 Self::Protocol(error) => error.code(),
53 Self::Release(error) => error.code(),
54 Self::Host(error) => error.code(),
55 Self::Code(error) => error.code(),
56 Self::SessionNotFound => "a3s.code.agent_protocol.session_not_found",
57 Self::SessionCapacity => "a3s.code.agent_protocol.session_capacity",
58 Self::Closed => "a3s.code.agent_protocol.harness_closed",
59 Self::Workspace(_) => "a3s.code.agent_protocol.workspace_isolation",
60 }
61 }
62}
63
64struct HarnessSessionEntry {
65 host: Arc<AgentProtocolHost>,
66 _workspace: HarnessSessionWorkspace,
67}
68
69enum HarnessSessionWorkspace {
70 Shared(PathBuf),
71 Isolated {
72 source: PathBuf,
73 path: PathBuf,
74 _temporary_root: tempfile::TempDir,
75 },
76}
77
78impl HarnessSessionWorkspace {
79 async fn prepare(source: PathBuf) -> Result<Self, AgentProtocolHarnessError> {
80 tokio::task::spawn_blocking(move || {
81 if !crate::git::is_git_repo(&source) {
82 return Ok(Self::Shared(source));
83 }
84 let temporary_root = tempfile::Builder::new()
85 .prefix("a3s-code-harness-session-")
86 .tempdir()
87 .map_err(|error| AgentProtocolHarnessError::Workspace(error.to_string()))?;
88 let path = temporary_root.path().join("workspace");
89 crate::git::create_isolated_worktree(&source, &path)
90 .map_err(|error| AgentProtocolHarnessError::Workspace(error.to_string()))?;
91 Ok(Self::Isolated {
92 source,
93 path,
94 _temporary_root: temporary_root,
95 })
96 })
97 .await
98 .map_err(|error| AgentProtocolHarnessError::Workspace(error.to_string()))?
99 }
100
101 fn path(&self) -> &Path {
102 match self {
103 Self::Shared(path) | Self::Isolated { path, .. } => path,
104 }
105 }
106}
107
108impl Drop for HarnessSessionWorkspace {
109 fn drop(&mut self) {
110 if let Self::Isolated { source, path, .. } = self {
111 if let Err(error) = crate::git::remove_isolated_worktree(source, path) {
112 tracing::warn!(%error, workspace = %path.display(), "could not remove Agent Harness session worktree");
113 }
114 }
115 }
116}
117
118pub struct AgentProtocolHarness {
126 manifest: Arc<AgentReleaseManifest>,
127 agent: Arc<Agent>,
128 workspace: String,
129 session_options: SessionOptions,
130 max_sessions: usize,
131 sessions: RwLock<HashMap<String, Arc<HarnessSessionEntry>>>,
132 admission: Mutex<()>,
133 closed: AtomicBool,
134}
135
136impl std::fmt::Debug for AgentProtocolHarness {
137 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 formatter
139 .debug_struct("AgentProtocolHarness")
140 .field("agent_release_identity", &self.manifest.artifact().digest())
141 .field("manifest_identity", &self.manifest.identity())
142 .field("workspace", &self.workspace)
143 .field("max_sessions", &self.max_sessions)
144 .field("closed", &self.closed.load(Ordering::Acquire))
145 .finish_non_exhaustive()
146 }
147}
148
149impl AgentProtocolHarness {
150 pub fn new(
152 manifest: AgentReleaseManifest,
153 agent: Arc<Agent>,
154 workspace: impl Into<String>,
155 ) -> Result<Self, AgentProtocolHarnessError> {
156 manifest.verify_compatibility(&agent_harness_compatibility_v1())?;
157 if manifest.protocol() != AGENT_PROTOCOL_V1 {
158 return Err(AgentProtocolHostError::ReleaseProtocolMismatch.into());
159 }
160 Ok(Self {
161 manifest: Arc::new(manifest),
162 agent,
163 workspace: workspace.into(),
164 session_options: SessionOptions::new(),
165 max_sessions: AGENT_PROTOCOL_HARNESS_MAX_SESSIONS,
166 sessions: RwLock::new(HashMap::new()),
167 admission: Mutex::new(()),
168 closed: AtomicBool::new(false),
169 })
170 }
171
172 pub fn with_session_options(mut self, options: SessionOptions) -> Self {
177 self.session_options = options;
178 self.session_options.session_id = None;
179 self.session_options.auto_save = true;
180 self
181 }
182
183 pub fn with_max_sessions(
185 mut self,
186 max_sessions: usize,
187 ) -> Result<Self, AgentProtocolHarnessError> {
188 if max_sessions == 0 {
189 return Err(AgentProtocolHarnessError::SessionCapacity);
190 }
191 self.max_sessions = max_sessions;
192 Ok(self)
193 }
194
195 pub fn manifest(&self) -> &AgentReleaseManifest {
196 &self.manifest
197 }
198
199 pub fn agent_release_identity(&self) -> &str {
200 self.manifest.artifact().digest()
201 }
202
203 pub fn max_sessions(&self) -> usize {
204 self.max_sessions
205 }
206
207 pub fn is_closed(&self) -> bool {
208 self.closed.load(Ordering::Acquire)
209 }
210
211 pub async fn session_count(&self) -> usize {
212 self.sessions.read().await.len()
213 }
214
215 pub async fn execute(
217 &self,
218 command: &AgentProtocolCommandV1,
219 ) -> Result<AgentProtocolCommandReceiptV1, AgentProtocolHarnessError> {
220 command.validate()?;
221 let create_if_missing = matches!(
222 command,
223 AgentProtocolCommandV1::Start { .. } | AgentProtocolCommandV1::Recover { .. }
224 );
225 let host = self.host_for(command.identity(), create_if_missing).await?;
226 host.execute(command).await.map_err(Into::into)
227 }
228
229 pub async fn event_page(
231 &self,
232 request: &AgentProtocolEventPageRequestV1,
233 ) -> Result<AgentProtocolEventPageV1, AgentProtocolHarnessError> {
234 request.validate()?;
235 let host = self.host_for(&request.identity, false).await?;
236 host.event_page_for(request).await.map_err(Into::into)
237 }
238
239 pub async fn change_set(
241 &self,
242 request: &AgentProtocolChangeSetRequestV1,
243 ) -> Result<AgentProtocolChangeSetV1, AgentProtocolHarnessError> {
244 request.validate()?;
245 let host = self.host_for(&request.identity, false).await?;
246 host.change_set_for(request).await.map_err(Into::into)
247 }
248
249 pub async fn close(&self) {
251 if self.closed.swap(true, Ordering::AcqRel) {
252 return;
253 }
254 let _admission = self.admission.lock().await;
255 self.agent.close().await;
256 self.sessions.write().await.clear();
257 }
258
259 async fn host_for(
260 &self,
261 identity: &AgentProtocolRunIdentityV1,
262 create_if_missing: bool,
263 ) -> Result<Arc<AgentProtocolHost>, AgentProtocolHarnessError> {
264 identity.validate()?;
265 if identity.agent_release_identity != self.manifest.artifact().digest() {
266 return Err(AgentProtocolHostError::ReleaseMismatch.into());
267 }
268 if self.is_closed() {
269 return Err(AgentProtocolHarnessError::Closed);
270 }
271 if let Some(host) = self
272 .sessions
273 .read()
274 .await
275 .get(&identity.session_id)
276 .map(|entry| Arc::clone(&entry.host))
277 {
278 return Ok(host);
279 }
280
281 let _admission = self.admission.lock().await;
282 if self.is_closed() {
283 return Err(AgentProtocolHarnessError::Closed);
284 }
285 if let Some(host) = self
286 .sessions
287 .read()
288 .await
289 .get(&identity.session_id)
290 .map(|entry| Arc::clone(&entry.host))
291 {
292 return Ok(host);
293 }
294 if self.sessions.read().await.len() >= self.max_sessions {
295 return Err(AgentProtocolHarnessError::SessionCapacity);
296 }
297
298 let workspace = HarnessSessionWorkspace::prepare(PathBuf::from(&self.workspace)).await?;
299 let options = self
300 .session_options
301 .clone()
302 .with_session_id(&identity.session_id)
303 .with_auto_save(true);
304 let session = self
305 .agent
306 .open_protocol_session_async(
307 workspace.path().to_string_lossy().into_owned(),
308 options,
309 create_if_missing,
310 )
311 .await?
312 .ok_or(AgentProtocolHarnessError::SessionNotFound)?;
313 let host = Arc::new(AgentProtocolHost::from_manifest(
314 &self.manifest,
315 Arc::new(session),
316 )?);
317 self.sessions.write().await.insert(
318 identity.session_id.clone(),
319 Arc::new(HarnessSessionEntry {
320 host: Arc::clone(&host),
321 _workspace: workspace,
322 }),
323 );
324 Ok(host)
325 }
326}