agent-runtime-http-api 0.1.1

Internal HTTP API contract for Agent Runtime
Documentation
//! Runtime-native Agent Task contract.
//!
//! A Task is the public representation of one execution. OpenAI and Claude
//! compatibility requests translate into this shape; provider wire details do
//! not leak into the Kernel.

use std::collections::BTreeMap;

use runtime_types::{ConversationId, ExecutionId, OperationId, RuntimeInstanceId, WorkspaceId};
use serde::{Deserialize, Serialize};

use crate::{
    ApiErrorBody, EventPayload, ExecutionOptions, ExecutionState, ExecutionView,
    ModelGenerationOptions, ModelToolChoice, RuntimeInput, SubmitInputRequest,
};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CreateAgentTaskRequest {
    pub runtime_id: RuntimeInstanceId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub conversation_id: Option<ConversationId>,
    pub input: AgentTaskInput,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<AgentTaskWorkspace>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model: Option<AgentTaskModel>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub instructions: Option<String>,
    #[serde(default)]
    pub limits: AgentTaskLimits,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub metadata: BTreeMap<String, String>,
}

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

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AgentTaskWorkspace {
    pub id: WorkspaceId,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AgentTaskModel {
    pub id: String,
    #[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<serde_json::Value>,
    #[serde(default)]
    pub tool_choice: ModelToolChoice,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AgentTaskLimits {
    #[serde(default = "task_default_deadline")]
    pub deadline_seconds: u64,
    #[serde(default = "task_default_model_turns")]
    pub max_model_turns: usize,
    #[serde(default = "task_default_tool_calls")]
    pub max_tool_calls: usize,
}

impl Default for AgentTaskLimits {
    fn default() -> Self {
        Self {
            deadline_seconds: task_default_deadline(),
            max_model_turns: task_default_model_turns(),
            max_tool_calls: task_default_tool_calls(),
        }
    }
}

fn task_default_deadline() -> u64 {
    900
}
fn task_default_model_turns() -> usize {
    64
}
fn task_default_tool_calls() -> usize {
    256
}

impl CreateAgentTaskRequest {
    pub fn into_execution(self) -> crate::CreateExecutionRequest {
        let generation = self
            .model
            .as_ref()
            .map(|model| ModelGenerationOptions {
                max_output_tokens: model.max_output_tokens,
                temperature: model.temperature,
                stop_sequences: model.stop_sequences.clone(),
                response_format: model.response_format.clone(),
                tool_choice: model.tool_choice.clone(),
            })
            .unwrap_or_default();
        crate::CreateExecutionRequest {
            runtime_instance_id: self.runtime_id,
            conversation_id: self.conversation_id,
            input: RuntimeInput::UserMessage {
                text: self.input.text,
            },
            workspace_id: self.workspace.map(|workspace| workspace.id),
            model: self.model.map(|model| model.id),
            instructions: self.instructions,
            metadata: self.metadata,
            generation,
            options: ExecutionOptions {
                deadline_seconds: self.limits.deadline_seconds,
                max_model_turns: self.limits.max_model_turns,
                max_tool_calls: self.limits.max_tool_calls,
            },
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AgentTask {
    pub id: ExecutionId,
    pub object: String,
    pub runtime_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: Option<AgentTaskWorkspace>,
    #[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 status: AgentTaskStatus,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub output: Option<AgentTaskOutput>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<ApiErrorBody>,
    pub created_at_ms: i64,
    pub updated_at_ms: i64,
    pub links: AgentTaskLinks,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AgentTaskStatus {
    Queued,
    Running,
    WaitingForInput,
    Finalizing,
    Completed,
    Failed,
    Canceled,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AgentTaskOutput {
    pub text: String,
    pub usage: AgentTaskUsage,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AgentTaskUsage {
    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 AgentTaskLinks {
    pub self_url: String,
    pub events: String,
    pub stream: String,
    pub websocket: String,
    pub input: String,
    pub cancel: String,
}

impl From<ExecutionView> for AgentTask {
    fn from(view: ExecutionView) -> Self {
        let base = format!("/v1/agent/tasks/{}", view.id);
        let output = view.outcome.map(|outcome| AgentTaskOutput {
            text: outcome.answer,
            usage: AgentTaskUsage {
                model_turns: outcome.model_turns,
                tool_calls: outcome.tool_calls,
                input_tokens: outcome.input_tokens,
                output_tokens: outcome.output_tokens,
            },
        });
        let error = view.failure.map(|failure| ApiErrorBody {
            code: failure.code,
            message: failure.message,
            request_id: None,
            details: Some(serde_json::json!({"retryable": failure.retryable})),
        });
        Self {
            id: view.id,
            object: "agent.task".into(),
            runtime_id: view.runtime_instance_id,
            conversation_id: view.conversation_id,
            workspace: view.workspace_id.map(|id| AgentTaskWorkspace { id }),
            model: view.model,
            metadata: view.metadata,
            status: view.state.into(),
            output,
            error,
            created_at_ms: view.created_at_ms,
            updated_at_ms: view.updated_at_ms,
            links: AgentTaskLinks {
                self_url: base.clone(),
                events: format!("{base}/events"),
                stream: format!("{base}/stream"),
                websocket: format!("{base}/ws"),
                input: format!("{base}/inputs"),
                cancel: format!("{base}/cancel"),
            },
        }
    }
}

impl From<ExecutionState> for AgentTaskStatus {
    fn from(value: ExecutionState) -> Self {
        match value {
            ExecutionState::Queued => Self::Queued,
            ExecutionState::Running => Self::Running,
            ExecutionState::WaitingForInput => Self::WaitingForInput,
            ExecutionState::Finalizing => Self::Finalizing,
            ExecutionState::Completed => Self::Completed,
            ExecutionState::Failed => Self::Failed,
            ExecutionState::Canceled => Self::Canceled,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AgentTaskEvent {
    pub task_id: ExecutionId,
    pub sequence: u64,
    pub created_at_ms: i64,
    pub kind: String,
    pub data: EventPayload,
}

impl From<crate::ExecutionEvent> for AgentTaskEvent {
    fn from(event: crate::ExecutionEvent) -> Self {
        let kind = match &event.payload {
            EventPayload::ExecutionQueued => "task.queued",
            EventPayload::ExecutionStarted => "task.started",
            EventPayload::ModelStarted { .. } => "model.started",
            EventPayload::ModelCompleted { .. } => "model.completed",
            EventPayload::ToolStarted { .. } => "tool.started",
            EventPayload::ToolCompleted { .. } => "tool.completed",
            EventPayload::InteractionRequired { .. } => "task.input_required",
            EventPayload::InteractionReceived { .. } => "task.input_received",
            EventPayload::Warning { .. } => "task.warning",
            EventPayload::ExecutionCompleted { .. } => "task.completed",
            EventPayload::ExecutionFailed { .. } => "task.failed",
            EventPayload::ExecutionCanceled => "task.canceled",
        };
        Self {
            task_id: event.execution_id,
            sequence: event.sequence,
            created_at_ms: event.created_at_ms,
            kind: kind.into(),
            data: event.payload,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AgentTaskEventPage {
    pub items: Vec<AgentTaskEvent>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub next_after: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(
    tag = "type",
    rename_all = "snake_case",
    rename_all_fields = "camelCase",
    deny_unknown_fields
)]
pub enum AgentTaskWebSocketClientMessage {
    SubmitInput {
        operation_id: OperationId,
        input: RuntimeInput,
    },
    Cancel {},
    /// Application-level acknowledgement helps clients record their own
    /// durable resume cursor. It does not delete server-side events.
    Ack {
        sequence: u64,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(
    tag = "type",
    rename_all = "snake_case",
    rename_all_fields = "camelCase",
    deny_unknown_fields
)]
pub enum AgentTaskWebSocketServerMessage {
    Connected {
        task: AgentTask,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        next_after: Option<u64>,
        heartbeat_seconds: u64,
    },
    Event {
        event: AgentTaskEvent,
    },
    Acknowledged {
        sequence: u64,
    },
    Task {
        task: AgentTask,
    },
    Error {
        error: ApiErrorBody,
    },
}

impl AgentTaskWebSocketClientMessage {
    pub fn into_submit(self) -> Option<SubmitInputRequest> {
        match self {
            Self::SubmitInput {
                operation_id,
                input,
            } => Some(SubmitInputRequest {
                operation_id,
                input,
            }),
            Self::Cancel {} | Self::Ack { .. } => None,
        }
    }
}