Skip to main content

a3s_code_core/
agent_protocol_harness.rs

1//! Code-owned multi-session kernel for the native `a3s code harness` process.
2//!
3//! The executable supplies HTTP and health transport. This kernel owns only
4//! admission into existing `Agent`/`AgentSession` state and deliberately has
5//! no parallel run store, scheduler, event journal, or checkpoint authority.
6
7use crate::agent_api::{Agent, SessionOptions};
8use crate::agent_protocol::{
9    AgentProtocolChangeSetRequestV1, AgentProtocolChangeSetV1, AgentProtocolCommandReceiptV1,
10    AgentProtocolCommandV1, AgentProtocolError, AgentProtocolEventPageRequestV1,
11    AgentProtocolEventPageV1, AgentProtocolRunIdentityV1, AgentProtocolRunRecoverExactV1,
12};
13use crate::agent_protocol_host::{
14    AgentProtocolExactRecoveryError, AgentProtocolHost, AgentProtocolHostError,
15};
16use crate::error::CodeError;
17use crate::release::{agent_harness_compatibility_v1, AgentReleaseError, AgentReleaseManifest};
18use crate::session_checkpoint::{SessionCheckpointError, SessionCheckpointExportV1};
19use std::collections::HashMap;
20use std::path::{Path, PathBuf};
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::sync::Arc;
23use thiserror::Error;
24use tokio::sync::{Mutex, RwLock};
25
26/// Finite default number of conversation sessions retained by one Harness.
27pub const AGENT_PROTOCOL_HARNESS_MAX_SESSIONS: usize = 1_024;
28
29// Test-only force switch. Always compiled so `--no-cfg-coverage` does not leave
30// a dead stub diluting line coverage; production never flips this atomic.
31static TEST_FORCE_INVALID_RECOVERY_BINDING: AtomicBool = AtomicBool::new(false);
32
33/// Force the next recovery capability-binding check to fail as `InvalidField`.
34pub fn force_invalid_recovery_binding_for_test() {
35    TEST_FORCE_INVALID_RECOVERY_BINDING.store(true, Ordering::SeqCst);
36}
37
38/// Stable failures returned by the Code-owned multi-session Harness kernel.
39#[derive(Debug, Error)]
40pub enum AgentProtocolHarnessError {
41    #[error(transparent)]
42    Protocol(#[from] AgentProtocolError),
43    #[error(transparent)]
44    Release(#[from] AgentReleaseError),
45    #[error(transparent)]
46    Host(#[from] AgentProtocolHostError),
47    #[error(transparent)]
48    Code(#[from] CodeError),
49    #[error("A3S Code Harness session was not found")]
50    SessionNotFound,
51    #[error("A3S Code Harness session capacity is exhausted")]
52    SessionCapacity,
53    #[error("A3S Code Harness is draining or stopped")]
54    Closed,
55    #[error("A3S Code Harness workspace isolation failed: {0}")]
56    Workspace(String),
57}
58
59impl AgentProtocolHarnessError {
60    pub const fn code(&self) -> &'static str {
61        match self {
62            Self::Protocol(error) => error.code(),
63            Self::Release(error) => error.code(),
64            Self::Host(error) => error.code(),
65            Self::Code(error) => error.code(),
66            Self::SessionNotFound => "a3s.code.agent_protocol.session_not_found",
67            Self::SessionCapacity => "a3s.code.agent_protocol.session_capacity",
68            Self::Closed => "a3s.code.agent_protocol.harness_closed",
69            Self::Workspace(_) => "a3s.code.agent_protocol.workspace_isolation",
70        }
71    }
72}
73
74/// Failures specific to one Harness-visible portable-checkpoint admission and
75/// its exact logical recovery.
76#[derive(Debug, Error)]
77pub enum AgentProtocolCheckpointRecoveryError {
78    #[error(transparent)]
79    Harness(#[from] AgentProtocolHarnessError),
80    #[error(transparent)]
81    Exact(#[from] AgentProtocolExactRecoveryError),
82    #[error(transparent)]
83    Checkpoint(#[from] SessionCheckpointError),
84    #[error("A3S Code Harness session is already active without the exact target Run")]
85    SessionAlreadyActive,
86}
87
88impl AgentProtocolCheckpointRecoveryError {
89    pub const fn code(&self) -> &'static str {
90        match self {
91            Self::Harness(error) => error.code(),
92            Self::Exact(error) => error.code(),
93            Self::Checkpoint(error) => error.code(),
94            Self::SessionAlreadyActive => {
95                "a3s.code.agent_protocol.checkpoint_session_already_active"
96            }
97        }
98    }
99}
100
101struct HarnessSessionEntry {
102    host: Arc<AgentProtocolHost>,
103    _workspace: HarnessSessionWorkspace,
104}
105
106enum HarnessSessionWorkspace {
107    Shared(PathBuf),
108    Isolated {
109        source: PathBuf,
110        path: PathBuf,
111        _temporary_root: tempfile::TempDir,
112    },
113}
114
115impl HarnessSessionWorkspace {
116    async fn prepare(source: PathBuf) -> Result<Self, AgentProtocolHarnessError> {
117        tokio::task::spawn_blocking(move || {
118            if !crate::git::is_git_repo(&source) {
119                return Ok(Self::Shared(source));
120            }
121            let temporary_root = tempfile::Builder::new()
122                .prefix("a3s-code-harness-session-")
123                .tempdir()
124                .map_err(|error| AgentProtocolHarnessError::Workspace(error.to_string()))?;
125            let path = temporary_root.path().join("workspace");
126            crate::git::create_isolated_worktree(&source, &path)
127                .map_err(|error| AgentProtocolHarnessError::Workspace(error.to_string()))?;
128            Ok(Self::Isolated {
129                source,
130                path,
131                _temporary_root: temporary_root,
132            })
133        })
134        .await
135        .map_err(|error| AgentProtocolHarnessError::Workspace(error.to_string()))?
136    }
137
138    fn path(&self) -> &Path {
139        match self {
140            Self::Shared(path) | Self::Isolated { path, .. } => path,
141        }
142    }
143}
144
145impl Drop for HarnessSessionWorkspace {
146    fn drop(&mut self) {
147        if let Self::Isolated { source, path, .. } = self {
148            if let Err(error) = crate::git::remove_isolated_worktree(source, path) {
149                tracing::warn!(%error, workspace = %path.display(), "could not remove Agent Harness session worktree");
150            }
151        }
152    }
153}
154
155/// Release-bound, multi-session kernel used by the sole native Harness.
156///
157/// Each entry is an [`AgentProtocolHost`] over one ordinary [`AgentSession`](crate::AgentSession).
158/// The map only retains those Code-owned sessions for conversation reuse; it
159/// never mirrors their runs or events. Miss admission is serialized so two
160/// concurrent commands cannot construct the same session twice, while work on
161/// already admitted sessions remains concurrent.
162pub struct AgentProtocolHarness {
163    manifest: Arc<AgentReleaseManifest>,
164    agent: Arc<Agent>,
165    workspace: String,
166    session_options: SessionOptions,
167    max_sessions: usize,
168    sessions: RwLock<HashMap<String, Arc<HarnessSessionEntry>>>,
169    admission: Mutex<()>,
170    closed: AtomicBool,
171}
172
173impl std::fmt::Debug for AgentProtocolHarness {
174    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175        formatter
176            .debug_struct("AgentProtocolHarness")
177            .field("agent_release_identity", &self.manifest.artifact().digest())
178            .field("manifest_identity", &self.manifest.identity())
179            .field("workspace", &self.workspace)
180            .field("max_sessions", &self.max_sessions)
181            .field("closed", &self.closed.load(Ordering::Acquire))
182            .finish_non_exhaustive()
183    }
184}
185
186impl AgentProtocolHarness {
187    /// Admit one release into the native Harness compatibility surface.
188    pub fn new(
189        manifest: AgentReleaseManifest,
190        agent: Arc<Agent>,
191        workspace: impl Into<String>,
192    ) -> Result<Self, AgentProtocolHarnessError> {
193        // Protocol identity is enforced by verify_compatibility against
194        // agent_harness_compatibility_v1() (same AGENT_PROTOCOL_V1 pin).
195        manifest.verify_compatibility(&agent_harness_compatibility_v1())?;
196        let workspace = workspace.into();
197        if workspace.trim().is_empty() {
198            return Err(AgentProtocolHarnessError::Workspace(
199                "workspace path must not be empty".into(),
200            ));
201        }
202        Ok(Self {
203            manifest: Arc::new(manifest),
204            agent,
205            workspace,
206            session_options: SessionOptions::new(),
207            max_sessions: AGENT_PROTOCOL_HARNESS_MAX_SESSIONS,
208            sessions: RwLock::new(HashMap::new()),
209            admission: Mutex::new(()),
210            closed: AtomicBool::new(false),
211        })
212    }
213
214    /// Apply common options to every Code session created by this Harness.
215    ///
216    /// A caller-provided session ID is ignored. The exact protocol identity is
217    /// authoritative, and auto-save is always enabled when a store is present.
218    pub fn with_session_options(mut self, options: SessionOptions) -> Self {
219        self.session_options = options;
220        self.session_options.session_id = None;
221        self.session_options.auto_save = true;
222        self
223    }
224
225    /// Override the finite retained-session limit.
226    pub fn with_max_sessions(
227        mut self,
228        max_sessions: usize,
229    ) -> Result<Self, AgentProtocolHarnessError> {
230        if max_sessions == 0 {
231            return Err(AgentProtocolHarnessError::SessionCapacity);
232        }
233        self.max_sessions = max_sessions;
234        Ok(self)
235    }
236
237    pub fn manifest(&self) -> &AgentReleaseManifest {
238        &self.manifest
239    }
240
241    pub fn agent_release_identity(&self) -> &str {
242        self.manifest.artifact().digest()
243    }
244
245    pub fn max_sessions(&self) -> usize {
246        self.max_sessions
247    }
248
249    pub fn is_closed(&self) -> bool {
250        self.closed.load(Ordering::Acquire)
251    }
252
253    pub async fn session_count(&self) -> usize {
254        self.sessions.read().await.len()
255    }
256
257    /// Route an exact command into its Code-owned conversation session.
258    pub async fn execute(
259        &self,
260        command: &AgentProtocolCommandV1,
261    ) -> Result<AgentProtocolCommandReceiptV1, AgentProtocolHarnessError> {
262        command.validate()?;
263        // A normal recovery must load an existing persisted Session. Creating
264        // a fresh Session before discovering that its checkpoint is missing
265        // would publish an unusable ghost conversation. Portable checkpoint
266        // recovery has its own exact admission API below and may construct an
267        // unpublished Session from the supplied checkpoint.
268        let create_if_missing = matches!(command, AgentProtocolCommandV1::Start { .. });
269        let host = self.host_for(command.identity(), create_if_missing).await?;
270        // Harness close drains an already admitted session. Callers see harness Closed.
271        match host.execute(command).await {
272            Ok(receipt) => Ok(receipt),
273            Err(error) => Err(map_host_execute_error(error, self.is_closed())),
274        }
275    }
276
277    /// Validate, restore, execute, and publish one portable checkpoint as one
278    /// Harness-visible admission.
279    ///
280    /// The complete descriptor is matched before payload decode. For a missing
281    /// Session, Code builds an unpublished Session directly from the semantic
282    /// snapshot and starts the target Run from the logical value decoded from
283    /// those same canonical bytes. Only successful admission enters the
284    /// Harness session map; no split SessionStore writes are required first.
285    /// External store revision fencing remains the embedding host's boundary.
286    pub async fn execute_checkpoint_recovery(
287        &self,
288        request: &AgentProtocolRunRecoverExactV1,
289        checkpoint: SessionCheckpointExportV1,
290    ) -> Result<AgentProtocolCommandReceiptV1, AgentProtocolCheckpointRecoveryError> {
291        self.execute_checkpoint_recovery_inner(request, checkpoint, None)
292            .await
293    }
294
295    /// Restore a portable checkpoint whose source Run used a non-empty scoped
296    /// capability generation.
297    ///
298    /// The batch is consumed only for a missing Session and must reconstruct
299    /// the checkpoint's exact historical generation. Existing Sessions are
300    /// never rolled backward by this API.
301    pub async fn execute_checkpoint_recovery_with_capability_batch(
302        &self,
303        request: &AgentProtocolRunRecoverExactV1,
304        checkpoint: SessionCheckpointExportV1,
305        capability_batch: crate::capability::SessionCapabilityBatch,
306    ) -> Result<AgentProtocolCommandReceiptV1, AgentProtocolCheckpointRecoveryError> {
307        self.execute_checkpoint_recovery_inner(request, checkpoint, Some(capability_batch))
308            .await
309    }
310
311    async fn execute_checkpoint_recovery_inner(
312        &self,
313        request: &AgentProtocolRunRecoverExactV1,
314        checkpoint: SessionCheckpointExportV1,
315        mut capability_batch: Option<crate::capability::SessionCapabilityBatch>,
316    ) -> Result<AgentProtocolCommandReceiptV1, AgentProtocolCheckpointRecoveryError> {
317        request
318            .validate()
319            .map_err(AgentProtocolHarnessError::from)?;
320        if request.identity.agent_release_identity != self.manifest.artifact().digest() {
321            return Err(
322                AgentProtocolHarnessError::from(AgentProtocolHostError::ReleaseMismatch).into(),
323            );
324        }
325        if request.checkpoint != *checkpoint.descriptor() {
326            return Err(SessionCheckpointError::ContentDrift(
327                "recovery request descriptor does not match the supplied portable checkpoint"
328                    .into(),
329            )
330            .into());
331        }
332        let payload = checkpoint.into_open()?;
333        let (mut snapshot, logical_resume) = payload.into_exact_recovery_parts()?;
334
335        let _admission = self.admission.lock().await;
336        if self.is_closed() {
337            return Err(AgentProtocolHarnessError::Closed.into());
338        }
339        if let Some(host) = self
340            .sessions
341            .read()
342            .await
343            .get(&request.identity.session_id)
344            .map(|entry| Arc::clone(&entry.host))
345        {
346            if capability_batch.is_some() {
347                return Err(AgentProtocolCheckpointRecoveryError::SessionAlreadyActive);
348            }
349            if host
350                .session()
351                .run_snapshot(&request.identity.run_id)
352                .await
353                .is_none()
354            {
355                return Err(AgentProtocolCheckpointRecoveryError::SessionAlreadyActive);
356            }
357            return host
358                .execute_exact_recovery_from_checkpoint(request, logical_resume)
359                .await
360                .map_err(Into::into);
361        }
362        if self.sessions.read().await.len() >= self.max_sessions {
363            return Err(AgentProtocolHarnessError::SessionCapacity.into());
364        }
365
366        let options = self
367            .session_options
368            .clone()
369            .with_session_id(&request.identity.session_id)
370            .with_auto_save(true);
371        if let Some(persisted) = self
372            .agent
373            .load_protocol_session_snapshot_async(&request.identity.session_id, &options)
374            .await
375            .map_err(AgentProtocolHarnessError::from)?
376        {
377            let target_already_persisted = persisted
378                .run_records
379                .iter()
380                .any(|record| record.snapshot.id == request.identity.run_id);
381            if target_already_persisted {
382                snapshot = persisted;
383            } else {
384                request.checkpoint.snapshot.validate_for(&persisted)?;
385            }
386        }
387
388        let workspace = HarnessSessionWorkspace::prepare(PathBuf::from(&self.workspace)).await?;
389        let session = self
390            .agent
391            .restore_protocol_checkpoint_session_async(
392                snapshot,
393                workspace.path().to_string_lossy().into_owned(),
394                options,
395            )
396            .await
397            .map_err(AgentProtocolHarnessError::from)?;
398        match (&logical_resume.capability_binding, capability_batch.take()) {
399            (Some(expected), batch) => {
400                let binding_result =
401                    if TEST_FORCE_INVALID_RECOVERY_BINDING.swap(false, Ordering::SeqCst) {
402                        Err(crate::capability::RunCapabilityBindingError::InvalidField {
403                            field: "schema",
404                            message: "forced invalid recovery binding".into(),
405                        })
406                    } else {
407                        session.ensure_recovery_capability_binding(expected)
408                    };
409                match binding_result {
410                    Ok(()) if batch.is_none() => {}
411                    Ok(()) => {
412                        session.close().await;
413                        return Err(SessionCheckpointError::InvalidPayload(
414                        "a recovery capability batch was supplied even though the restored Session already matches the checkpoint"
415                            .into(),
416                    )
417                    .into());
418                    }
419                    Err(crate::capability::RunCapabilityBindingError::ContentDrift { .. }) => {
420                        let Some(batch) = batch else {
421                            session.close().await;
422                            return Err(SessionCheckpointError::ContentDrift(
423                            "the portable checkpoint requires a scoped capability generation that was not reconstructed by the host"
424                                .into(),
425                        )
426                        .into());
427                        };
428                        if let Err(error) = session
429                            .bootstrap_recovery_capability_batch(
430                                expected,
431                                batch,
432                                tokio_util::sync::CancellationToken::new(),
433                            )
434                            .await
435                        {
436                            session.close().await;
437                            return Err(AgentProtocolHarnessError::Code(error.into()).into());
438                        }
439                    }
440                    Err(_error) => {
441                        session.close().await;
442                        return Err(SessionCheckpointError::InvalidPayload(
443                            "the portable checkpoint capability binding is invalid".into(),
444                        )
445                        .into());
446                    }
447                }
448            }
449            (None, Some(_)) => {
450                session.close().await;
451                return Err(SessionCheckpointError::InvalidPayload(
452                    "a recovery capability batch cannot accompany a legacy unbound checkpoint"
453                        .into(),
454                )
455                .into());
456            }
457            (None, None) => {}
458        }
459        let session = Arc::new(session);
460        // Protocol identity was pinned at Harness construction; from_manifest
461        // only re-checks that invariant for the host constructor.
462        let host = Arc::new(AgentProtocolHost::from_verified_manifest(
463            &self.manifest,
464            Arc::clone(&session),
465        ));
466        let receipt = match host
467            .execute_exact_recovery_from_checkpoint(request, logical_resume)
468            .await
469        {
470            Ok(receipt) => receipt,
471            Err(error) => {
472                host.session().close().await;
473                return Err(error.into());
474            }
475        };
476        if self.is_closed() {
477            host.session().close().await;
478            return Err(AgentProtocolHarnessError::Closed.into());
479        }
480        self.sessions.write().await.insert(
481            request.identity.session_id.clone(),
482            Arc::new(HarnessSessionEntry {
483                host,
484                _workspace: workspace,
485            }),
486        );
487        Ok(receipt)
488    }
489
490    /// Route a bounded event query into the same authoritative Code session.
491    pub async fn event_page(
492        &self,
493        request: &AgentProtocolEventPageRequestV1,
494    ) -> Result<AgentProtocolEventPageV1, AgentProtocolHarnessError> {
495        request.validate()?;
496        let host = self.host_for(&request.identity, false).await?;
497        host.event_page_for(request).await.map_err(Into::into)
498    }
499
500    /// Route an immutable change-set query into the same authoritative run.
501    pub async fn change_set(
502        &self,
503        request: &AgentProtocolChangeSetRequestV1,
504    ) -> Result<AgentProtocolChangeSetV1, AgentProtocolHarnessError> {
505        request.validate()?;
506        let host = self.host_for(&request.identity, false).await?;
507        host.change_set_for(request).await.map_err(Into::into)
508    }
509
510    /// Stop admission and close every Code-owned session and Agent resource.
511    pub async fn close(&self) {
512        if self.closed.swap(true, Ordering::AcqRel) {
513            return;
514        }
515        let _admission = self.admission.lock().await;
516        self.agent.close().await;
517        self.sessions.write().await.clear();
518    }
519
520    async fn host_for(
521        &self,
522        identity: &AgentProtocolRunIdentityV1,
523        create_if_missing: bool,
524    ) -> Result<Arc<AgentProtocolHost>, AgentProtocolHarnessError> {
525        identity.validate()?;
526        if identity.agent_release_identity != self.manifest.artifact().digest() {
527            return Err(AgentProtocolHostError::ReleaseMismatch.into());
528        }
529        if self.is_closed() {
530            return Err(AgentProtocolHarnessError::Closed);
531        }
532        if let Some(host) = self
533            .sessions
534            .read()
535            .await
536            .get(&identity.session_id)
537            .map(|entry| Arc::clone(&entry.host))
538        {
539            return Ok(host);
540        }
541
542        let _admission = self.admission.lock().await;
543        if self.is_closed() {
544            return Err(AgentProtocolHarnessError::Closed);
545        }
546        if let Some(host) = self
547            .sessions
548            .read()
549            .await
550            .get(&identity.session_id)
551            .map(|entry| Arc::clone(&entry.host))
552        {
553            return Ok(host);
554        }
555        if self.sessions.read().await.len() >= self.max_sessions {
556            return Err(AgentProtocolHarnessError::SessionCapacity);
557        }
558
559        let workspace = HarnessSessionWorkspace::prepare(PathBuf::from(&self.workspace)).await?;
560        let options = self
561            .session_options
562            .clone()
563            .with_session_id(&identity.session_id)
564            .with_auto_save(true);
565        let session = self
566            .agent
567            .open_protocol_session_async(
568                workspace.path().to_string_lossy().into_owned(),
569                options,
570                create_if_missing,
571            )
572            .await?
573            .ok_or(AgentProtocolHarnessError::SessionNotFound)?;
574        if self.is_closed() {
575            session.close().await;
576            return Err(AgentProtocolHarnessError::Closed);
577        }
578        let host = Arc::new(AgentProtocolHost::from_verified_manifest(
579            &self.manifest,
580            Arc::new(session),
581        ));
582        self.sessions.write().await.insert(
583            identity.session_id.clone(),
584            Arc::new(HarnessSessionEntry {
585                host: Arc::clone(&host),
586                _workspace: workspace,
587            }),
588        );
589        Ok(host)
590    }
591}
592
593fn map_host_execute_error(
594    error: AgentProtocolHostError,
595    harness_closed: bool,
596) -> AgentProtocolHarnessError {
597    match error {
598        AgentProtocolHostError::Code(CodeError::SessionClosed { .. }) if harness_closed => {
599            AgentProtocolHarnessError::Closed
600        }
601        other => other.into(),
602    }
603}
604
605#[cfg(test)]
606mod tests {
607    use super::*;
608    use crate::error::CodeError;
609    use crate::release::AgentReleaseError;
610    use crate::session_checkpoint::SessionCheckpointError;
611
612    #[test]
613    fn harness_error_codes_are_stable() {
614        let errors = [
615            AgentProtocolHarnessError::Protocol(AgentProtocolError::Encoding),
616            AgentProtocolHarnessError::Release(AgentReleaseError::UnsupportedContract),
617            AgentProtocolHarnessError::Host(AgentProtocolHostError::RunNotFound),
618            AgentProtocolHarnessError::Code(CodeError::TaskSchedulerClosed),
619            AgentProtocolHarnessError::SessionNotFound,
620            AgentProtocolHarnessError::SessionCapacity,
621            AgentProtocolHarnessError::Closed,
622            AgentProtocolHarnessError::Workspace("x".into()),
623        ];
624        assert_eq!(errors[0].code(), AgentProtocolError::Encoding.code());
625        assert_eq!(
626            errors[1].code(),
627            AgentReleaseError::UnsupportedContract.code()
628        );
629        assert_eq!(errors[2].code(), AgentProtocolHostError::RunNotFound.code());
630        assert_eq!(errors[3].code(), CodeError::TaskSchedulerClosed.code());
631        assert_eq!(
632            errors[4].code(),
633            "a3s.code.agent_protocol.session_not_found"
634        );
635        assert_eq!(errors[5].code(), "a3s.code.agent_protocol.session_capacity");
636        assert_eq!(errors[6].code(), "a3s.code.agent_protocol.harness_closed");
637        assert_eq!(
638            errors[7].code(),
639            "a3s.code.agent_protocol.workspace_isolation"
640        );
641        for error in &errors {
642            assert!(!error.to_string().is_empty());
643            assert!(!format!("{error:?}").is_empty());
644        }
645    }
646
647    #[test]
648    fn session_closed_during_harness_drain_maps_to_closed() {
649        let closed = map_host_execute_error(
650            AgentProtocolHostError::Code(CodeError::SessionClosed {
651                session_id: "s".into(),
652            }),
653            true,
654        );
655        assert!(matches!(closed, AgentProtocolHarnessError::Closed));
656
657        let open = map_host_execute_error(
658            AgentProtocolHostError::Code(CodeError::SessionClosed {
659                session_id: "s".into(),
660            }),
661            false,
662        );
663        assert!(matches!(
664            open,
665            AgentProtocolHarnessError::Host(AgentProtocolHostError::Code(
666                CodeError::SessionClosed { .. }
667            ))
668        ));
669
670        let other = map_host_execute_error(AgentProtocolHostError::RunNotFound, true);
671        assert!(matches!(
672            other,
673            AgentProtocolHarnessError::Host(AgentProtocolHostError::RunNotFound)
674        ));
675    }
676
677    #[test]
678    fn checkpoint_recovery_error_codes_are_stable() {
679        let errors = [
680            AgentProtocolCheckpointRecoveryError::Harness(AgentProtocolHarnessError::Closed),
681            AgentProtocolCheckpointRecoveryError::Exact(AgentProtocolExactRecoveryError::Host(
682                AgentProtocolHostError::RunNotFound,
683            )),
684            AgentProtocolCheckpointRecoveryError::Checkpoint(
685                SessionCheckpointError::InvalidPayload("x".into()),
686            ),
687            AgentProtocolCheckpointRecoveryError::SessionAlreadyActive,
688        ];
689        assert_eq!(errors[0].code(), AgentProtocolHarnessError::Closed.code());
690        assert_eq!(errors[1].code(), AgentProtocolHostError::RunNotFound.code());
691        assert_eq!(
692            errors[2].code(),
693            SessionCheckpointError::InvalidPayload("x".into()).code()
694        );
695        assert_eq!(
696            errors[3].code(),
697            "a3s.code.agent_protocol.checkpoint_session_already_active"
698        );
699        for error in &errors {
700            assert!(!error.to_string().is_empty());
701            assert!(!format!("{error:?}").is_empty());
702        }
703    }
704}