1use serde::{Deserialize, Serialize};
17
18use crate::artifact::Artifact;
19use crate::message::Message;
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
30pub struct TaskId(pub String);
31
32impl TaskId {
33 #[must_use]
35 pub fn new(s: impl Into<String>) -> Self {
36 Self(s.into())
37 }
38}
39
40impl std::fmt::Display for TaskId {
41 #[inline]
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.write_str(&self.0)
44 }
45}
46
47impl From<String> for TaskId {
48 fn from(s: String) -> Self {
49 Self(s)
50 }
51}
52
53impl From<&str> for TaskId {
54 fn from(s: &str) -> Self {
55 Self(s.to_owned())
56 }
57}
58
59impl AsRef<str> for TaskId {
60 #[inline]
61 fn as_ref(&self) -> &str {
62 &self.0
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
75pub struct ContextId(pub String);
76
77impl ContextId {
78 #[must_use]
80 pub fn new(s: impl Into<String>) -> Self {
81 Self(s.into())
82 }
83}
84
85impl std::fmt::Display for ContextId {
86 #[inline]
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 f.write_str(&self.0)
89 }
90}
91
92impl From<String> for ContextId {
93 fn from(s: String) -> Self {
94 Self(s)
95 }
96}
97
98impl From<&str> for ContextId {
99 fn from(s: &str) -> Self {
100 Self(s.to_owned())
101 }
102}
103
104impl AsRef<str> for ContextId {
105 #[inline]
106 fn as_ref(&self) -> &str {
107 &self.0
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
117pub struct TaskVersion(pub u64);
118
119impl TaskVersion {
120 #[must_use]
122 pub const fn new(v: u64) -> Self {
123 Self(v)
124 }
125
126 #[must_use]
128 pub const fn get(self) -> u64 {
129 self.0
130 }
131}
132
133impl std::fmt::Display for TaskVersion {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 write!(f, "{}", self.0)
136 }
137}
138
139impl From<u64> for TaskVersion {
140 fn from(v: u64) -> Self {
141 Self(v)
142 }
143}
144
145#[non_exhaustive]
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
153pub enum TaskState {
154 #[serde(rename = "TASK_STATE_UNSPECIFIED")]
156 Unspecified,
157 #[serde(rename = "TASK_STATE_SUBMITTED")]
159 Submitted,
160 #[serde(rename = "TASK_STATE_WORKING")]
162 Working,
163 #[serde(rename = "TASK_STATE_INPUT_REQUIRED")]
165 InputRequired,
166 #[serde(rename = "TASK_STATE_AUTH_REQUIRED")]
168 AuthRequired,
169 #[serde(rename = "TASK_STATE_COMPLETED")]
171 Completed,
172 #[serde(rename = "TASK_STATE_FAILED")]
174 Failed,
175 #[serde(rename = "TASK_STATE_CANCELED")]
177 Canceled,
178 #[serde(rename = "TASK_STATE_REJECTED")]
180 Rejected,
181}
182
183impl TaskState {
184 #[inline]
188 #[must_use]
189 pub const fn is_terminal(self) -> bool {
190 matches!(
191 self,
192 Self::Completed | Self::Failed | Self::Canceled | Self::Rejected
193 )
194 }
195
196 #[inline]
202 #[must_use]
203 pub const fn can_transition_to(self, next: Self) -> bool {
204 if self.is_terminal() {
206 return false;
207 }
208 if matches!(self, Self::Unspecified) {
210 return true;
211 }
212 matches!(
213 (self, next),
214 (Self::Submitted, Self::Working | Self::Failed | Self::Canceled | Self::Rejected)
216 | (Self::Working,
218 Self::Completed | Self::Failed | Self::Canceled | Self::InputRequired | Self::AuthRequired)
219 | (Self::InputRequired | Self::AuthRequired,
221 Self::Working | Self::Failed | Self::Canceled)
222 )
223 }
224}
225
226impl std::fmt::Display for TaskState {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 let s = match self {
229 Self::Unspecified => "TASK_STATE_UNSPECIFIED",
230 Self::Submitted => "TASK_STATE_SUBMITTED",
231 Self::Working => "TASK_STATE_WORKING",
232 Self::InputRequired => "TASK_STATE_INPUT_REQUIRED",
233 Self::AuthRequired => "TASK_STATE_AUTH_REQUIRED",
234 Self::Completed => "TASK_STATE_COMPLETED",
235 Self::Failed => "TASK_STATE_FAILED",
236 Self::Canceled => "TASK_STATE_CANCELED",
237 Self::Rejected => "TASK_STATE_REJECTED",
238 };
239 f.write_str(s)
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase")]
249pub struct TaskStatus {
250 pub state: TaskState,
252
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub message: Option<Message>,
256
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub timestamp: Option<String>,
260}
261
262impl TaskStatus {
263 #[must_use]
268 pub const fn new(state: TaskState) -> Self {
269 Self {
270 state,
271 message: None,
272 timestamp: None,
273 }
274 }
275
276 #[must_use]
278 pub fn with_timestamp(state: TaskState) -> Self {
279 Self {
280 state,
281 message: None,
282 timestamp: Some(crate::utc_now_iso8601()),
283 }
284 }
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
297#[serde(rename_all = "camelCase")]
298pub struct Task {
299 pub id: TaskId,
301
302 pub context_id: ContextId,
304
305 pub status: TaskStatus,
307
308 #[serde(skip_serializing_if = "Option::is_none")]
310 pub history: Option<Vec<Message>>,
311
312 #[serde(skip_serializing_if = "Option::is_none")]
314 pub artifacts: Option<Vec<Artifact>>,
315
316 #[serde(skip_serializing_if = "Option::is_none")]
318 pub metadata: Option<serde_json::Value>,
319}
320
321#[cfg(test)]
324mod tests {
325 use super::*;
326
327 fn make_task() -> Task {
328 Task {
329 id: TaskId::new("task-1"),
330 context_id: ContextId::new("ctx-1"),
331 status: TaskStatus::new(TaskState::Working),
332 history: None,
333 artifacts: None,
334 metadata: None,
335 }
336 }
337
338 #[test]
339 fn task_state_screaming_snake_case() {
340 assert_eq!(
341 serde_json::to_string(&TaskState::InputRequired).expect("ser"),
342 "\"TASK_STATE_INPUT_REQUIRED\""
343 );
344 assert_eq!(
345 serde_json::to_string(&TaskState::AuthRequired).expect("ser"),
346 "\"TASK_STATE_AUTH_REQUIRED\""
347 );
348 assert_eq!(
349 serde_json::to_string(&TaskState::Submitted).expect("ser"),
350 "\"TASK_STATE_SUBMITTED\""
351 );
352 assert_eq!(
353 serde_json::to_string(&TaskState::Unspecified).expect("ser"),
354 "\"TASK_STATE_UNSPECIFIED\""
355 );
356 }
357
358 #[test]
359 fn task_state_is_terminal() {
360 assert!(TaskState::Completed.is_terminal());
361 assert!(TaskState::Failed.is_terminal());
362 assert!(TaskState::Canceled.is_terminal());
363 assert!(TaskState::Rejected.is_terminal());
364 assert!(!TaskState::Working.is_terminal());
365 assert!(!TaskState::Submitted.is_terminal());
366 }
367
368 #[test]
369 fn task_roundtrip() {
370 let task = make_task();
371 let json = serde_json::to_string(&task).expect("serialize");
372 assert!(json.contains("\"id\":\"task-1\""));
373
374 let back: Task = serde_json::from_str(&json).expect("deserialize");
375 assert_eq!(back.id, TaskId::new("task-1"));
376 assert_eq!(back.context_id, ContextId::new("ctx-1"));
377 assert_eq!(back.status.state, TaskState::Working);
378 }
379
380 #[test]
381 fn optional_fields_omitted() {
382 let task = make_task();
383 let json = serde_json::to_string(&task).expect("serialize");
384 assert!(!json.contains("\"history\""), "history should be omitted");
385 assert!(
386 !json.contains("\"artifacts\""),
387 "artifacts should be omitted"
388 );
389 assert!(!json.contains("\"metadata\""), "metadata should be omitted");
390 }
391
392 #[test]
393 fn task_version_ordering() {
394 assert!(TaskVersion::new(2) > TaskVersion::new(1));
395 assert_eq!(TaskVersion::new(5).get(), 5);
396 }
397
398 #[test]
399 fn wire_format_submitted_state() {
400 let json = serde_json::to_string(&TaskState::Submitted).unwrap();
402 assert_eq!(json, "\"TASK_STATE_SUBMITTED\"");
403
404 let back: TaskState = serde_json::from_str("\"TASK_STATE_SUBMITTED\"").unwrap();
405 assert_eq!(back, TaskState::Submitted);
406 }
407
408 #[test]
409 fn task_version_serde_roundtrip() {
410 let v = TaskVersion::new(42);
411 let json = serde_json::to_string(&v).expect("serialize");
412 assert_eq!(json, "42");
413
414 let back: TaskVersion = serde_json::from_str(&json).expect("deserialize");
415 assert_eq!(back, TaskVersion::new(42));
416
417 let v0 = TaskVersion::new(0);
419 let json0 = serde_json::to_string(&v0).expect("serialize zero");
420 assert_eq!(json0, "0");
421 let back0: TaskVersion = serde_json::from_str(&json0).expect("deserialize zero");
422 assert_eq!(back0, TaskVersion::new(0));
423
424 let vmax = TaskVersion::new(u64::MAX);
426 let json_max = serde_json::to_string(&vmax).expect("serialize max");
427 let back_max: TaskVersion = serde_json::from_str(&json_max).expect("deserialize max");
428 assert_eq!(back_max, vmax);
429 }
430
431 #[test]
432 fn empty_string_ids_work() {
433 let tid = TaskId::new("");
434 let json = serde_json::to_string(&tid).expect("serialize empty TaskId");
435 assert_eq!(json, "\"\"");
436 let back: TaskId = serde_json::from_str(&json).expect("deserialize empty TaskId");
437 assert_eq!(back, TaskId::new(""));
438
439 let cid = ContextId::new("");
440 let json = serde_json::to_string(&cid).expect("serialize empty ContextId");
441 assert_eq!(json, "\"\"");
442 let back: ContextId = serde_json::from_str(&json).expect("deserialize empty ContextId");
443 assert_eq!(back, ContextId::new(""));
444
445 let task = Task {
447 id: TaskId::new(""),
448 context_id: ContextId::new(""),
449 status: TaskStatus::new(TaskState::Submitted),
450 history: None,
451 artifacts: None,
452 metadata: None,
453 };
454 let json = serde_json::to_string(&task).expect("serialize task with empty ids");
455 let back: Task = serde_json::from_str(&json).expect("deserialize task with empty ids");
456 assert_eq!(back.id, TaskId::new(""));
457 assert_eq!(back.context_id, ContextId::new(""));
458 }
459
460 #[test]
461 fn task_state_display_trait() {
462 assert_eq!(TaskState::Working.to_string(), "TASK_STATE_WORKING");
463 assert_eq!(TaskState::Completed.to_string(), "TASK_STATE_COMPLETED");
464 assert_eq!(TaskState::Failed.to_string(), "TASK_STATE_FAILED");
465 assert_eq!(TaskState::Canceled.to_string(), "TASK_STATE_CANCELED");
466 assert_eq!(TaskState::Rejected.to_string(), "TASK_STATE_REJECTED");
467 assert_eq!(TaskState::Submitted.to_string(), "TASK_STATE_SUBMITTED");
468 assert_eq!(
469 TaskState::InputRequired.to_string(),
470 "TASK_STATE_INPUT_REQUIRED"
471 );
472 assert_eq!(
473 TaskState::AuthRequired.to_string(),
474 "TASK_STATE_AUTH_REQUIRED"
475 );
476 assert_eq!(TaskState::Unspecified.to_string(), "TASK_STATE_UNSPECIFIED");
477 }
478}