Skip to main content

routa_core/models/
task.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4/// Transport protocol for task sessions
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
6#[serde(rename_all = "lowercase")]
7pub enum TaskSessionTransport {
8    /// Agent Chat Protocol
9    Acp,
10    /// Agent-to-Agent protocol
11    A2a,
12}
13
14/// Status of a task lane session
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16#[serde(rename_all = "snake_case")]
17pub enum TaskLaneSessionStatus {
18    Running,
19    Completed,
20    Failed,
21    TimedOut,
22    Transitioned,
23}
24
25/// Loop mode for task lane session recovery
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27#[serde(rename_all = "snake_case")]
28pub enum TaskLaneSessionLoopMode {
29    WatchdogRetry,
30    RalphLoop,
31}
32
33/// Completion requirement for task lane session
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35#[serde(rename_all = "snake_case")]
36pub enum TaskLaneSessionCompletionRequirement {
37    TurnComplete,
38    CompletionSummary,
39    VerificationReport,
40}
41
42/// Recovery reason for task lane session
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44#[serde(rename_all = "snake_case")]
45pub enum TaskLaneSessionRecoveryReason {
46    WatchdogInactivity,
47    AgentFailed,
48    CompletionCriteriaNotMet,
49}
50
51/// Session associated with a task lane transition
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53#[serde(rename_all = "camelCase")]
54pub struct TaskLaneSession {
55    pub session_id: String,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub routa_agent_id: Option<String>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub column_id: Option<String>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub column_name: Option<String>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub step_id: Option<String>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub step_index: Option<i64>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub step_name: Option<String>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub provider: Option<String>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub role: Option<String>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub specialist_id: Option<String>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub specialist_name: Option<String>,
76    /// Transport protocol used for this session
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub transport: Option<String>,
79    /// A2A-specific: External task ID from the agent system
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub external_task_id: Option<String>,
82    /// A2A-specific: Context ID for tracking the conversation
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub context_id: Option<String>,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub attempt: Option<i64>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub loop_mode: Option<TaskLaneSessionLoopMode>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub completion_requirement: Option<TaskLaneSessionCompletionRequirement>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub objective: Option<String>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub last_activity_at: Option<String>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub recovered_from_session_id: Option<String>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub recovery_reason: Option<TaskLaneSessionRecoveryReason>,
99    pub status: TaskLaneSessionStatus,
100    pub started_at: String,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub completed_at: Option<String>,
103}
104
105/// Handoff request type for task lane transitions
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
107#[serde(rename_all = "snake_case")]
108pub enum TaskLaneHandoffRequestType {
109    EnvironmentPreparation,
110    RuntimeContext,
111    Clarification,
112    RerunCommand,
113}
114
115/// Handoff status for task lane transitions
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
117#[serde(rename_all = "snake_case")]
118pub enum TaskLaneHandoffStatus {
119    Requested,
120    Delivered,
121    Completed,
122    Blocked,
123    Failed,
124}
125
126/// Handoff between adjacent lane sessions
127#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
128#[serde(rename_all = "camelCase")]
129pub struct TaskLaneHandoff {
130    pub id: String,
131    pub from_session_id: String,
132    pub to_session_id: String,
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub from_column_id: Option<String>,
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub to_column_id: Option<String>,
137    pub request_type: TaskLaneHandoffRequestType,
138    pub request: String,
139    pub status: TaskLaneHandoffStatus,
140    pub requested_at: String,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub responded_at: Option<String>,
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub response_summary: Option<String>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
148pub enum TaskPriority {
149    #[serde(rename = "low")]
150    Low,
151    #[serde(rename = "medium")]
152    Medium,
153    #[serde(rename = "high")]
154    High,
155    #[serde(rename = "urgent")]
156    Urgent,
157}
158
159impl TaskPriority {
160    pub fn as_str(&self) -> &'static str {
161        match self {
162            Self::Low => "low",
163            Self::Medium => "medium",
164            Self::High => "high",
165            Self::Urgent => "urgent",
166        }
167    }
168
169    #[allow(clippy::should_implement_trait)]
170    pub fn from_str(s: &str) -> Option<Self> {
171        match s {
172            "low" => Some(Self::Low),
173            "medium" => Some(Self::Medium),
174            "high" => Some(Self::High),
175            "urgent" => Some(Self::Urgent),
176            _ => None,
177        }
178    }
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
182pub enum TaskStatus {
183    #[serde(rename = "PENDING")]
184    Pending,
185    #[serde(rename = "IN_PROGRESS")]
186    InProgress,
187    #[serde(rename = "REVIEW_REQUIRED")]
188    ReviewRequired,
189    #[serde(rename = "COMPLETED")]
190    Completed,
191    #[serde(rename = "NEEDS_FIX")]
192    NeedsFix,
193    #[serde(rename = "BLOCKED")]
194    Blocked,
195    #[serde(rename = "CANCELLED")]
196    Cancelled,
197}
198
199impl TaskStatus {
200    pub fn as_str(&self) -> &'static str {
201        match self {
202            Self::Pending => "PENDING",
203            Self::InProgress => "IN_PROGRESS",
204            Self::ReviewRequired => "REVIEW_REQUIRED",
205            Self::Completed => "COMPLETED",
206            Self::NeedsFix => "NEEDS_FIX",
207            Self::Blocked => "BLOCKED",
208            Self::Cancelled => "CANCELLED",
209        }
210    }
211
212    #[allow(clippy::should_implement_trait)]
213    pub fn from_str(s: &str) -> Option<Self> {
214        match s {
215            "PENDING" => Some(Self::Pending),
216            "IN_PROGRESS" => Some(Self::InProgress),
217            "REVIEW_REQUIRED" => Some(Self::ReviewRequired),
218            "COMPLETED" => Some(Self::Completed),
219            "NEEDS_FIX" => Some(Self::NeedsFix),
220            "BLOCKED" => Some(Self::Blocked),
221            "CANCELLED" => Some(Self::Cancelled),
222            _ => None,
223        }
224    }
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
228pub enum VerificationVerdict {
229    #[serde(rename = "APPROVED")]
230    Approved,
231    #[serde(rename = "NOT_APPROVED")]
232    NotApproved,
233    #[serde(rename = "BLOCKED")]
234    Blocked,
235}
236
237impl VerificationVerdict {
238    pub fn as_str(&self) -> &'static str {
239        match self {
240            Self::Approved => "APPROVED",
241            Self::NotApproved => "NOT_APPROVED",
242            Self::Blocked => "BLOCKED",
243        }
244    }
245
246    #[allow(clippy::should_implement_trait)]
247    pub fn from_str(s: &str) -> Option<Self> {
248        match s {
249            "APPROVED" => Some(Self::Approved),
250            "NOT_APPROVED" => Some(Self::NotApproved),
251            "BLOCKED" => Some(Self::Blocked),
252            _ => None,
253        }
254    }
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
258#[serde(rename_all = "camelCase")]
259pub struct Task {
260    pub id: String,
261    pub title: String,
262    pub objective: String,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub comment: Option<String>,
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub scope: Option<String>,
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub acceptance_criteria: Option<Vec<String>>,
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub verification_commands: Option<Vec<String>>,
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub test_cases: Option<Vec<String>>,
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub assigned_to: Option<String>,
275    pub status: TaskStatus,
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub board_id: Option<String>,
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub column_id: Option<String>,
280    #[serde(default)]
281    pub position: i64,
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub priority: Option<TaskPriority>,
284    #[serde(default)]
285    pub labels: Vec<String>,
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub assignee: Option<String>,
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub assigned_provider: Option<String>,
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub assigned_role: Option<String>,
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub assigned_specialist_id: Option<String>,
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub assigned_specialist_name: Option<String>,
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub trigger_session_id: Option<String>,
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub github_id: Option<String>,
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub github_number: Option<i64>,
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub github_url: Option<String>,
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub github_repo: Option<String>,
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub github_state: Option<String>,
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub github_synced_at: Option<DateTime<Utc>>,
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub last_sync_error: Option<String>,
312    #[serde(default)]
313    pub dependencies: Vec<String>,
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub parallel_group: Option<String>,
316    pub workspace_id: String,
317    /// Session ID that created this task (for session-scoped filtering)
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub session_id: Option<String>,
320    /// Codebase IDs linked to this task
321    #[serde(default)]
322    pub codebase_ids: Vec<String>,
323    /// Worktree ID assigned to this task
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub worktree_id: Option<String>,
326    /// All session IDs that have been associated with this task (history)
327    #[serde(default)]
328    pub session_ids: Vec<String>,
329    /// Durable per-lane session history for Kanban workflow handoff
330    #[serde(default)]
331    pub lane_sessions: Vec<TaskLaneSession>,
332    /// Adjacent-lane handoff requests and responses
333    #[serde(default)]
334    pub lane_handoffs: Vec<TaskLaneHandoff>,
335    pub created_at: DateTime<Utc>,
336    pub updated_at: DateTime<Utc>,
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub completion_summary: Option<String>,
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub verification_verdict: Option<VerificationVerdict>,
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub verification_report: Option<String>,
343}
344
345impl Task {
346    #[allow(clippy::too_many_arguments)]
347    pub fn new(
348        id: String,
349        title: String,
350        objective: String,
351        workspace_id: String,
352        session_id: Option<String>,
353        scope: Option<String>,
354        acceptance_criteria: Option<Vec<String>>,
355        verification_commands: Option<Vec<String>>,
356        test_cases: Option<Vec<String>>,
357        dependencies: Option<Vec<String>>,
358        parallel_group: Option<String>,
359    ) -> Self {
360        let now = Utc::now();
361        Self {
362            id,
363            title,
364            objective,
365            comment: None,
366            scope,
367            acceptance_criteria,
368            verification_commands,
369            test_cases,
370            assigned_to: None,
371            status: TaskStatus::Pending,
372            board_id: None,
373            column_id: Some("backlog".to_string()),
374            position: 0,
375            priority: None,
376            labels: Vec::new(),
377            assignee: None,
378            assigned_provider: None,
379            assigned_role: None,
380            assigned_specialist_id: None,
381            assigned_specialist_name: None,
382            trigger_session_id: None,
383            github_id: None,
384            github_number: None,
385            github_url: None,
386            github_repo: None,
387            github_state: None,
388            github_synced_at: None,
389            last_sync_error: None,
390            dependencies: dependencies.unwrap_or_default(),
391            parallel_group,
392            workspace_id,
393            session_id,
394            codebase_ids: Vec::new(),
395            worktree_id: None,
396            session_ids: Vec::new(),
397            lane_sessions: Vec::new(),
398            lane_handoffs: Vec::new(),
399            created_at: now,
400            updated_at: now,
401            completion_summary: None,
402            verification_verdict: None,
403            verification_report: None,
404        }
405    }
406}