use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::agent::AgentId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MissionId(Uuid);
impl MissionId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::new_v4())
}
#[must_use]
pub fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
#[must_use]
pub fn short_id(&self) -> String {
self.0.to_string().chars().take(4).collect()
}
}
impl Default for MissionId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for MissionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::str::FromStr for MissionId {
type Err = uuid::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(Uuid::parse_str(s)?))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct WorkItemId(Uuid);
impl WorkItemId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::new_v4())
}
#[must_use]
pub fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
}
impl Default for WorkItemId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for WorkItemId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::str::FromStr for WorkItemId {
type Err = uuid::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(Uuid::parse_str(s)?))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct WatchId(Uuid);
impl WatchId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::new_v4())
}
#[must_use]
pub fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
}
impl Default for WatchId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for WatchId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::str::FromStr for WatchId {
type Err = uuid::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(Uuid::parse_str(s)?))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum MissionState {
#[default]
Planning,
Running,
Blocked,
Completed,
Failed,
}
impl MissionState {
#[must_use]
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Completed | Self::Failed)
}
#[must_use]
pub fn can_pause(&self) -> bool {
matches!(self, Self::Running)
}
#[must_use]
pub fn can_resume(&self) -> bool {
matches!(self, Self::Blocked)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum WorkKind {
Design,
#[default]
Implement,
Test,
Review,
MergeGate,
Followup,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum WorkStatus {
#[default]
Pending,
Ready,
Assigned,
Running,
Blocked,
Done,
}
impl WorkStatus {
#[must_use]
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Done)
}
#[must_use]
pub fn is_ready(&self) -> bool {
matches!(self, Self::Ready)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum WatchKind {
#[default]
PrChecks,
BugbotComments,
ReviewComments,
Mergeability,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum WatchStatus {
#[default]
Active,
Snoozed,
Done,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TriggerAction {
#[default]
CreateFixTask,
NotifyReviewer,
AdvancePipeline,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ObjectiveRef {
Issue {
owner: String,
repo: String,
number: u64,
},
Doc {
path: String,
},
}
impl std::fmt::Display for ObjectiveRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ObjectiveRef::Issue {
owner,
repo,
number,
} => {
write!(f, "{}/{}#{}", owner, repo, number)
}
ObjectiveRef::Doc { path } => write!(f, "{}", path),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MissionPolicy {
pub max_parallel_items: u32,
pub reviewer_required: bool,
pub auto_merge: bool,
pub watch_interval_secs: u64,
}
impl Default for MissionPolicy {
fn default() -> Self {
Self {
max_parallel_items: 2,
reviewer_required: true,
auto_merge: false,
watch_interval_secs: 180,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MissionRun {
pub id: MissionId,
pub objective_refs: Vec<ObjectiveRef>,
pub state: MissionState,
pub policy: MissionPolicy,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub next_wake_at: Option<DateTime<Utc>>,
pub blocked_reason: Option<String>,
pub dispatcher_last_tick_at: Option<DateTime<Utc>>,
pub dispatcher_last_progress_at: Option<DateTime<Utc>>,
pub dispatcher_last_help_request_at: Option<DateTime<Utc>>,
pub dispatcher_last_help_request_reason: Option<String>,
#[serde(default)]
pub dispatcher_help_request_attempts: u32,
}
impl MissionRun {
#[must_use]
pub fn new(objectives: Vec<ObjectiveRef>) -> Self {
let now = Utc::now();
Self {
id: MissionId::new(),
objective_refs: objectives,
state: MissionState::Planning,
policy: MissionPolicy::default(),
created_at: now,
updated_at: now,
next_wake_at: None,
blocked_reason: None,
dispatcher_last_tick_at: None,
dispatcher_last_progress_at: None,
dispatcher_last_help_request_at: None,
dispatcher_last_help_request_reason: None,
dispatcher_help_request_attempts: 0,
}
}
#[must_use]
pub fn with_policy(mut self, policy: MissionPolicy) -> Self {
self.policy = policy;
self
}
pub fn start(&mut self) {
self.state = MissionState::Running;
self.next_wake_at = None;
self.blocked_reason = None;
self.updated_at = Utc::now();
}
pub fn block(&mut self, reason: impl Into<String>) {
self.state = MissionState::Blocked;
self.blocked_reason = Some(reason.into());
self.updated_at = Utc::now();
}
pub fn set_next_wake_at(&mut self, next_wake_at: Option<DateTime<Utc>>) {
self.next_wake_at = next_wake_at;
self.updated_at = Utc::now();
}
pub fn record_dispatch_tick(&mut self) {
self.dispatcher_last_tick_at = Some(Utc::now());
self.updated_at = Utc::now();
}
pub fn record_dispatch_progress(&mut self) {
let now = Utc::now();
self.dispatcher_last_tick_at = Some(now);
self.dispatcher_last_progress_at = Some(now);
self.dispatcher_help_request_attempts = 0;
self.updated_at = now;
}
pub fn record_help_request(&mut self, reason: impl Into<String>, reason_changed: bool) {
let now = Utc::now();
self.dispatcher_last_help_request_at = Some(now);
self.dispatcher_last_help_request_reason = Some(reason.into());
self.dispatcher_help_request_attempts = if reason_changed {
1
} else {
self.dispatcher_help_request_attempts.saturating_add(1)
};
self.updated_at = now;
}
pub fn complete(&mut self) {
self.state = MissionState::Completed;
self.next_wake_at = None;
self.blocked_reason = None;
self.updated_at = Utc::now();
}
pub fn fail(&mut self, reason: impl Into<String>) {
self.state = MissionState::Failed;
self.next_wake_at = None;
self.blocked_reason = Some(reason.into());
self.updated_at = Utc::now();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkItem {
pub id: WorkItemId,
pub mission_id: MissionId,
pub title: String,
pub kind: WorkKind,
pub depends_on: Vec<WorkItemId>,
pub owner_role: Option<String>,
pub status: WorkStatus,
pub assigned_to: Option<AgentId>,
pub artifact_refs: Vec<String>,
#[serde(default)]
pub reviewer_approved: bool,
pub source_ref: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl WorkItem {
#[must_use]
pub fn new(mission_id: MissionId, title: impl Into<String>, kind: WorkKind) -> Self {
let now = Utc::now();
Self {
id: WorkItemId::new(),
mission_id,
title: title.into(),
kind,
depends_on: Vec::new(),
owner_role: None,
status: WorkStatus::Pending,
assigned_to: None,
artifact_refs: Vec::new(),
reviewer_approved: false,
source_ref: None,
created_at: now,
updated_at: now,
}
}
#[must_use]
pub fn with_dependencies(mut self, deps: Vec<WorkItemId>) -> Self {
self.depends_on = deps;
self
}
#[must_use]
pub fn with_owner_role(mut self, role: impl Into<String>) -> Self {
self.owner_role = Some(role.into());
self
}
#[must_use]
pub fn with_source_ref(mut self, source: impl Into<String>) -> Self {
self.source_ref = Some(source.into());
self
}
pub fn mark_ready(&mut self) {
self.status = WorkStatus::Ready;
self.updated_at = Utc::now();
}
pub fn assign(&mut self, agent_id: AgentId) {
self.assigned_to = Some(agent_id);
self.status = WorkStatus::Assigned;
self.reviewer_approved = false;
self.updated_at = Utc::now();
}
pub fn start(&mut self) {
self.status = WorkStatus::Running;
self.updated_at = Utc::now();
}
pub fn block(&mut self) {
self.status = WorkStatus::Blocked;
self.updated_at = Utc::now();
}
pub fn record_artifacts(&mut self, artifacts: impl IntoIterator<Item = impl Into<String>>) {
self.artifact_refs
.extend(artifacts.into_iter().map(Into::into));
self.updated_at = Utc::now();
}
pub fn approve_review(&mut self) {
self.reviewer_approved = true;
self.updated_at = Utc::now();
}
pub fn clear_review_approval(&mut self) {
self.reviewer_approved = false;
self.updated_at = Utc::now();
}
pub fn complete(&mut self, artifacts: Vec<String>) {
self.status = WorkStatus::Done;
self.artifact_refs.extend(artifacts);
self.updated_at = Utc::now();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchItem {
pub id: WatchId,
pub mission_id: MissionId,
pub work_item_id: WorkItemId,
pub kind: WatchKind,
pub target_ref: String,
pub interval_secs: u64,
pub next_due_at: DateTime<Utc>,
pub status: WatchStatus,
pub on_trigger: TriggerAction,
pub last_check_at: Option<DateTime<Utc>>,
pub consecutive_failures: u32,
}
impl WatchItem {
#[must_use]
pub fn new(
mission_id: MissionId,
work_item_id: WorkItemId,
kind: WatchKind,
target_ref: impl Into<String>,
interval_secs: u64,
) -> Self {
Self {
id: WatchId::new(),
mission_id,
work_item_id,
kind,
target_ref: target_ref.into(),
interval_secs,
next_due_at: Utc::now(),
status: WatchStatus::Active,
on_trigger: TriggerAction::default(),
last_check_at: None,
consecutive_failures: 0,
}
}
#[must_use]
pub fn with_trigger(mut self, action: TriggerAction) -> Self {
self.on_trigger = action;
self
}
#[must_use]
pub fn is_due(&self) -> bool {
matches!(self.status, WatchStatus::Active | WatchStatus::Snoozed)
&& Utc::now() >= self.next_due_at
}
pub fn record_check(&mut self) {
self.status = WatchStatus::Active;
self.last_check_at = Some(Utc::now());
self.next_due_at = Utc::now() + chrono::Duration::seconds(self.interval_secs as i64);
self.consecutive_failures = 0;
}
pub fn record_failure(&mut self) {
self.status = WatchStatus::Active;
self.last_check_at = Some(Utc::now());
self.consecutive_failures += 1;
let backoff_secs = match self.consecutive_failures {
1 => 60,
2 => 120,
_ => 300,
};
self.next_due_at = Utc::now() + chrono::Duration::seconds(backoff_secs);
}
pub fn snooze(&mut self, duration_secs: u64) {
self.status = WatchStatus::Snoozed;
self.next_due_at = Utc::now() + chrono::Duration::seconds(duration_secs as i64);
}
pub fn complete(&mut self) {
self.status = WatchStatus::Done;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MissionControlMessage {
pub id: String,
pub mission_id: MissionId,
pub sender: String,
pub body: String,
pub created_at: DateTime<Utc>,
#[serde(default)]
pub processed_at: Option<DateTime<Utc>>,
}
impl MissionControlMessage {
#[must_use]
pub fn new(mission_id: MissionId, sender: impl Into<String>, body: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4().to_string(),
mission_id,
sender: sender.into(),
body: body.into(),
created_at: Utc::now(),
processed_at: None,
}
}
pub fn mark_processed(&mut self) {
self.processed_at = Some(Utc::now());
}
#[must_use]
pub fn is_pending(&self) -> bool {
self.processed_at.is_none()
}
}