use crate::agent_api::{AgentRunSpawn, AgentSession};
use crate::agent_protocol::{
validate_lower_sha256, AgentProtocolCommandReceiptV1, AgentProtocolCommandV1,
AgentProtocolError, AgentProtocolEventPageRequestV1, AgentProtocolEventPageV1,
AgentProtocolRunIdentityV1, AGENT_PROTOCOL_MAX_EVENTS_PER_PAGE,
};
use crate::error::CodeError;
use crate::release::{AgentReleaseManifest, AGENT_PROTOCOL_V1};
use crate::run::RunSnapshot;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AgentProtocolHostError {
#[error(transparent)]
Protocol(#[from] AgentProtocolError),
#[error("A3S Code Agent command targets another release")]
ReleaseMismatch,
#[error("A3S Code Agent release declares another protocol")]
ReleaseProtocolMismatch,
#[error("A3S Code Agent command targets another session")]
SessionMismatch,
#[error("A3S Code Agent run was not found")]
RunNotFound,
#[error("A3S Code Agent run is not active; recover it from a durable checkpoint")]
RunUnavailable,
#[error("A3S Code Agent sequence cannot be represented on this host")]
SequenceOverflow,
#[error(transparent)]
Code(#[from] CodeError),
}
impl AgentProtocolHostError {
pub const fn code(&self) -> &'static str {
match self {
Self::Protocol(error) => error.code(),
Self::ReleaseMismatch => "a3s.code.agent_protocol.release_mismatch",
Self::ReleaseProtocolMismatch => "a3s.code.agent_protocol.release_protocol_mismatch",
Self::SessionMismatch => "a3s.code.agent_protocol.session_mismatch",
Self::RunNotFound => "a3s.code.agent_protocol.run_not_found",
Self::RunUnavailable => "a3s.code.agent_protocol.run_unavailable",
Self::SequenceOverflow => "a3s.code.agent_protocol.sequence_overflow",
Self::Code(error) => error.code(),
}
}
}
#[derive(Clone)]
pub struct AgentProtocolHost {
agent_release_identity: String,
session: Arc<AgentSession>,
}
impl AgentProtocolHost {
pub fn new(
agent_release_identity: impl Into<String>,
session: Arc<AgentSession>,
) -> Result<Self, AgentProtocolHostError> {
let agent_release_identity = agent_release_identity.into();
validate_lower_sha256("agent_release_identity", &agent_release_identity)?;
Ok(Self {
agent_release_identity,
session,
})
}
pub fn from_manifest(
manifest: &AgentReleaseManifest,
session: Arc<AgentSession>,
) -> Result<Self, AgentProtocolHostError> {
if manifest.protocol() != AGENT_PROTOCOL_V1 {
return Err(AgentProtocolHostError::ReleaseProtocolMismatch);
}
Ok(Self {
agent_release_identity: manifest.artifact().digest().to_string(),
session,
})
}
pub fn agent_release_identity(&self) -> &str {
&self.agent_release_identity
}
pub fn session(&self) -> &Arc<AgentSession> {
&self.session
}
pub async fn execute(
&self,
command: &AgentProtocolCommandV1,
) -> Result<AgentProtocolCommandReceiptV1, AgentProtocolHostError> {
command.validate()?;
self.validate_identity(command.identity())?;
let replayed = match command {
AgentProtocolCommandV1::Start { request } => {
let spawned = self
.session
.spawn_run_with_id(&request.identity.run_id, &request.prompt)
.await?;
detach(spawned)
}
AgentProtocolCommandV1::Recover { request } => {
let spawned = self
.session
.spawn_recovery_with_run_id(
&request.checkpoint_run_id,
&request.identity.run_id,
)
.await?;
detach(spawned)
}
AgentProtocolCommandV1::Cancel { request } => {
let snapshot = self.snapshot(&request.identity).await?;
if snapshot.status.is_terminal() {
true
} else if self.session.cancel_run(&request.identity.run_id).await {
false
} else if self.snapshot(&request.identity).await?.status.is_terminal() {
true
} else {
return Err(AgentProtocolHostError::RunUnavailable);
}
}
};
let snapshot = self.snapshot(command.identity()).await?;
let receipt = AgentProtocolCommandReceiptV1 {
schema: AgentProtocolCommandReceiptV1::SCHEMA.into(),
action: command.action(),
request_id: command.request_id().into(),
identity: command.identity().clone(),
command_digest: command.digest()?,
state: snapshot.status.into(),
latest_event_sequence_exclusive: u64::try_from(snapshot.event_count)
.map_err(|_| AgentProtocolHostError::SequenceOverflow)?,
observed_at_ms: now_ms().max(snapshot.updated_at_ms),
replayed,
};
receipt.validate_for(command)?;
Ok(receipt)
}
pub async fn event_page(
&self,
identity: &AgentProtocolRunIdentityV1,
after_event_sequence: Option<u64>,
limit: usize,
) -> Result<AgentProtocolEventPageV1, AgentProtocolHostError> {
identity.validate()?;
self.validate_identity(identity)?;
if limit == 0 || limit > AGENT_PROTOCOL_MAX_EVENTS_PER_PAGE {
return Err(AgentProtocolError::InvalidField("limit").into());
}
let after_sequence = after_event_sequence
.map(|sequence| {
usize::try_from(sequence).map_err(|_| AgentProtocolHostError::SequenceOverflow)
})
.transpose()?;
let snapshot = self.snapshot(identity).await?;
let page = self
.session
.run_event_page(&identity.run_id, after_sequence, limit)
.await
.ok_or(AgentProtocolHostError::RunNotFound)?;
AgentProtocolEventPageV1::from_run_page(
identity.clone(),
snapshot.status,
now_ms().max(snapshot.updated_at_ms),
after_sequence,
&page,
)
.map_err(Into::into)
}
pub async fn event_page_for(
&self,
request: &AgentProtocolEventPageRequestV1,
) -> Result<AgentProtocolEventPageV1, AgentProtocolHostError> {
request.validate()?;
self.event_page(
&request.identity,
request.after_event_sequence,
usize::from(request.limit),
)
.await
}
fn validate_identity(
&self,
identity: &AgentProtocolRunIdentityV1,
) -> Result<(), AgentProtocolHostError> {
if identity.agent_release_identity != self.agent_release_identity {
return Err(AgentProtocolHostError::ReleaseMismatch);
}
if identity.session_id != self.session.session_id() {
return Err(AgentProtocolHostError::SessionMismatch);
}
Ok(())
}
async fn snapshot(
&self,
identity: &AgentProtocolRunIdentityV1,
) -> Result<RunSnapshot, AgentProtocolHostError> {
let snapshot = self
.session
.run_snapshot(&identity.run_id)
.await
.ok_or(AgentProtocolHostError::RunNotFound)?;
if snapshot.session_id != identity.session_id {
return Err(AgentProtocolHostError::SessionMismatch);
}
Ok(snapshot)
}
}
fn detach(spawned: AgentRunSpawn) -> bool {
match spawned {
AgentRunSpawn::Started { worker, .. } => {
drop(worker);
false
}
AgentRunSpawn::Replayed { .. } => true,
}
}
fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_millis() as u64)
.unwrap_or_default()
}