Skip to main content

twine_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    /// Twine 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    pub kind: WorkflowKind,
68    /// The harness filling a single-agent workflow's role.
69    pub harness: Option<HarnessId>,
70    /// The workflow's own shell, or a single agent's harness. Zero when it couldn't restart, and for
71    /// agents workflows, whose agents have the terminals instead.
72    pub terminal_id: TerminalId,
73    /// In role order. Empty unless the workflow's kind is [`WorkflowKind::Agents`].
74    pub agents: Vec<Agent>,
75    pub status: WorkflowStatus,
76    pub started_at: u64,
77    pub ended_at: Option<u64>,
78    /// Restored from storage, with fresh shells or archived output for stopped harnesses.
79    pub restored: bool,
80    /// Earlier terminal invocations, in creation order, retained independently of Activity spans.
81    pub terminal_history: Vec<WorkflowTerminal>,
82    /// Present for a workflow executing a pinned type.
83    pub run: Option<Box<crate::WorkflowRun>>,
84}
85
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct WorkflowTerminal {
88    pub terminal_id: TerminalId,
89    pub agent_id: Option<AgentId>,
90}
91
92/// One process filling one role in a workflow.
93#[derive(Clone, Debug, Eq, PartialEq)]
94pub struct Agent {
95    pub agent_id: AgentId,
96    pub role: String,
97    /// Zero when the agent's shell couldn't restart.
98    pub terminal_id: TerminalId,
99}
100
101impl Workflow {
102    /// Every process the workflow owns: its agents' in an agents workflow, and otherwise its own
103    /// shell or harness. One that couldn't restart is zero.
104    pub(crate) fn shells(&self) -> Vec<TerminalId> {
105        match self.kind {
106            WorkflowKind::Draft | WorkflowKind::Terminal | WorkflowKind::SingleAgent => {
107                vec![self.terminal_id]
108            }
109            WorkflowKind::Agents => self.agents.iter().map(|agent| agent.terminal_id).collect(),
110        }
111    }
112
113    /// The workflow's live shells.
114    pub(crate) fn terminal_ids(&self) -> Vec<TerminalId> {
115        self.shells()
116            .into_iter()
117            .filter(|terminal_id| terminal_id.value() != 0)
118            .filter(|id| {
119                !self.restored
120                    || !(self.kind == WorkflowKind::SingleAgent || self.run.is_some())
121                    || !self
122                        .terminal_history
123                        .iter()
124                        .any(|entry| entry.terminal_id == *id)
125            })
126            .collect()
127    }
128}
129
130#[derive(Clone, Debug, Default, Eq, PartialEq)]
131pub struct WorkflowState {
132    /// Remembers that this folder has had sessions, even after the user deletes the last one.
133    pub sessions_initialized: bool,
134    /// The selected session. Selection is remembered per folder.
135    pub session: Option<Session>,
136    pub sessions: Vec<Session>,
137    /// All workflows in the open folder, including sessions that aren't selected.
138    pub workflows: Vec<Workflow>,
139}
140
141/// Whether a session name or agent role is 1–200 characters on one line.
142pub(crate) fn valid_name(name: &str) -> bool {
143    !name.is_empty() && name.chars().count() <= 200 && !name.chars().any(char::is_control)
144}
145
146pub(crate) fn timestamp() -> u64 {
147    u64::try_from(
148        SystemTime::now()
149            .duration_since(UNIX_EPOCH)
150            .unwrap_or_default()
151            .as_millis(),
152    )
153    .unwrap_or(u64::MAX)
154}