Skip to main content

task_runs/
types.rs

1//! Core types for the TaskRun system.
2//!
3//! These mirror the data model in `.yah/docs/working/yah-task-runs.md`.
4//! Intentionally kept free of I/O — the store layer owns persistence.
5//!
6//! The types `TaskRunId`, `Level`, `EventSource`, `ChunkRef`, `Event`,
7//! `Diagnostic`, `Initiator`, and `RunStatus` live in
8//! `crates/yah/observation/` and are re-exported here for backward
9//! compatibility.
10
11use serde::{Deserialize, Serialize};
12use std::path::PathBuf;
13
14// Re-export the hoisted observation types so all existing callers continue to
15// work via `use task_runs::{TaskRunId, Event, ...}`.
16pub use observation::{
17    ChunkRef, Diagnostic, Event, EventScope, EventSource, ForgeId, Initiator, Level, RunStatus,
18    TaskRunId, RESERVED_FIELD_PATHS,
19};
20
21// ─── BeholderStatus ───────────────────────────────────────────────────────────
22
23/// Opaque string surfaced on `TaskRunMeta.beholder_status`.
24///
25/// Examples: `"attached:cargo@1.78"`, `"none:auto"`, `"declined:cargo
26/// reason=\"explicit --message-format=human\""`, `"unknown_format"`,
27/// `"attached:cargo@1.78 rewrite=\"--message-format=json-render-diagnostics\""`.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct BeholderStatus {
30    pub text: String,
31    /// Args added to argv by a `Rewriter` beholder. `None` for `Parser` mode or
32    /// when no beholder attached.
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub rewrite_added: Option<Vec<String>>,
35}
36
37impl BeholderStatus {
38    fn make(text: String) -> Self {
39        Self { text, rewrite_added: None }
40    }
41
42    pub fn none_auto() -> Self {
43        Self::make("none:auto".to_string())
44    }
45    /// Bytes-only because the caller explicitly set `BeholderSelect::None`.
46    pub fn none_explicit() -> Self {
47        Self::make("none:explicit".to_string())
48    }
49    pub fn attached(name: &str, version: &str) -> Self {
50        Self::make(format!("attached:{name}@{version}"))
51    }
52    pub fn declined(name: &str, reason: &str) -> Self {
53        Self::make(format!("declined:{name} reason=\"{reason}\""))
54    }
55    /// `BeholderSelect::Force` matched; `matches` predicate agreed.
56    pub fn forced(name: &str, version: &str) -> Self {
57        Self::make(format!("forced:{name}@{version}"))
58    }
59    /// `BeholderSelect::Force` matched; beholder's `matches` would have declined.
60    pub fn forced_against_flags(name: &str, version: &str) -> Self {
61        Self::make(format!("forced-against-flags:{name}@{version}"))
62    }
63    /// `BeholderSelect::Force` matched on a TTY-attached run where a Rewriter
64    /// beholder would normally decline to preserve human output.
65    pub fn forced_against_tty(name: &str, version: &str) -> Self {
66        Self::make(format!("forced-against-tty:{name}@{version}"))
67    }
68    pub fn unknown_format() -> Self {
69        Self::make("unknown_format".to_string())
70    }
71    /// Beholder detected that the tool's output format is unrecognized (schema
72    /// drift). Records which beholder made the call and why.
73    pub fn unknown_format_with_reason(name: &str, reason: &str) -> Self {
74        Self::make(format!("unknown_format:{name} reason=\"{reason}\""))
75    }
76
77    /// Append rewrite info to this status if `added` is non-empty.
78    ///
79    /// Called after a `Rewriter` beholder adjusts argv so agents can see exactly
80    /// what was injected into the command line.
81    pub fn with_rewrite(mut self, added: Vec<String>) -> Self {
82        if !added.is_empty() {
83            let repr = added.join(" ");
84            self.text = format!("{} rewrite=\"{repr}\"", self.text);
85            self.rewrite_added = Some(added);
86        }
87        self
88    }
89}
90
91// ─── TaskRunMeta ──────────────────────────────────────────────────────────────
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct TaskRunMeta {
95    pub id: TaskRunId,
96    pub command: String,
97    pub cwd: PathBuf,
98    pub env: Vec<(String, String)>,
99    pub started_at: u64,
100    pub status: RunStatus,
101    pub label: Option<String>,
102    pub initiator: Initiator,
103    pub beholder_status: Option<BeholderStatus>,
104    /// If true, the GC sweep will not drop this run's output during warm rolloff.
105    /// Pinned runs are exempt until explicitly unpinned or archived.
106    #[serde(default)]
107    pub pinned: bool,
108    /// What surface spawned this run. `None` (the default) is an ordinary
109    /// `task.run` job; `Some("terminal")` marks an interactive terminal
110    /// session (SSH / local PTY / camp shell). A generic provenance tag, not
111    /// a UI concept — it lets a consumer (e.g. the desktop terminal rail) list
112    /// just its own runs from `task.list` without scooping up unrelated jobs.
113    #[serde(default)]
114    pub origin: Option<String>,
115}
116
117// ─── Stream ───────────────────────────────────────────────────────────────────
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "lowercase")]
121pub enum Stream {
122    Stdout,
123    Stderr,
124    Synth,
125}
126
127impl Stream {
128    pub fn as_str(self) -> &'static str {
129        match self {
130            Stream::Stdout => "stdout",
131            Stream::Stderr => "stderr",
132            Stream::Synth => "synth",
133        }
134    }
135}
136
137impl std::str::FromStr for Stream {
138    type Err = String;
139    fn from_str(s: &str) -> Result<Self, Self::Err> {
140        match s {
141            "stdout" => Ok(Stream::Stdout),
142            "stderr" => Ok(Stream::Stderr),
143            "synth" => Ok(Stream::Synth),
144            other => Err(format!("unknown stream: {other}")),
145        }
146    }
147}
148
149// ─── OutputChunk ──────────────────────────────────────────────────────────────
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct OutputChunk {
153    pub run_id: TaskRunId,
154    pub seq: u32,
155    pub offset_ms: u32,
156    pub stream: Stream,
157    pub bytes: Vec<u8>,
158}
159
160// ─── SeqRange ─────────────────────────────────────────────────────────────────
161
162/// Inclusive range over chunk `seq` numbers within a single run.
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct SeqRange {
165    pub lo: u32,
166    pub hi: u32,
167}
168
169// ─── Triage (Tier 1.75 — pruner output) ───────────────────────────────────────
170
171/// Pruner output for a run: a list of verbatim chunk ranges + a human-facing
172/// synopsis.
173///
174/// **Agents must read `keep`/`primary` ranges and resolve them to bytes via
175/// `task.lines`. The `synopsis` is for human display only — it can paraphrase
176/// or hallucinate. Never parse it programmatically.**
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct Triage {
179    pub run_id: TaskRunId,
180    pub synopsis: String,
181    pub keep: Vec<KeepRange>,
182    pub primary: SeqRange,
183    pub model: String,
184    pub prompt_version: u32,
185    pub cached_at: u64,
186    pub partial: bool,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct KeepRange {
191    pub range: SeqRange,
192    pub reason: String,
193}