Skip to main content

agent_runtime_execution_api/
lib.rs

1//! Stable Outer Shell to Link contract and execution journal port.
2
3use async_trait::async_trait;
4use runtime_api_contract::{
5    CreateExecutionRequest, CreateExecutionResponse, EventPage, EventPayload, ExecutionEvent,
6    ExecutionFailure, ExecutionOutcome, ExecutionState, ExecutionView, SubmitInputRequest,
7};
8use runtime_types::{CallerScope, DelegationLeaseRef, ExecutionId, OperationId, RequestAuthority};
9use thiserror::Error;
10
11#[async_trait]
12pub trait RuntimeLink: Send + Sync + 'static {
13    async fn create_execution(
14        &self,
15        authority: &RequestAuthority,
16        idempotency_key: &str,
17        request: CreateExecutionRequest,
18    ) -> Result<CreateExecutionResponse, LinkError>;
19
20    async fn execution(
21        &self,
22        authority: &RequestAuthority,
23        id: &ExecutionId,
24    ) -> Result<ExecutionView, LinkError>;
25
26    async fn events(
27        &self,
28        authority: &RequestAuthority,
29        id: &ExecutionId,
30        after: Option<u64>,
31        limit: usize,
32    ) -> Result<EventPage, LinkError>;
33
34    async fn submit_input(
35        &self,
36        authority: &RequestAuthority,
37        id: &ExecutionId,
38        request: SubmitInputRequest,
39    ) -> Result<ExecutionView, LinkError>;
40
41    async fn cancel(
42        &self,
43        authority: &RequestAuthority,
44        id: &ExecutionId,
45    ) -> Result<ExecutionView, LinkError>;
46}
47
48#[derive(Debug, Error)]
49pub enum LinkError {
50    #[error("invalid request: {0}")]
51    Invalid(String),
52    #[error("execution not found")]
53    NotFound,
54    #[error("caller is not allowed to access this execution")]
55    Forbidden,
56    #[error("execution state conflict: {0}")]
57    Conflict(String),
58    #[error("runtime is overloaded")]
59    Overloaded,
60    #[error("runtime dependency unavailable: {0}")]
61    Unavailable(String),
62    #[error("runtime internal failure: {0}")]
63    Internal(String),
64}
65
66#[derive(Debug, Clone)]
67pub struct NewJournalExecution {
68    pub idempotency_key: String,
69    pub caller: CallerScope,
70    pub request: CreateExecutionRequest,
71    pub view: ExecutionView,
72}
73
74#[derive(Debug, Clone)]
75pub struct JournalExecution {
76    pub idempotency_key: String,
77    pub caller: CallerScope,
78    pub request: CreateExecutionRequest,
79    pub view: ExecutionView,
80    pub version: u64,
81    pub pending_interaction: Option<JournalInteraction>,
82    pub input_receipts: std::collections::BTreeMap<OperationId, JournalInputReceipt>,
83    pub delegation: Option<JournalDelegation>,
84    pub claim: Option<JournalClaim>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
88#[serde(rename_all = "camelCase", deny_unknown_fields)]
89pub struct JournalClaim {
90    pub worker_id: String,
91    pub expires_at_ms: i64,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
95#[serde(rename_all = "camelCase", deny_unknown_fields)]
96pub struct JournalDelegation {
97    pub lease_ref: DelegationLeaseRef,
98    pub expires_at_seconds: u64,
99    pub revision: u64,
100    pub cleanup_complete: bool,
101}
102
103#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
104#[serde(rename_all = "camelCase", deny_unknown_fields)]
105pub struct JournalInteraction {
106    pub request_id: String,
107    pub prompt: String,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
111#[serde(rename_all = "camelCase", deny_unknown_fields)]
112pub struct JournalInputReceipt {
113    pub request_id: String,
114    pub response_text: String,
115    pub event_sequence: u64,
116}
117
118#[derive(Debug, Clone)]
119pub struct JournalMutation {
120    pub execution: JournalExecution,
121    pub event: ExecutionEvent,
122    pub replayed: bool,
123}
124
125#[derive(Debug, Clone)]
126pub enum JournalReservation {
127    Created(JournalExecution),
128    Existing(JournalExecution),
129}
130
131#[derive(Debug, Error)]
132pub enum JournalError {
133    #[error("execution not found")]
134    NotFound,
135    #[error("journal state conflict")]
136    Conflict,
137    #[error("journal unavailable: {0}")]
138    Unavailable(String),
139}
140
141#[async_trait]
142pub trait ExecutionJournal: Send + Sync + 'static {
143    async fn reserve(
144        &self,
145        execution: NewJournalExecution,
146    ) -> Result<JournalReservation, JournalError>;
147
148    async fn get(&self, id: &ExecutionId) -> Result<JournalExecution, JournalError>;
149
150    async fn claim(
151        &self,
152        id: &ExecutionId,
153        worker_id: &str,
154        now_ms: i64,
155        expires_at_ms: i64,
156    ) -> Result<JournalExecution, JournalError>;
157
158    async fn recoverable(
159        &self,
160        now_ms: i64,
161        limit: usize,
162    ) -> Result<Vec<JournalExecution>, JournalError>;
163
164    async fn transition(
165        &self,
166        id: &ExecutionId,
167        expected: &[ExecutionState],
168        next: ExecutionState,
169    ) -> Result<JournalExecution, JournalError>;
170
171    /// Persist the opaque delegation reference before any Infra-backed Kernel
172    /// operation. Replays with the same lease are idempotent.
173    async fn attach_delegation(
174        &self,
175        id: &ExecutionId,
176        delegation: JournalDelegation,
177    ) -> Result<JournalExecution, JournalError>;
178
179    /// Mark terminal delegation cleanup after broker revoke succeeds.
180    async fn complete_delegation_cleanup(
181        &self,
182        id: &ExecutionId,
183        lease_ref: &DelegationLeaseRef,
184    ) -> Result<JournalExecution, JournalError>;
185
186    async fn finish(
187        &self,
188        id: &ExecutionId,
189        expected: &[ExecutionState],
190        next: ExecutionState,
191        outcome: Option<ExecutionOutcome>,
192        failure: Option<ExecutionFailure>,
193    ) -> Result<JournalExecution, JournalError>;
194
195    /// Atomically commit a state transition and its corresponding event so
196    /// replay and SSE cannot observe one without the other. Terminal outcome
197    /// and failure payloads are supplied only for terminal transitions.
198    async fn transition_with_event(
199        &self,
200        id: &ExecutionId,
201        expected: &[ExecutionState],
202        next: ExecutionState,
203        outcome: Option<ExecutionOutcome>,
204        failure: Option<ExecutionFailure>,
205        payload: EventPayload,
206    ) -> Result<JournalMutation, JournalError>;
207
208    async fn append_event(
209        &self,
210        id: &ExecutionId,
211        payload: EventPayload,
212    ) -> Result<ExecutionEvent, JournalError>;
213
214    /// Atomically publish a pending interaction and its committed event.
215    async fn begin_interaction(
216        &self,
217        id: &ExecutionId,
218        request_id: &str,
219        prompt: &str,
220    ) -> Result<JournalMutation, JournalError>;
221
222    /// Atomically deduplicate input, transition back to running, and commit
223    /// the interaction-received event before any in-process waiter is woken.
224    async fn commit_interaction_input(
225        &self,
226        id: &ExecutionId,
227        operation_id: &OperationId,
228        request_id: &str,
229        response_text: &str,
230    ) -> Result<JournalMutation, JournalError>;
231
232    /// Mark a journal-committed response as consumed by the running tool.
233    async fn complete_interaction(
234        &self,
235        id: &ExecutionId,
236        request_id: &str,
237    ) -> Result<JournalExecution, JournalError>;
238
239    async fn events(
240        &self,
241        id: &ExecutionId,
242        after: Option<u64>,
243        limit: usize,
244    ) -> Result<Vec<ExecutionEvent>, JournalError>;
245}