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 {},
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,
}
}
}