1use std::path::PathBuf;
4
5use chrono::{DateTime, Utc};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use zagens_runtime_adapters::json_schema_util::path_as_string;
9
10const TIMELINE_SUMMARY_LIMIT: usize = 240;
11
12pub const CURRENT_TASK_SCHEMA_VERSION: u32 = 2;
13
14const fn default_task_schema_version() -> u32 {
15 CURRENT_TASK_SCHEMA_VERSION
16}
17
18fn default_auto_approve() -> bool {
19 true
20}
21
22#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
24#[serde(rename_all = "snake_case")]
25pub enum TaskStatus {
26 Queued,
27 Running,
28 Completed,
29 Failed,
30 Canceled,
31}
32
33impl TaskStatus {
34 #[must_use]
35 pub fn is_terminal(self) -> bool {
36 matches!(self, Self::Completed | Self::Failed | Self::Canceled)
37 }
38}
39
40#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
42#[serde(rename_all = "snake_case")]
43pub enum TaskToolStatus {
44 Running,
45 Success,
46 Failed,
47 Canceled,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct TaskTimelineEntry {
53 pub timestamp: DateTime<Utc>,
54 pub kind: String,
55 pub summary: String,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub detail_path: Option<PathBuf>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct TaskToolCallSummary {
63 pub id: String,
64 pub name: String,
65 pub status: TaskToolStatus,
66 pub started_at: DateTime<Utc>,
67 pub ended_at: Option<DateTime<Utc>>,
68 pub duration_ms: Option<u64>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub input_summary: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub output_summary: Option<String>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub detail_path: Option<PathBuf>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub patch_ref: Option<PathBuf>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct TaskChecklistItem {
82 pub id: u32,
83 pub content: String,
84 pub status: String,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, Default)]
89pub struct TaskChecklistState {
90 pub items: Vec<TaskChecklistItem>,
91 pub completion_pct: u8,
92 pub in_progress_id: Option<u32>,
93 pub updated_at: Option<DateTime<Utc>>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct TaskGateRecord {
99 pub id: String,
100 pub gate: String,
101 pub command: String,
102 pub cwd: PathBuf,
103 pub exit_code: Option<i32>,
104 pub status: String,
105 pub classification: String,
106 pub duration_ms: u64,
107 pub summary: String,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub log_path: Option<PathBuf>,
110 pub recorded_at: DateTime<Utc>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct TaskAttemptRecord {
116 pub id: String,
117 pub attempt_group_id: String,
118 pub attempt_index: u32,
119 pub attempt_count: u32,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub base_ref: Option<String>,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub base_sha: Option<String>,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub head_ref: Option<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub head_sha: Option<String>,
128 pub summary: String,
129 pub changed_files: Vec<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub patch_path: Option<PathBuf>,
132 pub verification: Vec<String>,
133 pub selected: bool,
134 pub recorded_at: DateTime<Utc>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct TaskArtifactRef {
140 pub label: String,
141 pub path: PathBuf,
142 pub summary: String,
143 pub created_at: DateTime<Utc>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct TaskGithubEvent {
149 pub id: String,
150 pub action: String,
151 pub target: String,
152 pub number: u64,
153 pub summary: String,
154 pub url: Option<String>,
155 pub recorded_at: DateTime<Utc>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
160pub struct TaskRecord {
161 #[serde(default = "default_task_schema_version")]
162 pub schema_version: u32,
163 pub id: String,
164 pub prompt: String,
165 pub model: String,
166 #[schemars(schema_with = "path_as_string")]
167 pub workspace: PathBuf,
168 pub mode: String,
169 pub allow_shell: bool,
170 pub trust_mode: bool,
171 #[serde(default = "default_auto_approve")]
172 pub auto_approve: bool,
173 pub status: TaskStatus,
174 pub created_at: DateTime<Utc>,
175 pub started_at: Option<DateTime<Utc>>,
176 pub ended_at: Option<DateTime<Utc>>,
177 pub duration_ms: Option<u64>,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 pub result_summary: Option<String>,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub result_detail_path: Option<PathBuf>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub error: Option<String>,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub thread_id: Option<String>,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub turn_id: Option<String>,
188 #[serde(default)]
189 pub runtime_event_count: usize,
190 #[serde(default)]
191 #[schemars(skip)]
192 pub checklist: TaskChecklistState,
193 #[serde(default)]
194 #[schemars(skip)]
195 pub gates: Vec<TaskGateRecord>,
196 #[serde(default)]
197 #[schemars(skip)]
198 pub attempts: Vec<TaskAttemptRecord>,
199 #[serde(default)]
200 #[schemars(skip)]
201 pub artifacts: Vec<TaskArtifactRef>,
202 #[serde(default)]
203 #[schemars(skip)]
204 pub github_events: Vec<TaskGithubEvent>,
205 #[schemars(skip)]
206 pub tool_calls: Vec<TaskToolCallSummary>,
207 #[schemars(skip)]
208 pub timeline: Vec<TaskTimelineEntry>,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
213pub struct TaskSummary {
214 pub id: String,
215 pub status: TaskStatus,
216 pub prompt_summary: String,
217 pub model: String,
218 pub mode: String,
219 pub created_at: DateTime<Utc>,
220 pub started_at: Option<DateTime<Utc>>,
221 pub ended_at: Option<DateTime<Utc>>,
222 pub duration_ms: Option<u64>,
223 #[serde(skip_serializing_if = "Option::is_none")]
224 pub error: Option<String>,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub thread_id: Option<String>,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub turn_id: Option<String>,
229}
230
231fn summarize_text(text: &str, limit: usize) -> String {
232 let take = limit.saturating_sub(3);
233 let mut count = 0;
234 let mut out = String::new();
235 for ch in text.chars() {
236 if count >= take {
237 out.push_str("...");
238 return out;
239 }
240 if ch.is_control() && ch != '\n' && ch != '\t' {
241 continue;
242 }
243 out.push(ch);
244 count += 1;
245 }
246 out
247}
248
249impl From<&TaskRecord> for TaskSummary {
250 fn from(value: &TaskRecord) -> Self {
251 Self {
252 id: value.id.clone(),
253 status: value.status,
254 prompt_summary: summarize_text(&value.prompt, TIMELINE_SUMMARY_LIMIT),
255 model: value.model.clone(),
256 mode: value.mode.clone(),
257 created_at: value.created_at,
258 started_at: value.started_at,
259 ended_at: value.ended_at,
260 duration_ms: value.duration_ms,
261 error: value.error.clone(),
262 thread_id: value.thread_id.clone(),
263 turn_id: value.turn_id.clone(),
264 }
265 }
266}
267
268#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, JsonSchema)]
270pub struct TaskCounts {
271 pub queued: usize,
272 pub running: usize,
273 pub completed: usize,
274 pub failed: usize,
275 pub canceled: usize,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
279pub struct TasksResponse {
280 pub tasks: Vec<TaskSummary>,
281 pub counts: TaskCounts,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct NewTaskRequest {
287 pub prompt: String,
288 pub model: Option<String>,
289 pub workspace: Option<PathBuf>,
290 pub mode: Option<String>,
291 pub allow_shell: Option<bool>,
292 pub trust_mode: Option<bool>,
293 pub auto_approve: Option<bool>,
294}
295
296impl NewTaskRequest {
297 #[must_use]
298 pub fn from_prompt(prompt: impl Into<String>) -> Self {
299 Self {
300 prompt: prompt.into(),
301 model: None,
302 workspace: None,
303 mode: None,
304 allow_shell: None,
305 trust_mode: None,
306 auto_approve: Some(true),
307 }
308 }
309}