mod claude;
mod openai;
mod task;
pub use claude::*;
pub use openai::*;
pub use task::*;
use std::collections::BTreeMap;
use runtime_types::{ConversationId, ExecutionId, OperationId, RuntimeInstanceId, WorkspaceId};
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, skip_serializing_if = "Option::is_none")]
pub workspace_id: Option<WorkspaceId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, String>,
#[serde(default)]
pub generation: ModelGenerationOptions,
#[serde(default)]
pub options: ExecutionOptions,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ModelGenerationOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stop_sequences: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_format: Option<Value>,
#[serde(default)]
pub tool_choice: ModelToolChoice,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum ModelToolChoice {
#[default]
Auto,
None,
}
impl ModelGenerationOptions {
pub fn validate(&self) -> Result<(), &'static str> {
if self.max_output_tokens.is_some_and(|value| value == 0) {
return Err("maxOutputTokens must be greater than zero");
}
if self
.temperature
.is_some_and(|value| !value.is_finite() || !(0.0..=2.0).contains(&value))
{
return Err("temperature must be finite and within 0..=2");
}
if self.stop_sequences.len() > 4
|| self
.stop_sequences
.iter()
.any(|value| value.is_empty() || value.len() > 1024)
{
return Err("stopSequences supports at most 4 non-empty values up to 1024 bytes");
}
if self
.response_format
.as_ref()
.is_some_and(|value| !value.is_object())
{
return Err("responseFormat must be a JSON object");
}
Ok(())
}
}
#[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 },
Messages { messages: Vec<RuntimeMessage> },
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),
Self::Messages { messages } => messages
.iter()
.rev()
.find(|message| message.role == RuntimeMessageRole::User)
.or_else(|| messages.last())
.map(|message| message.text.as_str()),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeMessage {
pub role: RuntimeMessageRole,
pub text: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeMessageRole {
System,
Developer,
User,
Assistant,
}
#[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..=604_800).contains(&self.deadline_seconds) {
return Err("deadlineSeconds must be within 1..=604800");
}
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>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace_id: Option<WorkspaceId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, String>,
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,
invocation_id: String,
},
ModelCompleted {
turn: usize,
invocation_id: String,
finish_reason: String,
input_tokens: Option<u64>,
output_tokens: Option<u64>,
},
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/agent/tasks",
"/v1/agent/tasks/{taskId}/stream",
"/v1/agent/tasks/{taskId}/ws",
"/v1/responses",
"/v1/messages",
] {
assert!(document["paths"].get(path).is_some(), "missing {path}");
}
}
#[test]
fn native_task_preserves_workspace_model_and_long_task_limits() {
let request: CreateAgentTaskRequest = serde_json::from_value(serde_json::json!({
"runtimeId": "runtime-1",
"input": {"text": "inspect it"},
"workspace": {"id": "workspace-2"},
"model": {"id": "model-2", "maxOutputTokens": 4096},
"limits": {"deadlineSeconds": 86400, "maxModelTurns": 8, "maxToolCalls": 32},
"metadata": {"traceId": "trace-1"}
}))
.unwrap();
let execution = request.into_execution();
assert_eq!(execution.workspace_id.unwrap().as_str(), "workspace-2");
assert_eq!(execution.model.as_deref(), Some("model-2"));
assert_eq!(execution.generation.max_output_tokens, Some(4096));
assert_eq!(execution.options.deadline_seconds, 86400);
assert!(execution.options.validate().is_ok());
}
#[test]
fn provider_contracts_accept_their_native_parameter_names() {
let openai: OpenAiResponseRequest = serde_json::from_value(serde_json::json!({
"model": "runtime-1",
"input": "hello",
"max_output_tokens": 512,
"background": true,
"agent": {"workspaceId": "workspace-2"}
}))
.unwrap();
assert!(openai.background);
assert_eq!(openai.max_output_tokens, Some(512));
let claude: ClaudeMessageRequest = serde_json::from_value(serde_json::json!({
"model": "runtime-1",
"max_tokens": 512,
"messages": [{"role": "user", "content": "hello"}],
"agent": {"workspaceId": "workspace-2"}
}))
.unwrap();
assert_eq!(claude.max_tokens, 512);
assert_eq!(claude.messages[0].role, "user");
}
}