Skip to main content

agent_runtime_http_api/
task.rs

1//! Runtime-native Agent Task contract.
2//!
3//! A Task is the public representation of one execution. OpenAI and Claude
4//! compatibility requests translate into this shape; provider wire details do
5//! not leak into the Kernel.
6
7use std::collections::BTreeMap;
8
9use runtime_types::{ConversationId, ExecutionId, OperationId, RuntimeInstanceId, WorkspaceId};
10use serde::{Deserialize, Serialize};
11
12use crate::{
13    ApiErrorBody, EventPayload, ExecutionOptions, ExecutionState, ExecutionView,
14    ModelGenerationOptions, ModelToolChoice, RuntimeInput, SubmitInputRequest,
15};
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18#[serde(rename_all = "camelCase", deny_unknown_fields)]
19pub struct CreateAgentTaskRequest {
20    pub runtime_id: RuntimeInstanceId,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub conversation_id: Option<ConversationId>,
23    pub input: AgentTaskInput,
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub workspace: Option<AgentTaskWorkspace>,
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub model: Option<AgentTaskModel>,
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub instructions: Option<String>,
30    #[serde(default)]
31    pub limits: AgentTaskLimits,
32    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
33    pub metadata: BTreeMap<String, String>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
37#[serde(rename_all = "camelCase", deny_unknown_fields)]
38pub struct AgentTaskInput {
39    pub text: String,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
43#[serde(rename_all = "camelCase", deny_unknown_fields)]
44pub struct AgentTaskWorkspace {
45    pub id: WorkspaceId,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
49#[serde(rename_all = "camelCase", deny_unknown_fields)]
50pub struct AgentTaskModel {
51    pub id: String,
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub max_output_tokens: Option<u32>,
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub temperature: Option<f32>,
56    #[serde(default, skip_serializing_if = "Vec::is_empty")]
57    pub stop_sequences: Vec<String>,
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub response_format: Option<serde_json::Value>,
60    #[serde(default)]
61    pub tool_choice: ModelToolChoice,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65#[serde(rename_all = "camelCase", deny_unknown_fields)]
66pub struct AgentTaskLimits {
67    #[serde(default = "task_default_deadline")]
68    pub deadline_seconds: u64,
69    #[serde(default = "task_default_model_turns")]
70    pub max_model_turns: usize,
71    #[serde(default = "task_default_tool_calls")]
72    pub max_tool_calls: usize,
73}
74
75impl Default for AgentTaskLimits {
76    fn default() -> Self {
77        Self {
78            deadline_seconds: task_default_deadline(),
79            max_model_turns: task_default_model_turns(),
80            max_tool_calls: task_default_tool_calls(),
81        }
82    }
83}
84
85fn task_default_deadline() -> u64 {
86    900
87}
88fn task_default_model_turns() -> usize {
89    64
90}
91fn task_default_tool_calls() -> usize {
92    256
93}
94
95impl CreateAgentTaskRequest {
96    pub fn into_execution(self) -> crate::CreateExecutionRequest {
97        let generation = self
98            .model
99            .as_ref()
100            .map(|model| ModelGenerationOptions {
101                max_output_tokens: model.max_output_tokens,
102                temperature: model.temperature,
103                stop_sequences: model.stop_sequences.clone(),
104                response_format: model.response_format.clone(),
105                tool_choice: model.tool_choice.clone(),
106            })
107            .unwrap_or_default();
108        crate::CreateExecutionRequest {
109            runtime_instance_id: self.runtime_id,
110            conversation_id: self.conversation_id,
111            input: RuntimeInput::UserMessage {
112                text: self.input.text,
113            },
114            workspace_id: self.workspace.map(|workspace| workspace.id),
115            model: self.model.map(|model| model.id),
116            instructions: self.instructions,
117            metadata: self.metadata,
118            generation,
119            options: ExecutionOptions {
120                deadline_seconds: self.limits.deadline_seconds,
121                max_model_turns: self.limits.max_model_turns,
122                max_tool_calls: self.limits.max_tool_calls,
123            },
124        }
125    }
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
129#[serde(rename_all = "camelCase", deny_unknown_fields)]
130pub struct AgentTask {
131    pub id: ExecutionId,
132    pub object: String,
133    pub runtime_id: RuntimeInstanceId,
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub conversation_id: Option<ConversationId>,
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub workspace: Option<AgentTaskWorkspace>,
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub model: Option<String>,
140    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
141    pub metadata: BTreeMap<String, String>,
142    pub status: AgentTaskStatus,
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub output: Option<AgentTaskOutput>,
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub error: Option<ApiErrorBody>,
147    pub created_at_ms: i64,
148    pub updated_at_ms: i64,
149    pub links: AgentTaskLinks,
150}
151
152#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
153#[serde(rename_all = "snake_case")]
154pub enum AgentTaskStatus {
155    Queued,
156    Running,
157    WaitingForInput,
158    Finalizing,
159    Completed,
160    Failed,
161    Canceled,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
165#[serde(rename_all = "camelCase", deny_unknown_fields)]
166pub struct AgentTaskOutput {
167    pub text: String,
168    pub usage: AgentTaskUsage,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
172#[serde(rename_all = "camelCase", deny_unknown_fields)]
173pub struct AgentTaskUsage {
174    pub model_turns: usize,
175    pub tool_calls: usize,
176    pub input_tokens: Option<u64>,
177    pub output_tokens: Option<u64>,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
181#[serde(rename_all = "camelCase", deny_unknown_fields)]
182pub struct AgentTaskLinks {
183    pub self_url: String,
184    pub events: String,
185    pub stream: String,
186    pub websocket: String,
187    pub input: String,
188    pub cancel: String,
189}
190
191impl From<ExecutionView> for AgentTask {
192    fn from(view: ExecutionView) -> Self {
193        let base = format!("/v1/agent/tasks/{}", view.id);
194        let output = view.outcome.map(|outcome| AgentTaskOutput {
195            text: outcome.answer,
196            usage: AgentTaskUsage {
197                model_turns: outcome.model_turns,
198                tool_calls: outcome.tool_calls,
199                input_tokens: outcome.input_tokens,
200                output_tokens: outcome.output_tokens,
201            },
202        });
203        let error = view.failure.map(|failure| ApiErrorBody {
204            code: failure.code,
205            message: failure.message,
206            request_id: None,
207            details: Some(serde_json::json!({"retryable": failure.retryable})),
208        });
209        Self {
210            id: view.id,
211            object: "agent.task".into(),
212            runtime_id: view.runtime_instance_id,
213            conversation_id: view.conversation_id,
214            workspace: view.workspace_id.map(|id| AgentTaskWorkspace { id }),
215            model: view.model,
216            metadata: view.metadata,
217            status: view.state.into(),
218            output,
219            error,
220            created_at_ms: view.created_at_ms,
221            updated_at_ms: view.updated_at_ms,
222            links: AgentTaskLinks {
223                self_url: base.clone(),
224                events: format!("{base}/events"),
225                stream: format!("{base}/stream"),
226                websocket: format!("{base}/ws"),
227                input: format!("{base}/inputs"),
228                cancel: format!("{base}/cancel"),
229            },
230        }
231    }
232}
233
234impl From<ExecutionState> for AgentTaskStatus {
235    fn from(value: ExecutionState) -> Self {
236        match value {
237            ExecutionState::Queued => Self::Queued,
238            ExecutionState::Running => Self::Running,
239            ExecutionState::WaitingForInput => Self::WaitingForInput,
240            ExecutionState::Finalizing => Self::Finalizing,
241            ExecutionState::Completed => Self::Completed,
242            ExecutionState::Failed => Self::Failed,
243            ExecutionState::Canceled => Self::Canceled,
244        }
245    }
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
249#[serde(rename_all = "camelCase", deny_unknown_fields)]
250pub struct AgentTaskEvent {
251    pub task_id: ExecutionId,
252    pub sequence: u64,
253    pub created_at_ms: i64,
254    pub kind: String,
255    pub data: EventPayload,
256}
257
258impl From<crate::ExecutionEvent> for AgentTaskEvent {
259    fn from(event: crate::ExecutionEvent) -> Self {
260        let kind = match &event.payload {
261            EventPayload::ExecutionQueued => "task.queued",
262            EventPayload::ExecutionStarted => "task.started",
263            EventPayload::ModelStarted { .. } => "model.started",
264            EventPayload::ModelCompleted { .. } => "model.completed",
265            EventPayload::ToolStarted { .. } => "tool.started",
266            EventPayload::ToolCompleted { .. } => "tool.completed",
267            EventPayload::InteractionRequired { .. } => "task.input_required",
268            EventPayload::InteractionReceived { .. } => "task.input_received",
269            EventPayload::Warning { .. } => "task.warning",
270            EventPayload::ExecutionCompleted { .. } => "task.completed",
271            EventPayload::ExecutionFailed { .. } => "task.failed",
272            EventPayload::ExecutionCanceled => "task.canceled",
273        };
274        Self {
275            task_id: event.execution_id,
276            sequence: event.sequence,
277            created_at_ms: event.created_at_ms,
278            kind: kind.into(),
279            data: event.payload,
280        }
281    }
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
285#[serde(rename_all = "camelCase", deny_unknown_fields)]
286pub struct AgentTaskEventPage {
287    pub items: Vec<AgentTaskEvent>,
288    #[serde(default, skip_serializing_if = "Option::is_none")]
289    pub next_after: Option<u64>,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
293#[serde(
294    tag = "type",
295    rename_all = "snake_case",
296    rename_all_fields = "camelCase",
297    deny_unknown_fields
298)]
299pub enum AgentTaskWebSocketClientMessage {
300    SubmitInput {
301        operation_id: OperationId,
302        input: RuntimeInput,
303    },
304    Cancel {},
305    /// Application-level acknowledgement helps clients record their own
306    /// durable resume cursor. It does not delete server-side events.
307    Ack {
308        sequence: u64,
309    },
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
313#[serde(
314    tag = "type",
315    rename_all = "snake_case",
316    rename_all_fields = "camelCase",
317    deny_unknown_fields
318)]
319pub enum AgentTaskWebSocketServerMessage {
320    Connected {
321        task: AgentTask,
322        #[serde(default, skip_serializing_if = "Option::is_none")]
323        next_after: Option<u64>,
324        heartbeat_seconds: u64,
325    },
326    Event {
327        event: AgentTaskEvent,
328    },
329    Acknowledged {
330        sequence: u64,
331    },
332    Task {
333        task: AgentTask,
334    },
335    Error {
336        error: ApiErrorBody,
337    },
338}
339
340impl AgentTaskWebSocketClientMessage {
341    pub fn into_submit(self) -> Option<SubmitInputRequest> {
342        match self {
343            Self::SubmitInput {
344                operation_id,
345                input,
346            } => Some(SubmitInputRequest {
347                operation_id,
348                input,
349            }),
350            Self::Cancel {} | Self::Ack { .. } => None,
351        }
352    }
353}