use serde::{Deserialize, Serialize};
use crate::review::JobOutcome;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentState {
Starting,
Working,
AwaitingInput,
Idle,
Queued,
Completed,
Failed,
Declined,
Timeout,
Cancelled,
Interrupted,
#[serde(other)]
Unknown,
}
impl AgentState {
pub fn as_str(self) -> &'static str {
match self {
Self::Starting => "starting",
Self::Working => "working",
Self::AwaitingInput => "awaiting_input",
Self::Idle => "idle",
Self::Queued => "queued",
Self::Completed => "completed",
Self::Failed => "failed",
Self::Declined => "declined",
Self::Timeout => "timeout",
Self::Cancelled => "cancelled",
Self::Interrupted => "interrupted",
Self::Unknown => "unknown",
}
}
pub fn is_final(self) -> bool {
matches!(
self,
Self::Completed
| Self::Failed
| Self::Declined
| Self::Timeout
| Self::Cancelled
| Self::Interrupted
)
}
pub fn needs_attention(self) -> bool {
matches!(self, Self::AwaitingInput)
}
}
impl std::fmt::Display for AgentState {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JobOrigin {
#[default]
Model,
Owner,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorktreeRef {
pub path: String,
pub branch: String,
pub repo: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorktreeInfo {
#[serde(flatten)]
pub worktree: WorktreeRef,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dirty: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub jobs: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkbenchSession {
pub id: String,
pub label: String,
pub workspace: String,
pub live: bool,
pub state: AgentState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkbenchJob {
pub session: String,
pub job: String,
pub agent: String,
pub task: String,
pub workspace: String,
pub cwd: String,
pub state: AgentState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
pub started_by: JobOrigin,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conversation: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub run: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worktree: Option<WorktreeRef>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub reviewed: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub resumable: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resumed_as: Option<String>,
pub created_unix_seconds: u64,
pub updated_unix_seconds: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkbenchRun {
pub handle: String,
pub agent: String,
pub session: String,
pub transport: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conversation: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub job: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub jobs: Vec<String>,
pub cwd: String,
pub state: AgentState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
pub started_unix_seconds: u64,
pub last_activity_unix_seconds: u64,
pub live: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub steerable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkbenchApproval {
pub id: String,
pub session: String,
pub job: String,
pub name: String,
pub risk: String,
pub cwd: String,
pub summary: String,
pub since_unix_seconds: u64,
pub expires_unix_seconds: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RestorableSession {
pub id: String,
pub label: String,
pub workspace: String,
pub queued: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interrupted: Option<String>,
pub saved_unix_seconds: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkbenchSnapshot {
pub generated_unix_seconds: u64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sessions: Vec<WorkbenchSession>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub jobs: Vec<WorkbenchJob>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub runs: Vec<WorkbenchRun>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub approvals: Vec<WorkbenchApproval>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub restorable: Vec<RestorableSession>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RunEventKind {
RunStarted,
TurnStarted,
Text,
Tool,
ToolFailed,
Plan,
ApprovalRequested,
ApprovalResolved,
Steered,
TurnEnded,
RunEnded,
Note,
Truncated,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunEvent {
pub seq: u64,
pub ts_ms: u64,
pub kind: RunEventKind,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub job: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub risk: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub by: Option<String>,
}
pub const MAX_JOB_RESULT_BYTES: usize = 4 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JobResult {
pub session: String,
pub job: String,
pub agent: String,
pub state: AgentState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reply: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outcome: Option<JobOutcome>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finished_unix_seconds: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunLogPage {
pub handle: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub events: Vec<RunEvent>,
pub next: u64,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub ended: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub more: bool,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SendMode {
#[default]
Auto,
Queue,
Steer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AttachColor {
Index(u8),
Rgb([u8; 3]),
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct AttachSpan {
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fg: Option<AttachColor>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bg: Option<AttachColor>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub bold: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub italic: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub underline: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub inverse: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AttachRow {
pub row: u16,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub spans: Vec<AttachSpan>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AttachPage {
pub id: String,
pub session: String,
pub conversation: String,
pub agent: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub run: Option<String>,
pub generation: u64,
pub cols: u16,
pub rows: u16,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub lines: Vec<AttachRow>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub full: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<[u16; 2]>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub application_cursor: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub bracketed_paste: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub ended: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RestoredQueue {
pub from: String,
pub queued: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interrupted: Option<String>,
#[serde(default, skip_serializing_if = "zero")]
pub dropped: u32,
}
#[allow(
clippy::trivially_copy_pass_by_ref,
reason = "serde skip_serializing_if passes the field by reference"
)]
fn zero(value: &u32) -> bool {
*value == 0
}
#[cfg(test)]
mod tests;