use runtime_types::{ConversationId, ExecutionId, OperationId, RuntimeInstanceId};
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub fn openapi_document() -> Value {
serde_json::from_str(include_str!("../openapi/runtime-v3.json"))
.expect("embedded Runtime OpenAPI must be valid JSON")
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CreateExecutionRequest {
pub runtime_instance_id: RuntimeInstanceId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conversation_id: Option<ConversationId>,
pub input: RuntimeInput,
#[serde(default)]
pub options: ExecutionOptions,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(
tag = "type",
rename_all = "snake_case",
rename_all_fields = "camelCase",
deny_unknown_fields
)]
pub enum RuntimeInput {
UserMessage { text: String },
ToolApproval { request_id: String, approved: bool },
ElicitationResponse { request_id: String, text: String },
}
impl RuntimeInput {
pub fn user_text(&self) -> Option<&str> {
match self {
Self::UserMessage { text } => Some(text),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ExecutionOptions {
#[serde(default = "default_deadline_seconds")]
pub deadline_seconds: u64,
#[serde(default = "default_model_turns")]
pub max_model_turns: usize,
#[serde(default = "default_tool_calls")]
pub max_tool_calls: usize,
}
impl Default for ExecutionOptions {
fn default() -> Self {
Self {
deadline_seconds: default_deadline_seconds(),
max_model_turns: default_model_turns(),
max_tool_calls: default_tool_calls(),
}
}
}
impl ExecutionOptions {
pub fn validate(&self) -> Result<(), &'static str> {
if !(1..=3600).contains(&self.deadline_seconds) {
return Err("deadlineSeconds must be within 1..=3600");
}
if !(1..=256).contains(&self.max_model_turns) {
return Err("maxModelTurns must be within 1..=256");
}
if !(1..=2048).contains(&self.max_tool_calls) {
return Err("maxToolCalls must be within 1..=2048");
}
Ok(())
}
}
fn default_deadline_seconds() -> u64 {
900
}
fn default_model_turns() -> usize {
64
}
fn default_tool_calls() -> usize {
256
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CreateExecutionResponse {
pub execution: ExecutionView,
pub replayed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ExecutionView {
pub id: ExecutionId,
pub runtime_instance_id: RuntimeInstanceId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conversation_id: Option<ConversationId>,
pub state: ExecutionState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outcome: Option<ExecutionOutcome>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub failure: Option<ExecutionFailure>,
pub created_at_ms: i64,
pub updated_at_ms: i64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionState {
Queued,
Running,
WaitingForInput,
Finalizing,
Completed,
Failed,
Canceled,
}
impl ExecutionState {
pub fn is_terminal(self) -> bool {
matches!(self, Self::Completed | Self::Failed | Self::Canceled)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ExecutionOutcome {
pub answer: String,
pub model_turns: usize,
pub tool_calls: usize,
pub input_tokens: Option<u64>,
pub output_tokens: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ExecutionFailure {
pub code: String,
pub message: String,
pub retryable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SubmitInputRequest {
pub operation_id: OperationId,
pub input: RuntimeInput,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ExecutionEvent {
pub execution_id: ExecutionId,
pub sequence: u64,
pub created_at_ms: i64,
pub payload: EventPayload,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum EventPayload {
ExecutionQueued,
ExecutionStarted,
ModelStarted {
turn: usize,
},
ModelCompleted {
turn: usize,
},
ToolStarted {
call_id: String,
name: String,
},
ToolCompleted {
call_id: String,
name: String,
failed: bool,
},
InteractionRequired {
request_id: String,
prompt: String,
},
InteractionReceived {
request_id: String,
},
Warning {
code: String,
message: String,
},
ExecutionCompleted {
answer: String,
},
ExecutionFailed {
code: String,
message: String,
},
ExecutionCanceled,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct EventPage {
pub items: Vec<ExecutionEvent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_after: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ApiErrorBody {
pub code: String,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details: Option<Value>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn input_is_strict_and_tagged() {
let input: RuntimeInput = serde_json::from_value(serde_json::json!({
"type": "user_message", "text": "hello"
}))
.unwrap();
assert_eq!(input.user_text(), Some("hello"));
assert!(
serde_json::from_value::<RuntimeInput>(serde_json::json!({
"type":"user_message", "text":"hello", "secret":"no"
}))
.is_err()
);
let approval: RuntimeInput = serde_json::from_value(serde_json::json!({
"type":"tool_approval", "requestId":"call-1", "approved":true
}))
.unwrap();
assert!(matches!(approval, RuntimeInput::ToolApproval { .. }));
assert!(
serde_json::from_value::<RuntimeInput>(serde_json::json!({
"type":"tool_approval", "request_id":"call-1", "approved":true
}))
.is_err()
);
}
#[test]
fn limits_are_bounded() {
let mut options = ExecutionOptions::default();
assert!(options.validate().is_ok());
options.max_tool_calls = usize::MAX;
assert!(options.validate().is_err());
}
#[test]
fn openapi_snapshot_covers_the_public_execution_surface() {
let document = openapi_document();
assert_eq!(document["openapi"], "3.1.0");
for path in [
"/v1/executions",
"/v1/executions/{executionId}",
"/v1/executions/{executionId}/events",
"/v1/executions/{executionId}/stream",
"/v1/executions/{executionId}/inputs",
"/v1/executions/{executionId}/cancel",
] {
assert!(document["paths"].get(path).is_some(), "missing {path}");
}
}
}