| 1 | //! Runs and recorded sessions. Kept by the work service. |
| 2 | //! |
| 3 | //! Every sandbox g1t starts for checks, the merge queue or a merge check is |
| 4 | //! an [`AgentRun`]: what it is doing, on which pull request, step by step, |
| 5 | //! and how it ended. Runs of the agents g1t once ran itself stay readable. |
| 6 | //! |
| 7 | //! A session is what a coding agent recorded on a pull request |
| 8 | //! (`append_session`, `read_session`), summed up and listed here. |
| 9 | //! |
| 10 | //! Each `*Args` struct is the argument of the method of the same name, |
| 11 | //! served at `POST /rpc/<method>`. |
| 12 | |
| 13 | use serde::{Deserialize, Serialize}; |
| 14 | |
| 15 | use crate::repos::RepoPath; |
| 16 | use crate::work::{Pull, PullStatus, SessionEntry}; |
| 17 | use crate::{User, Viewer}; |
| 18 | |
| 19 | /// The work a run does. Only `Checks`, `Queue` and `Mergecheck` are |
| 20 | /// started now; the others are runs of the agents g1t once ran itself, |
| 21 | /// kept so the runs and run credentials already stored still parse. None |
| 22 | /// of them is started or granted any more. |
| 23 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 24 | #[serde(rename_all = "snake_case")] |
| 25 | pub enum RunKind { |
| 26 | /// Making the change an issue asks for. |
| 27 | Implement, |
| 28 | /// Addressing failed checks or a review on its own pull request. |
| 29 | Revise, |
| 30 | /// Reviewing someone's pull request. |
| 31 | Review, |
| 32 | /// Answering another agent's question or handoff. |
| 33 | Answer, |
| 34 | /// Merging in the branch a pull request will land on. |
| 35 | Update, |
| 36 | /// Turning an outcome into a plan of issues. |
| 37 | Plan, |
| 38 | /// Running an issue's acceptance checks. Not an agent: a sandbox. |
| 39 | Checks, |
| 40 | /// Building and checking a state of the merge queue. Not an agent. |
| 41 | Queue, |
| 42 | /// Finding out whether a pull request merges cleanly. Not an agent. |
| 43 | Mergecheck, |
| 44 | } |
| 45 | |
| 46 | impl RunKind { |
| 47 | pub const ALL: [RunKind; 9] = [ |
| 48 | RunKind::Implement, |
| 49 | RunKind::Revise, |
| 50 | RunKind::Review, |
| 51 | RunKind::Answer, |
| 52 | RunKind::Update, |
| 53 | RunKind::Plan, |
| 54 | RunKind::Checks, |
| 55 | RunKind::Queue, |
| 56 | RunKind::Mergecheck, |
| 57 | ]; |
| 58 | |
| 59 | pub fn as_str(self) -> &'static str { |
| 60 | match self { |
| 61 | RunKind::Implement => "implement", |
| 62 | RunKind::Revise => "revise", |
| 63 | RunKind::Review => "review", |
| 64 | RunKind::Answer => "answer", |
| 65 | RunKind::Update => "update", |
| 66 | RunKind::Plan => "plan", |
| 67 | RunKind::Checks => "checks", |
| 68 | RunKind::Queue => "queue", |
| 69 | RunKind::Mergecheck => "mergecheck", |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | pub fn parse(value: &str) -> Option<RunKind> { |
| 74 | RunKind::ALL.into_iter().find(|kind| kind.as_str() == value) |
| 75 | } |
| 76 | |
| 77 | } |
| 78 | |
| 79 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 80 | #[serde(rename_all = "snake_case")] |
| 81 | pub enum RunStatus { |
| 82 | /// Its sandbox is starting. |
| 83 | Queued, |
| 84 | Running, |
| 85 | Succeeded, |
| 86 | Failed, |
| 87 | /// A person stopped it. |
| 88 | Stopped, |
| 89 | } |
| 90 | |
| 91 | impl RunStatus { |
| 92 | pub fn as_str(self) -> &'static str { |
| 93 | match self { |
| 94 | RunStatus::Queued => "queued", |
| 95 | RunStatus::Running => "running", |
| 96 | RunStatus::Succeeded => "succeeded", |
| 97 | RunStatus::Failed => "failed", |
| 98 | RunStatus::Stopped => "stopped", |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | pub fn parse(value: &str) -> Option<RunStatus> { |
| 103 | [ |
| 104 | RunStatus::Queued, |
| 105 | RunStatus::Running, |
| 106 | RunStatus::Succeeded, |
| 107 | RunStatus::Failed, |
| 108 | RunStatus::Stopped, |
| 109 | ] |
| 110 | .into_iter() |
| 111 | .find(|status| status.as_str() == value) |
| 112 | } |
| 113 | |
| 114 | pub fn is_active(self) -> bool { |
| 115 | matches!(self, RunStatus::Queued | RunStatus::Running) |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | /// One step of a run, as one short line. |
| 120 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 121 | pub struct RunStep { |
| 122 | /// RFC 3339. |
| 123 | pub at: String, |
| 124 | pub text: String, |
| 125 | } |
| 126 | |
| 127 | /// One sandbox g1t started: a run of checks, a merge queue build or a |
| 128 | /// merge check (or, on older rows, one of its former agents at work). |
| 129 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 130 | #[serde(rename_all = "camelCase")] |
| 131 | pub struct AgentRun { |
| 132 | pub id: String, |
| 133 | pub repo: RepoPath, |
| 134 | /// The pull request it works on; for a plan, none. |
| 135 | pub number: Option<u32>, |
| 136 | /// That pull request's title, or the plan's brief. |
| 137 | pub title: Option<String>, |
| 138 | pub kind: RunKind, |
| 139 | /// Who is working: `g1t` for g1t's own agent and for a sandbox that |
| 140 | /// runs commands. |
| 141 | pub agent: String, |
| 142 | /// The model, by its public name. Members only. |
| 143 | pub model: Option<String>, |
| 144 | pub status: RunStatus, |
| 145 | /// What it is doing now, in one line. |
| 146 | pub step: Option<String>, |
| 147 | /// Every step so far, oldest first. Empty in lists. |
| 148 | #[serde(default)] |
| 149 | pub steps: Vec<RunStep>, |
| 150 | /// How many steps there are. |
| 151 | #[serde(default)] |
| 152 | pub step_count: u32, |
| 153 | /// Who put it to work, when a person did. |
| 154 | pub started_by: Option<String>, |
| 155 | /// Why it failed. |
| 156 | pub error: Option<String>, |
| 157 | /// What it cost, in US dollars, as the harness reported it. Members only. |
| 158 | pub cost_usd: Option<f64>, |
| 159 | pub turns: Option<u32>, |
| 160 | /// The most it may cost, in US dollars, from its guardrails. None: no |
| 161 | /// cap. Members only. |
| 162 | #[serde(default)] |
| 163 | pub budget_usd: Option<f64>, |
| 164 | /// The longest it may take, in minutes, from its guardrails. |
| 165 | #[serde(default)] |
| 166 | pub time_cap_minutes: Option<u32>, |
| 167 | /// `budget` or `time` when g1t stopped it for reaching a cap. |
| 168 | #[serde(default)] |
| 169 | pub halted: Option<String>, |
| 170 | /// RFC 3339. |
| 171 | pub created_at: String, |
| 172 | pub started_at: Option<String>, |
| 173 | pub finished_at: Option<String>, |
| 174 | pub updated_at: String, |
| 175 | } |
| 176 | |
| 177 | /// What lets a sandbox, and nothing else, report on its run. |
| 178 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 179 | #[serde(rename_all = "camelCase")] |
| 180 | pub struct AgentRunTicket { |
| 181 | pub run_id: String, |
| 182 | pub token: String, |
| 183 | } |
| 184 | |
| 185 | /// `open_run`: records a sandbox the runner is starting. Called by the |
| 186 | /// runner service only. Returns `Outcome<AgentRunTicket>`. |
| 187 | #[derive(Debug, Serialize, Deserialize)] |
| 188 | #[serde(rename_all = "camelCase")] |
| 189 | pub struct OpenRunArgs { |
| 190 | /// Who the sandbox acts as. |
| 191 | pub actor: User, |
| 192 | pub repo: RepoPath, |
| 193 | #[serde(default)] |
| 194 | pub number: Option<u32>, |
| 195 | #[serde(default)] |
| 196 | pub pull_id: Option<String>, |
| 197 | /// For a run with no pull request, such as a plan: what it is about. |
| 198 | #[serde(default)] |
| 199 | pub title: Option<String>, |
| 200 | pub kind: RunKind, |
| 201 | #[serde(default)] |
| 202 | pub agent: Option<String>, |
| 203 | #[serde(default)] |
| 204 | pub model: Option<String>, |
| 205 | /// The sandbox's name, which stopping it needs. |
| 206 | pub sandbox: String, |
| 207 | #[serde(default)] |
| 208 | pub started_by: Option<String>, |
| 209 | /// Its cost cap, from its guardrails, in US dollars. |
| 210 | #[serde(default)] |
| 211 | pub budget_usd: Option<f64>, |
| 212 | /// Its time cap, from its guardrails, in minutes. |
| 213 | #[serde(default)] |
| 214 | pub time_cap_minutes: Option<u32>, |
| 215 | } |
| 216 | |
| 217 | /// `report_run`: a sandbox telling how its run goes, with the run's token. |
| 218 | /// Steps are added, the current step replaced, and an outcome ends the run. |
| 219 | /// A finished run accepts nothing more. Returns `Outcome<RunStatus>`: the |
| 220 | /// run's status after the report, so a sandbox can tell it was stopped. |
| 221 | #[derive(Debug, Default, Serialize, Deserialize)] |
| 222 | #[serde(rename_all = "camelCase", default)] |
| 223 | pub struct ReportRunArgs { |
| 224 | pub run_id: String, |
| 225 | pub token: String, |
| 226 | pub steps: Vec<String>, |
| 227 | pub step: Option<String>, |
| 228 | pub cost_usd: Option<f64>, |
| 229 | pub turns: Option<u32>, |
| 230 | /// `succeeded` or `failed`, to end the run. |
| 231 | pub outcome: Option<RunStatus>, |
| 232 | pub error: Option<String>, |
| 233 | /// `budget` or `time`: the run reached a cap of its guardrails and |
| 234 | /// stopped. Ends it as stopped, like a person's stop. |
| 235 | pub halt: Option<crate::guardrails::Halt>, |
| 236 | } |
| 237 | |
| 238 | /// `list_runs`: runs in a repository, or with `workspace` instead, in every |
| 239 | /// repository of a workspace (members only), newest first. Returns |
| 240 | /// `Outcome<Vec<AgentRun>>`. |
| 241 | #[derive(Debug, Default, Serialize, Deserialize)] |
| 242 | #[serde(rename_all = "camelCase", default)] |
| 243 | pub struct ListRunsArgs { |
| 244 | pub viewer: Viewer, |
| 245 | pub repo: Option<RepoPath>, |
| 246 | pub workspace: Option<String>, |
| 247 | /// Only those queued or running. |
| 248 | pub active: bool, |
| 249 | pub kind: Option<RunKind>, |
| 250 | pub status: Option<RunStatus>, |
| 251 | /// Only those on this pull request. |
| 252 | pub number: Option<u32>, |
| 253 | /// At most 200; 50 if not given. |
| 254 | pub limit: Option<u32>, |
| 255 | } |
| 256 | |
| 257 | /// `get_run`: one run with all its steps. Returns `Outcome<AgentRun>`. |
| 258 | #[derive(Debug, Serialize, Deserialize)] |
| 259 | pub struct GetRunArgs { |
| 260 | pub viewer: Viewer, |
| 261 | pub repo: RepoPath, |
| 262 | pub id: String, |
| 263 | } |
| 264 | |
| 265 | /// A pull request's recorded session, summed up for a list. |
| 266 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 267 | #[serde(rename_all = "camelCase")] |
| 268 | pub struct SessionSummary { |
| 269 | pub number: u32, |
| 270 | pub title: String, |
| 271 | pub status: PullStatus, |
| 272 | /// The agent label on the pull request, such as `g1t`. |
| 273 | pub agent: String, |
| 274 | pub entries: u32, |
| 275 | /// How many tools it called. |
| 276 | pub tools: u32, |
| 277 | /// The start of the first prompt. |
| 278 | pub prompt: Option<String>, |
| 279 | /// The kinds of the runs g1t made for it. |
| 280 | pub kinds: Vec<RunKind>, |
| 281 | pub runs: u32, |
| 282 | /// What its runs cost together. Members only. |
| 283 | pub cost_usd: Option<f64>, |
| 284 | /// Whether one of its runs is under way. |
| 285 | pub active: bool, |
| 286 | /// RFC 3339. |
| 287 | pub started_at: String, |
| 288 | pub last_at: String, |
| 289 | } |
| 290 | |
| 291 | /// `list_sessions`: the pull requests of a repository that have a session, |
| 292 | /// most recently active first. Returns `Outcome<Vec<SessionSummary>>`. |
| 293 | #[derive(Debug, Default, Serialize, Deserialize)] |
| 294 | #[serde(rename_all = "camelCase", default)] |
| 295 | pub struct ListSessionsArgs { |
| 296 | pub viewer: Viewer, |
| 297 | pub repo: Option<RepoPath>, |
| 298 | /// Only those with a run of this kind. |
| 299 | pub kind: Option<RunKind>, |
| 300 | /// The pull request's status: `draft`, `open`, `merged` or `closed`. |
| 301 | pub outcome: Option<PullStatus>, |
| 302 | pub number: Option<u32>, |
| 303 | } |
| 304 | |
| 305 | /// `get_session`: one pull request's session in full, with its runs. |
| 306 | /// Returns `Outcome<SessionView>`. |
| 307 | #[derive(Debug, Serialize, Deserialize)] |
| 308 | pub struct GetSessionArgs { |
| 309 | pub viewer: Viewer, |
| 310 | pub repo: RepoPath, |
| 311 | pub number: u32, |
| 312 | } |
| 313 | |
| 314 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 315 | #[serde(rename_all = "camelCase")] |
| 316 | pub struct SessionView { |
| 317 | pub pull: Pull, |
| 318 | /// Oldest first; the first 2000. |
| 319 | pub entries: Vec<SessionEntry>, |
| 320 | /// Newest first, with their steps. |
| 321 | pub runs: Vec<AgentRun>, |
| 322 | /// Members only. |
| 323 | pub cost_usd: Option<f64>, |
| 324 | } |
| 325 | |
| 326 | #[cfg(test)] |
| 327 | mod tests { |
| 328 | use super::*; |
| 329 | |
| 330 | #[test] |
| 331 | fn kinds_and_statuses_round_trip() { |
| 332 | for kind in RunKind::ALL { |
| 333 | assert_eq!(RunKind::parse(kind.as_str()), Some(kind)); |
| 334 | } |
| 335 | assert_eq!(RunStatus::parse("stopped"), Some(RunStatus::Stopped)); |
| 336 | assert!(RunStatus::Queued.is_active() && !RunStatus::Failed.is_active()); |
| 337 | } |
| 338 | } |