1use crate::{
4 config::{ArtifactKind, Profile, ProfileKind},
5 graph_structure::{self, Structure},
6 monitor::model::{self, Card, Status, Tone},
7 project::{
8 ArtifactCondition, EvalCondition, StatusView, VerifyAction, VerifyOptions,
9 selection::Selection,
10 },
11 store::{Execution, ExecutionResult},
12 types::{ArtifactName, EvalId, Fingerprint},
13};
14use std::{
15 collections::{BTreeMap, BTreeSet},
16 path::{Path, PathBuf},
17};
18use time::OffsetDateTime;
19use tokio_util::sync::CancellationToken;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub enum Plan {
25 Run,
27 Blocked,
29 Wait,
31 Reuse,
34}
35
36impl Plan {
37 pub fn word(self) -> &'static str {
39 match self {
40 Self::Run => "would run",
41 Self::Blocked => "blocked",
42 Self::Wait => "waiting",
43 Self::Reuse => "reused",
44 }
45 }
46
47 pub fn glyph(self) -> &'static str {
50 match self {
51 Self::Run => "○",
52 Self::Blocked => Status::Blocked.glyph(),
53 Self::Wait => Status::Wait.glyph(),
54 Self::Reuse => "↻",
55 }
56 }
57
58 pub fn tone(self) -> Tone {
59 match self {
60 Self::Run => Tone::Running,
61 Self::Blocked => Tone::Blocked,
62 Self::Wait => Tone::Queued,
63 Self::Reuse => Tone::Green,
64 }
65 }
66}
67
68impl CurrentArtifact {
69 pub fn text(&self) -> String {
71 match (self.plan, self.runs) {
72 (Plan::Run, 1) => "would run (1 eval)".into(),
73 (Plan::Run, runs) => format!("would run ({runs} evals)"),
74 (plan, _) => plan.word().into(),
75 }
76 }
77}
78
79impl From<VerifyAction> for Plan {
80 fn from(action: VerifyAction) -> Self {
81 match action {
82 VerifyAction::Execute => Self::Run,
83 VerifyAction::Blocked => Self::Blocked,
84 VerifyAction::Wait => Self::Wait,
85 VerifyAction::Reuse => Self::Reuse,
86 }
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct CurrentEval {
93 pub id: EvalId,
94 pub name: String,
96 pub title: String,
97 pub kind: ProfileKind,
98 pub plan: Plan,
99 pub state: EvalCondition,
100 pub reason: String,
102 pub card: Card,
104 pub facts: Vec<(&'static str, String)>,
106}
107
108#[derive(Debug, Clone)]
110pub struct CurrentArtifact {
111 pub name: ArtifactName,
112 pub kind: ArtifactKind,
113 pub plan: Plan,
114 pub runs: usize,
116 pub path: PathBuf,
117 pub state: ArtifactCondition,
118 pub reason: String,
119 pub evals: Vec<CurrentEval>,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct CurrentEdge {
125 pub from: ArtifactName,
126 pub to: ArtifactName,
127 pub plan: Plan,
128}
129
130#[derive(Debug, Clone)]
132pub struct Current {
133 pub scanned_at: OffsetDateTime,
134 pub fingerprint: Fingerprint,
135 pub structure: String,
137 pub artifacts: Vec<CurrentArtifact>,
139 pub edges: Vec<CurrentEdge>,
140}
141
142pub async fn scan(
146 repo: &Path,
147 state: &Path,
148 cancellation: CancellationToken,
149) -> Result<Current, String> {
150 let structure = graph_structure::read(repo)?;
151 let (status, reused) = crate::project::scan(
152 repo,
153 Some(state),
154 &Selection::All,
155 &VerifyOptions::default(),
156 cancellation,
157 )
158 .await?;
159 Ok(project(
160 &structure,
161 &status,
162 &reused,
163 OffsetDateTime::now_utc(),
164 ))
165}
166
167fn profile(profile: &Profile) -> String {
169 match profile {
170 Profile::Agent { backend, model, .. } => format!("agent · {} · {model}", backend.as_str()),
171 other => other.kind().name().to_owned(),
172 }
173}
174
175fn card(
176 instruction: &str,
177 profile: &Profile,
178 plan: Plan,
179 reused: Option<&Execution>,
180 now: OffsetDateTime,
181) -> Card {
182 let result = reused.and_then(Execution::result);
183 Card {
184 instruction: instruction.to_owned(),
185 result: result.map(model::entries),
186 exit_code: match result {
187 Some(ExecutionResult::Runtime(result)) => result.exit_code.value().copied(),
188 _ => None,
189 },
190 error: None,
191 reused: plan == Plan::Reuse,
192 took: reused
193 .and_then(|execution| model::span(execution.started_at, execution.completed_at(), now)),
194 tokens: reused
195 .and_then(|execution| execution.usage.as_ref())
196 .map(|attempts| {
197 attempts
198 .iter()
199 .filter_map(|attempt| attempt.usage.get("totalTokens"))
200 .sum::<u64>()
201 })
202 .filter(|tokens| *tokens > 0),
203 model: match profile {
204 Profile::Agent { model, .. } => Some(model.to_string()),
205 _ => None,
206 },
207 }
208}
209
210pub(crate) fn project(
212 structure: &Structure,
213 status: &StatusView,
214 reused: &BTreeMap<EvalId, Execution>,
215 now: OffsetDateTime,
216) -> Current {
217 let artifacts: Vec<CurrentArtifact> = status
218 .artifacts
219 .iter()
220 .map(|artifact| {
221 let evals: Vec<CurrentEval> =
222 status
223 .evals
224 .iter()
225 .filter(|eval| eval.target == artifact.id)
226 .map(|eval| {
227 let plan = Plan::from(eval.action);
228 let declared = structure.evals.get(&eval.id);
229 let instruction = declared
230 .and_then(|declared| declared.payload.as_ref())
231 .map_or("", |payload| payload.instruction.as_str());
232 let mut facts = vec![("Profile", profile(&eval.profile))];
233 facts.extend(
234 eval.fingerprint
235 .as_ref()
236 .map(|fingerprint| ("Fingerprint", fingerprint.to_string())),
237 );
238 facts.push(("Eval", eval.id.to_string()));
239 facts.push(("Verify would", eval.action.to_string()));
240 facts.push(("State", eval.state.to_string()));
241 facts.extend(eval.key.as_ref().map(|key| ("Reuse key", key.to_string())));
242 facts.push(("Definition hash", eval.eval_def_hash.to_string()));
243 facts.extend(eval.last.as_ref().map(|last| {
244 ("Last Run", format!("{} · {}", last.run_id, last.verdict))
245 }));
246 if let Some(changes) = &eval.changes {
247 facts.push(("Changes", changes.summary.clone()));
248 facts.extend(
249 changes
250 .files
251 .as_ref()
252 .map(|files| ("Changed files", files.join("\n"))),
253 );
254 facts.extend(
255 changes
256 .dependencies
257 .as_ref()
258 .map(|names| ("Changed dependencies", names.join("\n"))),
259 );
260 }
261 CurrentEval {
262 id: eval.id.clone(),
263 name: crate::monitor::model::tree::local(&eval.id).to_owned(),
264 title: eval.title.clone(),
265 kind: eval.profile.kind(),
266 plan,
267 state: eval.state,
268 reason: eval.reason.clone(),
269 card: card(instruction, &eval.profile, plan, reused.get(&eval.id), now),
270 facts,
271 }
272 })
273 .collect();
274 CurrentArtifact {
275 name: artifact.id.clone(),
276 kind: artifact.kind,
277 plan: evals
278 .iter()
279 .map(|eval| eval.plan)
280 .min()
281 .unwrap_or(Plan::Reuse),
282 runs: evals.iter().filter(|eval| eval.plan == Plan::Run).count(),
283 path: artifact.path.clone(),
284 state: artifact.state,
285 reason: artifact.reason.clone(),
286 evals,
287 }
288 })
289 .collect();
290 let plans: BTreeMap<&ArtifactName, Plan> = artifacts
291 .iter()
292 .map(|artifact| (&artifact.name, artifact.plan))
293 .collect();
294 let pairs: BTreeSet<(&ArtifactName, &ArtifactName)> = structure
295 .relations
296 .iter()
297 .filter(|relation| relation.source != relation.target)
298 .map(|relation| (&relation.source, &relation.target))
299 .collect();
300 let edges = pairs
301 .into_iter()
302 .filter_map(|(from, to)| {
303 Some(CurrentEdge {
304 plan: *plans.get(from)?,
305 from: from.clone(),
306 to: plans.contains_key(to).then(|| to.clone())?,
307 })
308 })
309 .collect();
310 Current {
311 scanned_at: now,
312 fingerprint: structure.fingerprint(),
313 structure: structure.tree(),
314 artifacts,
315 edges,
316 }
317}