1use runtime_types::{ConversationId, ExecutionId, OperationId, RuntimeInstanceId};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10pub fn openapi_document() -> Value {
13 serde_json::from_str(include_str!("../openapi/runtime-v3.json"))
14 .expect("embedded Runtime OpenAPI must be valid JSON")
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18#[serde(rename_all = "camelCase", deny_unknown_fields)]
19pub struct CreateExecutionRequest {
20 pub runtime_instance_id: RuntimeInstanceId,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub conversation_id: Option<ConversationId>,
23 pub input: RuntimeInput,
24 #[serde(default)]
25 pub options: ExecutionOptions,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29#[serde(
30 tag = "type",
31 rename_all = "snake_case",
32 rename_all_fields = "camelCase",
33 deny_unknown_fields
34)]
35pub enum RuntimeInput {
36 UserMessage { text: String },
37 ToolApproval { request_id: String, approved: bool },
38 ElicitationResponse { request_id: String, text: String },
39}
40
41impl RuntimeInput {
42 pub fn user_text(&self) -> Option<&str> {
43 match self {
44 Self::UserMessage { text } => Some(text),
45 _ => None,
46 }
47 }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51#[serde(rename_all = "camelCase", deny_unknown_fields)]
52pub struct ExecutionOptions {
53 #[serde(default = "default_deadline_seconds")]
54 pub deadline_seconds: u64,
55 #[serde(default = "default_model_turns")]
56 pub max_model_turns: usize,
57 #[serde(default = "default_tool_calls")]
58 pub max_tool_calls: usize,
59}
60
61impl Default for ExecutionOptions {
62 fn default() -> Self {
63 Self {
64 deadline_seconds: default_deadline_seconds(),
65 max_model_turns: default_model_turns(),
66 max_tool_calls: default_tool_calls(),
67 }
68 }
69}
70
71impl ExecutionOptions {
72 pub fn validate(&self) -> Result<(), &'static str> {
73 if !(1..=3600).contains(&self.deadline_seconds) {
74 return Err("deadlineSeconds must be within 1..=3600");
75 }
76 if !(1..=256).contains(&self.max_model_turns) {
77 return Err("maxModelTurns must be within 1..=256");
78 }
79 if !(1..=2048).contains(&self.max_tool_calls) {
80 return Err("maxToolCalls must be within 1..=2048");
81 }
82 Ok(())
83 }
84}
85
86fn default_deadline_seconds() -> u64 {
87 900
88}
89fn default_model_turns() -> usize {
90 64
91}
92fn default_tool_calls() -> usize {
93 256
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97#[serde(rename_all = "camelCase", deny_unknown_fields)]
98pub struct CreateExecutionResponse {
99 pub execution: ExecutionView,
100 pub replayed: bool,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
104#[serde(rename_all = "camelCase", deny_unknown_fields)]
105pub struct ExecutionView {
106 pub id: ExecutionId,
107 pub runtime_instance_id: RuntimeInstanceId,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub conversation_id: Option<ConversationId>,
110 pub state: ExecutionState,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub outcome: Option<ExecutionOutcome>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub failure: Option<ExecutionFailure>,
115 pub created_at_ms: i64,
116 pub updated_at_ms: i64,
117}
118
119#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
120#[serde(rename_all = "snake_case")]
121pub enum ExecutionState {
122 Queued,
123 Running,
124 WaitingForInput,
125 Finalizing,
126 Completed,
127 Failed,
128 Canceled,
129}
130
131impl ExecutionState {
132 pub fn is_terminal(self) -> bool {
133 matches!(self, Self::Completed | Self::Failed | Self::Canceled)
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
138#[serde(rename_all = "camelCase", deny_unknown_fields)]
139pub struct ExecutionOutcome {
140 pub answer: String,
141 pub model_turns: usize,
142 pub tool_calls: usize,
143 pub input_tokens: Option<u64>,
145 pub output_tokens: Option<u64>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
149#[serde(rename_all = "camelCase", deny_unknown_fields)]
150pub struct ExecutionFailure {
151 pub code: String,
152 pub message: String,
153 pub retryable: bool,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
157#[serde(rename_all = "camelCase", deny_unknown_fields)]
158pub struct SubmitInputRequest {
159 pub operation_id: OperationId,
160 pub input: RuntimeInput,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
164#[serde(rename_all = "camelCase", deny_unknown_fields)]
165pub struct ExecutionEvent {
166 pub execution_id: ExecutionId,
167 pub sequence: u64,
168 pub created_at_ms: i64,
169 pub payload: EventPayload,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
173#[serde(tag = "type", content = "data", rename_all = "snake_case")]
174pub enum EventPayload {
175 ExecutionQueued,
176 ExecutionStarted,
177 ModelStarted {
178 turn: usize,
179 },
180 ModelCompleted {
181 turn: usize,
182 },
183 ToolStarted {
184 call_id: String,
185 name: String,
186 },
187 ToolCompleted {
188 call_id: String,
189 name: String,
190 failed: bool,
191 },
192 InteractionRequired {
193 request_id: String,
194 prompt: String,
195 },
196 InteractionReceived {
197 request_id: String,
198 },
199 Warning {
200 code: String,
201 message: String,
202 },
203 ExecutionCompleted {
204 answer: String,
205 },
206 ExecutionFailed {
207 code: String,
208 message: String,
209 },
210 ExecutionCanceled,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
214#[serde(rename_all = "camelCase", deny_unknown_fields)]
215pub struct EventPage {
216 pub items: Vec<ExecutionEvent>,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub next_after: Option<u64>,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
222#[serde(rename_all = "camelCase", deny_unknown_fields)]
223pub struct ApiErrorBody {
224 pub code: String,
225 pub message: String,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub request_id: Option<String>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub details: Option<Value>,
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn input_is_strict_and_tagged() {
238 let input: RuntimeInput = serde_json::from_value(serde_json::json!({
239 "type": "user_message", "text": "hello"
240 }))
241 .unwrap();
242 assert_eq!(input.user_text(), Some("hello"));
243 assert!(
244 serde_json::from_value::<RuntimeInput>(serde_json::json!({
245 "type":"user_message", "text":"hello", "secret":"no"
246 }))
247 .is_err()
248 );
249 let approval: RuntimeInput = serde_json::from_value(serde_json::json!({
250 "type":"tool_approval", "requestId":"call-1", "approved":true
251 }))
252 .unwrap();
253 assert!(matches!(approval, RuntimeInput::ToolApproval { .. }));
254 assert!(
255 serde_json::from_value::<RuntimeInput>(serde_json::json!({
256 "type":"tool_approval", "request_id":"call-1", "approved":true
257 }))
258 .is_err()
259 );
260 }
261
262 #[test]
263 fn limits_are_bounded() {
264 let mut options = ExecutionOptions::default();
265 assert!(options.validate().is_ok());
266 options.max_tool_calls = usize::MAX;
267 assert!(options.validate().is_err());
268 }
269
270 #[test]
271 fn openapi_snapshot_covers_the_public_execution_surface() {
272 let document = openapi_document();
273 assert_eq!(document["openapi"], "3.1.0");
274 for path in [
275 "/v1/executions",
276 "/v1/executions/{executionId}",
277 "/v1/executions/{executionId}/events",
278 "/v1/executions/{executionId}/stream",
279 "/v1/executions/{executionId}/inputs",
280 "/v1/executions/{executionId}/cancel",
281 ] {
282 assert!(document["paths"].get(path).is_some(), "missing {path}");
283 }
284 }
285}