use std::path::PathBuf;
use chrono::{DateTime, Utc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use zagens_runtime_adapters::json_schema_util::path_as_string;
const TIMELINE_SUMMARY_LIMIT: usize = 240;
pub const CURRENT_TASK_SCHEMA_VERSION: u32 = 2;
const fn default_task_schema_version() -> u32 {
CURRENT_TASK_SCHEMA_VERSION
}
fn default_auto_approve() -> bool {
true
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TaskStatus {
Queued,
Running,
Completed,
Failed,
Canceled,
}
impl TaskStatus {
#[must_use]
pub fn is_terminal(self) -> bool {
matches!(self, Self::Completed | Self::Failed | Self::Canceled)
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TaskToolStatus {
Running,
Success,
Failed,
Canceled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskTimelineEntry {
pub timestamp: DateTime<Utc>,
pub kind: String,
pub summary: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail_path: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskToolCallSummary {
pub id: String,
pub name: String,
pub status: TaskToolStatus,
pub started_at: DateTime<Utc>,
pub ended_at: Option<DateTime<Utc>>,
pub duration_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail_path: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub patch_ref: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskChecklistItem {
pub id: u32,
pub content: String,
pub status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TaskChecklistState {
pub items: Vec<TaskChecklistItem>,
pub completion_pct: u8,
pub in_progress_id: Option<u32>,
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskGateRecord {
pub id: String,
pub gate: String,
pub command: String,
pub cwd: PathBuf,
pub exit_code: Option<i32>,
pub status: String,
pub classification: String,
pub duration_ms: u64,
pub summary: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub log_path: Option<PathBuf>,
pub recorded_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskAttemptRecord {
pub id: String,
pub attempt_group_id: String,
pub attempt_index: u32,
pub attempt_count: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_sha: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub head_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub head_sha: Option<String>,
pub summary: String,
pub changed_files: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub patch_path: Option<PathBuf>,
pub verification: Vec<String>,
pub selected: bool,
pub recorded_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskArtifactRef {
pub label: String,
pub path: PathBuf,
pub summary: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskGithubEvent {
pub id: String,
pub action: String,
pub target: String,
pub number: u64,
pub summary: String,
pub url: Option<String>,
pub recorded_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TaskRecord {
#[serde(default = "default_task_schema_version")]
pub schema_version: u32,
pub id: String,
pub prompt: String,
pub model: String,
#[schemars(schema_with = "path_as_string")]
pub workspace: PathBuf,
pub mode: String,
pub allow_shell: bool,
pub trust_mode: bool,
#[serde(default = "default_auto_approve")]
pub auto_approve: bool,
pub status: TaskStatus,
pub created_at: DateTime<Utc>,
pub started_at: Option<DateTime<Utc>>,
pub ended_at: Option<DateTime<Utc>>,
pub duration_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result_summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result_detail_path: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thread_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_id: Option<String>,
#[serde(default)]
pub runtime_event_count: usize,
#[serde(default)]
#[schemars(skip)]
pub checklist: TaskChecklistState,
#[serde(default)]
#[schemars(skip)]
pub gates: Vec<TaskGateRecord>,
#[serde(default)]
#[schemars(skip)]
pub attempts: Vec<TaskAttemptRecord>,
#[serde(default)]
#[schemars(skip)]
pub artifacts: Vec<TaskArtifactRef>,
#[serde(default)]
#[schemars(skip)]
pub github_events: Vec<TaskGithubEvent>,
#[schemars(skip)]
pub tool_calls: Vec<TaskToolCallSummary>,
#[schemars(skip)]
pub timeline: Vec<TaskTimelineEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TaskSummary {
pub id: String,
pub status: TaskStatus,
pub prompt_summary: String,
pub model: String,
pub mode: String,
pub created_at: DateTime<Utc>,
pub started_at: Option<DateTime<Utc>>,
pub ended_at: Option<DateTime<Utc>>,
pub duration_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thread_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_id: Option<String>,
}
fn summarize_text(text: &str, limit: usize) -> String {
let take = limit.saturating_sub(3);
let mut count = 0;
let mut out = String::new();
for ch in text.chars() {
if count >= take {
out.push_str("...");
return out;
}
if ch.is_control() && ch != '\n' && ch != '\t' {
continue;
}
out.push(ch);
count += 1;
}
out
}
impl From<&TaskRecord> for TaskSummary {
fn from(value: &TaskRecord) -> Self {
Self {
id: value.id.clone(),
status: value.status,
prompt_summary: summarize_text(&value.prompt, TIMELINE_SUMMARY_LIMIT),
model: value.model.clone(),
mode: value.mode.clone(),
created_at: value.created_at,
started_at: value.started_at,
ended_at: value.ended_at,
duration_ms: value.duration_ms,
error: value.error.clone(),
thread_id: value.thread_id.clone(),
turn_id: value.turn_id.clone(),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, JsonSchema)]
pub struct TaskCounts {
pub queued: usize,
pub running: usize,
pub completed: usize,
pub failed: usize,
pub canceled: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TasksResponse {
pub tasks: Vec<TaskSummary>,
pub counts: TaskCounts,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewTaskRequest {
pub prompt: String,
pub model: Option<String>,
pub workspace: Option<PathBuf>,
pub mode: Option<String>,
pub allow_shell: Option<bool>,
pub trust_mode: Option<bool>,
pub auto_approve: Option<bool>,
}
impl NewTaskRequest {
#[must_use]
pub fn from_prompt(prompt: impl Into<String>) -> Self {
Self {
prompt: prompt.into(),
model: None,
workspace: None,
mode: None,
allow_shell: None,
trust_mode: None,
auto_approve: Some(true),
}
}
}