Skip to main content

twine_core/
trace.rs

1//! Durable records of activity Twine performs or observes, separate from terminal bytes.
2
3use thiserror::Error;
4
5use crate::{TerminalId, WorkflowId};
6
7pub const MAX_TRACE_PAGE_SIZE: usize = 200;
8
9#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
10pub struct TraceLaneId(pub u64);
11
12#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
13pub struct TraceSpanId(pub u64);
14
15#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
16pub struct TraceEventId(pub u64);
17
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct TraceLane {
20    pub lane_id: TraceLaneId,
21    pub workflow_id: WorkflowId,
22    pub agent_id: Option<crate::AgentId>,
23    pub name: String,
24    pub is_agent: bool,
25    pub role: Option<String>,
26    pub harness: Option<String>,
27}
28
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub enum TraceSpanStatus {
31    Running,
32    Completed,
33    Exited,
34    Failed,
35    Stopped,
36}
37
38#[derive(Clone, Debug, Eq, PartialEq)]
39pub struct TraceSpan {
40    pub span_id: TraceSpanId,
41    pub lane_id: TraceLaneId,
42    pub title: String,
43    /// Unix milliseconds; ordering is also preserved by stable record IDs.
44    pub started_at: u64,
45    pub ended_at: Option<u64>,
46    pub status: TraceSpanStatus,
47    pub terminal_id: Option<TerminalId>,
48    /// True while this application owns the active assignment or corresponding running process.
49    /// A historical open span has an unrecorded ending, rather than an invented completion.
50    pub is_live: bool,
51}
52
53#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
54pub struct TraceAnchor {
55    pub terminal_id: TerminalId,
56    /// The exclusive boundary after output observed when the event happened.
57    pub byte_offset: u64,
58    /// Ordered resizes at this exact boundary when the event was observed. `None` means unavailable.
59    pub boundary_sizes: Option<Vec<crate::TerminalSize>>,
60}
61
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum TraceEventKind {
64    ProcessStarted,
65    ProcessExited,
66    ProcessFailed,
67    ProcessStopped,
68    WorkflowEvent,
69}
70
71#[derive(Clone, Debug, Eq, PartialEq)]
72pub struct TraceEvent {
73    pub event_id: TraceEventId,
74    pub workflow_id: WorkflowId,
75    pub span_id: Option<TraceSpanId>,
76    pub timestamp: u64,
77    pub kind: TraceEventKind,
78    pub message: String,
79    pub anchor: Option<TraceAnchor>,
80}
81
82#[derive(Clone, Debug, Eq, PartialEq)]
83pub struct TraceSummary {
84    pub workflow_id: WorkflowId,
85    /// Last durable event ID for this workflow. Independent of the transient event journal.
86    pub revision: u64,
87    pub span_count: u64,
88    pub agent_count: u64,
89}
90
91#[derive(Clone, Debug, Eq, PartialEq)]
92pub struct WorkflowTracePage {
93    pub summary: TraceSummary,
94    pub lanes: Vec<TraceLane>,
95    /// Most recent spans first. The cursor reads older spans.
96    pub spans: Vec<TraceSpan>,
97    pub next_before: Option<TraceSpanId>,
98}
99
100#[derive(Clone, Debug, Eq, PartialEq)]
101pub struct TraceEventsPage {
102    pub workflow_id: WorkflowId,
103    pub span_id: TraceSpanId,
104    pub revision: u64,
105    pub events: Vec<TraceEvent>,
106    pub next_after: Option<TraceEventId>,
107}
108
109/// Durable native tool or subagent identity within a prompt or assignment.
110#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
111pub struct TraceActivityId(pub u64);
112
113#[derive(Clone, Copy, Debug, Eq, PartialEq)]
114pub enum TraceActivityKind {
115    Tool,
116    Subagent,
117    Model,
118    Note,
119}
120
121#[derive(Clone, Copy, Debug, Eq, PartialEq)]
122pub enum TraceActivityStatus {
123    Running,
124    Completed,
125    Failed,
126    Interrupted,
127}
128
129#[derive(Clone, Debug, Eq, PartialEq)]
130pub struct TraceActivity {
131    pub activity_id: TraceActivityId,
132    pub span_id: TraceSpanId,
133    pub parent_activity_id: Option<TraceActivityId>,
134    pub kind: TraceActivityKind,
135    pub title: String,
136    /// Missing endpoints remain missing; observers do not invent elapsed time.
137    pub started_at: Option<u64>,
138    pub ended_at: Option<u64>,
139    pub status: TraceActivityStatus,
140    pub input: String,
141    pub output: String,
142    pub anchor: Option<TraceAnchor>,
143    pub metadata: serde_json::Value,
144    pub input_bytes: Option<u64>,
145    pub output_bytes: Option<u64>,
146    pub input_version: Option<String>,
147    pub output_version: Option<String>,
148}
149
150#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize)]
151#[serde(rename_all = "camelCase")]
152pub struct TraceActivityCounts {
153    pub tools: u64,
154    pub subagents: u64,
155    pub models: u64,
156    pub notes: u64,
157    pub failures: u64,
158}
159
160#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
161#[serde(rename_all = "camelCase")]
162pub struct TraceDetailPage {
163    pub activity_id: u64,
164    pub output: bool,
165    pub offset: u64,
166    pub next_offset: Option<u64>,
167    pub total_bytes: u64,
168    pub text: String,
169    pub version: Option<String>,
170}
171
172#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
173#[serde(rename_all = "camelCase")]
174pub struct TraceStorageStatus {
175    pub payload_bytes: u64,
176    pub payload_files: u64,
177    pub budget_bytes: u64,
178    pub retention_days: u32,
179    pub updated_at: u64,
180    pub pinned: bool,
181    pub clear_generation: u64,
182    pub completed_clear_generation: u64,
183}
184
185#[derive(Clone, Debug, Eq, PartialEq)]
186pub struct TraceActivitiesPage {
187    pub workflow_id: WorkflowId,
188    pub span_id: TraceSpanId,
189    pub revision: u64,
190    pub activities: Vec<TraceActivity>,
191    pub next_after: Option<TraceActivityId>,
192    pub counts: TraceActivityCounts,
193}
194
195#[derive(Debug, Error)]
196pub enum TraceError {
197    #[error("trace page size must be between 1 and {MAX_TRACE_PAGE_SIZE}")]
198    InvalidLimit,
199    #[error("the workflow does not exist")]
200    WorkflowNotFound,
201    #[error("the trace span does not exist")]
202    SpanNotFound,
203    #[error(transparent)]
204    Store(#[from] crate::StoreError),
205}
206
207pub(crate) fn validate_limit(limit: usize) -> Result<(), TraceError> {
208    if !(1..=MAX_TRACE_PAGE_SIZE).contains(&limit) {
209        return Err(TraceError::InvalidLimit);
210    }
211    Ok(())
212}