Skip to main content

handshake_core/
workflow.rs

1//! Sessions and live workflow state for the open folder.
2
3use std::path::PathBuf;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use crate::harness::HarnessId;
7use crate::terminal::TerminalId;
8
9#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
10pub struct SessionId(pub u64);
11
12#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
13pub struct WorkflowId(pub u64);
14
15#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
16pub struct AgentId(pub u64);
17
18/// The most agents one workflow can run. They all run at once, so this is the workflow types' limit
19/// on agents running in parallel.
20pub(crate) const MAX_AGENTS: usize = crate::workflow_type::MAX_PARALLEL_AGENTS as usize;
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum WorkflowKind {
24    Draft,
25    Terminal,
26    SingleAgent,
27    /// One agent per role, each with its own terminal. Until harnesses can launch, every agent runs
28    /// the default shell.
29    Agents,
30}
31
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub enum WorkflowStatus {
34    Running,
35    Completed,
36    Exited,
37    Failed,
38    /// The user stopped the agent.
39    Cancelled,
40    /// Handshake quit or crashed while the work was running; it did not finish.
41    Interrupted,
42    Closed,
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub enum SessionStatus {
47    Active,
48    Closed,
49}
50
51#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct Session {
53    pub session_id: SessionId,
54    pub name: String,
55    pub folder: PathBuf,
56    pub status: SessionStatus,
57    /// Unix time in milliseconds.
58    pub started_at: u64,
59    pub ended_at: Option<u64>,
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub struct Workflow {
64    pub workflow_id: WorkflowId,
65    pub session_id: SessionId,
66    pub name: String,
67    /// A user title, independent of the automatic workflow name.
68    pub custom_name: Option<String>,
69    /// Reopened history never starts processes implicitly, including after relaunch.
70    pub archived: bool,
71    pub kind: WorkflowKind,
72    /// The harness filling a single-agent workflow's role.
73    pub harness: Option<HarnessId>,
74    /// The workflow's own shell, or a single agent's harness. Zero when it couldn't restart, and for
75    /// agents workflows, whose agents have the terminals instead.
76    pub terminal_id: TerminalId,
77    /// In role order. Empty unless the workflow's kind is [`WorkflowKind::Agents`].
78    pub agents: Vec<Agent>,
79    pub status: WorkflowStatus,
80    pub started_at: u64,
81    pub ended_at: Option<u64>,
82    /// Restored from storage, with fresh shells or archived output for stopped harnesses.
83    pub restored: bool,
84    /// Earlier terminal invocations, in creation order, retained independently of Activity spans.
85    pub terminal_history: Vec<WorkflowTerminal>,
86    /// Present for a workflow executing a pinned type.
87    pub run: Option<Box<crate::WorkflowRun>>,
88}
89
90#[derive(Clone, Debug, Eq, PartialEq)]
91pub struct WorkflowTerminal {
92    pub terminal_id: TerminalId,
93    pub agent_id: Option<AgentId>,
94}
95
96/// One process filling one role in a workflow.
97#[derive(Clone, Debug, Eq, PartialEq)]
98pub struct Agent {
99    pub agent_id: AgentId,
100    pub role: String,
101    /// Zero when the agent's shell couldn't restart.
102    pub terminal_id: TerminalId,
103}
104
105impl Workflow {
106    /// Every process the workflow owns: its agents' in an agents workflow, and otherwise its own
107    /// shell or harness. One that couldn't restart is zero.
108    pub(crate) fn shells(&self) -> Vec<TerminalId> {
109        match self.kind {
110            WorkflowKind::Draft | WorkflowKind::Terminal | WorkflowKind::SingleAgent => {
111                vec![self.terminal_id]
112            }
113            WorkflowKind::Agents => self.agents.iter().map(|agent| agent.terminal_id).collect(),
114        }
115    }
116
117    /// The workflow's live shells.
118    pub(crate) fn terminal_ids(&self) -> Vec<TerminalId> {
119        if self.archived {
120            return Vec::new();
121        }
122        self.shells()
123            .into_iter()
124            .filter(|terminal_id| terminal_id.value() != 0)
125            .filter(|id| {
126                !self.restored
127                    || !(self.kind == WorkflowKind::SingleAgent || self.run.is_some())
128                    || !self
129                        .terminal_history
130                        .iter()
131                        .any(|entry| entry.terminal_id == *id)
132            })
133            .collect()
134    }
135}
136
137#[derive(Clone, Debug, Default, Eq, PartialEq)]
138pub struct WorkflowState {
139    /// Remembers that this folder has had sessions, even after the user deletes the last one.
140    pub sessions_initialized: bool,
141    /// The selected session. Selection is remembered per folder.
142    pub session: Option<Session>,
143    pub sessions: Vec<Session>,
144    /// All workflows in the open folder, including sessions that aren't selected.
145    pub workflows: Vec<Workflow>,
146}
147
148/// Whether a session name or agent role is 1–200 characters on one line.
149pub(crate) fn valid_name(name: &str) -> bool {
150    !name.is_empty() && name.chars().count() <= 200 && !name.chars().any(char::is_control)
151}
152
153pub(crate) fn timestamp() -> u64 {
154    u64::try_from(
155        SystemTime::now()
156            .duration_since(UNIX_EPOCH)
157            .unwrap_or_default()
158            .as_millis(),
159    )
160    .unwrap_or(u64::MAX)
161}