agent-runtime-execution-api 0.1.1

Internal execution lifecycle contract for Agent Runtime
Documentation
//! Stable Outer Shell to Link contract and execution journal port.

use async_trait::async_trait;
use runtime_api_contract::{
    CreateExecutionRequest, CreateExecutionResponse, EventPage, EventPayload, ExecutionEvent,
    ExecutionFailure, ExecutionOutcome, ExecutionState, ExecutionView, SubmitInputRequest,
};
use runtime_types::{CallerScope, DelegationLeaseRef, ExecutionId, OperationId, RequestAuthority};
use thiserror::Error;

#[async_trait]
pub trait RuntimeLink: Send + Sync + 'static {
    async fn create_execution(
        &self,
        authority: &RequestAuthority,
        idempotency_key: &str,
        request: CreateExecutionRequest,
    ) -> Result<CreateExecutionResponse, LinkError>;

    async fn execution(
        &self,
        authority: &RequestAuthority,
        id: &ExecutionId,
    ) -> Result<ExecutionView, LinkError>;

    async fn events(
        &self,
        authority: &RequestAuthority,
        id: &ExecutionId,
        after: Option<u64>,
        limit: usize,
    ) -> Result<EventPage, LinkError>;

    async fn submit_input(
        &self,
        authority: &RequestAuthority,
        id: &ExecutionId,
        request: SubmitInputRequest,
    ) -> Result<ExecutionView, LinkError>;

    async fn cancel(
        &self,
        authority: &RequestAuthority,
        id: &ExecutionId,
    ) -> Result<ExecutionView, LinkError>;
}

#[derive(Debug, Error)]
pub enum LinkError {
    #[error("invalid request: {0}")]
    Invalid(String),
    #[error("execution not found")]
    NotFound,
    #[error("caller is not allowed to access this execution")]
    Forbidden,
    #[error("execution state conflict: {0}")]
    Conflict(String),
    #[error("runtime is overloaded")]
    Overloaded,
    #[error("runtime dependency unavailable: {0}")]
    Unavailable(String),
    #[error("runtime internal failure: {0}")]
    Internal(String),
}

#[derive(Debug, Clone)]
pub struct NewJournalExecution {
    pub idempotency_key: String,
    pub caller: CallerScope,
    pub request: CreateExecutionRequest,
    pub view: ExecutionView,
}

#[derive(Debug, Clone)]
pub struct JournalExecution {
    pub idempotency_key: String,
    pub caller: CallerScope,
    pub request: CreateExecutionRequest,
    pub view: ExecutionView,
    pub version: u64,
    pub pending_interaction: Option<JournalInteraction>,
    pub input_receipts: std::collections::BTreeMap<OperationId, JournalInputReceipt>,
    pub delegation: Option<JournalDelegation>,
    pub claim: Option<JournalClaim>,
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct JournalClaim {
    pub worker_id: String,
    pub expires_at_ms: i64,
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct JournalDelegation {
    pub lease_ref: DelegationLeaseRef,
    pub expires_at_seconds: u64,
    pub revision: u64,
    pub cleanup_complete: bool,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct JournalInteraction {
    pub request_id: String,
    pub prompt: String,
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct JournalInputReceipt {
    pub request_id: String,
    pub response_text: String,
    pub event_sequence: u64,
}

#[derive(Debug, Clone)]
pub struct JournalMutation {
    pub execution: JournalExecution,
    pub event: ExecutionEvent,
    pub replayed: bool,
}

#[derive(Debug, Clone)]
pub enum JournalReservation {
    Created(JournalExecution),
    Existing(JournalExecution),
}

#[derive(Debug, Error)]
pub enum JournalError {
    #[error("execution not found")]
    NotFound,
    #[error("journal state conflict")]
    Conflict,
    #[error("journal unavailable: {0}")]
    Unavailable(String),
}

#[async_trait]
pub trait ExecutionJournal: Send + Sync + 'static {
    async fn reserve(
        &self,
        execution: NewJournalExecution,
    ) -> Result<JournalReservation, JournalError>;

    async fn get(&self, id: &ExecutionId) -> Result<JournalExecution, JournalError>;

    async fn claim(
        &self,
        id: &ExecutionId,
        worker_id: &str,
        now_ms: i64,
        expires_at_ms: i64,
    ) -> Result<JournalExecution, JournalError>;

    async fn recoverable(
        &self,
        now_ms: i64,
        limit: usize,
    ) -> Result<Vec<JournalExecution>, JournalError>;

    async fn transition(
        &self,
        id: &ExecutionId,
        expected: &[ExecutionState],
        next: ExecutionState,
    ) -> Result<JournalExecution, JournalError>;

    /// Persist the opaque delegation reference before any Infra-backed Kernel
    /// operation. Replays with the same lease are idempotent.
    async fn attach_delegation(
        &self,
        id: &ExecutionId,
        delegation: JournalDelegation,
    ) -> Result<JournalExecution, JournalError>;

    /// Mark terminal delegation cleanup after broker revoke succeeds.
    async fn complete_delegation_cleanup(
        &self,
        id: &ExecutionId,
        lease_ref: &DelegationLeaseRef,
    ) -> Result<JournalExecution, JournalError>;

    async fn finish(
        &self,
        id: &ExecutionId,
        expected: &[ExecutionState],
        next: ExecutionState,
        outcome: Option<ExecutionOutcome>,
        failure: Option<ExecutionFailure>,
    ) -> Result<JournalExecution, JournalError>;

    /// Atomically commit a state transition and its corresponding event so
    /// replay and SSE cannot observe one without the other. Terminal outcome
    /// and failure payloads are supplied only for terminal transitions.
    async fn transition_with_event(
        &self,
        id: &ExecutionId,
        expected: &[ExecutionState],
        next: ExecutionState,
        outcome: Option<ExecutionOutcome>,
        failure: Option<ExecutionFailure>,
        payload: EventPayload,
    ) -> Result<JournalMutation, JournalError>;

    async fn append_event(
        &self,
        id: &ExecutionId,
        payload: EventPayload,
    ) -> Result<ExecutionEvent, JournalError>;

    /// Atomically publish a pending interaction and its committed event.
    async fn begin_interaction(
        &self,
        id: &ExecutionId,
        request_id: &str,
        prompt: &str,
    ) -> Result<JournalMutation, JournalError>;

    /// Atomically deduplicate input, transition back to running, and commit
    /// the interaction-received event before any in-process waiter is woken.
    async fn commit_interaction_input(
        &self,
        id: &ExecutionId,
        operation_id: &OperationId,
        request_id: &str,
        response_text: &str,
    ) -> Result<JournalMutation, JournalError>;

    /// Mark a journal-committed response as consumed by the running tool.
    async fn complete_interaction(
        &self,
        id: &ExecutionId,
        request_id: &str,
    ) -> Result<JournalExecution, JournalError>;

    async fn events(
        &self,
        id: &ExecutionId,
        after: Option<u64>,
        limit: usize,
    ) -> Result<Vec<ExecutionEvent>, JournalError>;
}