1use crate::{
4 config::{ArtifactKind, Profile, ProfileKind},
5 graph_structure::{self, Structure},
6 monitor::model::{self, Card, Status, Tone},
7 project::{
8 ArtifactCondition, EvalCondition, EvalState, StatusView, VerifyAction, VerifyOptions,
9 selection::Selection,
10 },
11 store::{Claim, Execution, ExecutionResult},
12 types::{ArtifactName, EvalId, Fingerprint},
13};
14use std::{
15 collections::{BTreeMap, BTreeSet},
16 path::{Path, PathBuf},
17 sync::Arc,
18};
19use time::OffsetDateTime;
20use tokio_util::sync::CancellationToken;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub enum Plan {
27 Run,
29 Check,
32 Blocked,
34 Wait,
36 Reuse,
39}
40
41impl Plan {
42 pub fn word(self) -> &'static str {
45 match self {
46 Self::Run => "would run",
47 Self::Check => "checking",
48 Self::Blocked => "blocked",
49 Self::Wait => "waiting",
50 Self::Reuse => "reused",
51 }
52 }
53
54 pub fn glyph(self) -> &'static str {
57 match self {
58 Self::Run => "○",
59 Self::Check => "…",
60 Self::Blocked => Status::Blocked.glyph(),
61 Self::Wait => Status::Wait.glyph(),
62 Self::Reuse => "↻",
63 }
64 }
65
66 pub fn tone(self) -> Tone {
67 match self {
68 Self::Run => Tone::Running,
69 Self::Check => Tone::Muted,
70 Self::Blocked => Tone::Blocked,
71 Self::Wait => Tone::Queued,
72 Self::Reuse => Tone::Green,
73 }
74 }
75}
76
77impl CurrentArtifact {
78 pub fn text(&self) -> String {
80 match (self.plan, self.runs) {
81 (Plan::Run, 1) => "would run (1 eval)".into(),
82 (plan, 0) => plan.word().into(),
84 (Plan::Run, runs) => format!("would run ({runs} evals)"),
85 (plan, _) => plan.word().into(),
86 }
87 }
88}
89
90impl From<VerifyAction> for Plan {
91 fn from(action: VerifyAction) -> Self {
92 match action {
93 VerifyAction::Execute => Self::Run,
94 VerifyAction::Blocked => Self::Blocked,
95 VerifyAction::Wait => Self::Wait,
96 VerifyAction::Reuse => Self::Reuse,
97 }
98 }
99}
100
101const CHECKING: &str = "Still being checked: a fingerprint its key needs, or the plan of an eval it waits for, is not known yet.";
103
104#[derive(Debug, Clone)]
106pub struct CurrentEval {
107 pub id: EvalId,
108 pub name: String,
110 pub title: String,
111 pub kind: ProfileKind,
112 pub plan: Plan,
113 pub state: EvalCondition,
114 pub reason: String,
116 pub card: Card,
118 pub facts: Vec<(&'static str, String)>,
120}
121
122#[derive(Debug, Clone)]
124pub struct CurrentArtifact {
125 pub name: ArtifactName,
126 pub kind: ArtifactKind,
127 pub plan: Plan,
128 pub runs: usize,
130 pub path: PathBuf,
131 pub state: ArtifactCondition,
132 pub reason: String,
133 pub evals: Vec<CurrentEval>,
134 pub parts: Vec<ArtifactName>,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct CurrentEdge {
141 pub from: ArtifactName,
142 pub to: ArtifactName,
143 pub plan: Plan,
144}
145
146#[derive(Debug, Clone)]
148pub struct Current {
149 pub scanned_at: OffsetDateTime,
150 pub fingerprint: Fingerprint,
151 pub structure: Arc<str>,
153 pub artifacts: Vec<CurrentArtifact>,
155 pub edges: Vec<CurrentEdge>,
156}
157
158pub async fn scan(
163 repo: &Path,
164 state: &Path,
165 cancellation: CancellationToken,
166 publish: impl FnMut(Current) + Send,
167) -> Result<Current, String> {
168 let current = read(repo, state, cancellation, publish).await;
169 crate::platform::release_free_memory();
172 current
173}
174
175async fn read(
176 repo: &Path,
177 state: &Path,
178 cancellation: CancellationToken,
179 mut publish: impl FnMut(Current) + Send,
180) -> Result<Current, String> {
181 let config = crate::config::read_workspace_config(repo).map_err(|error| error.to_string())?;
183 let structure = graph_structure::extract(&config);
184 let declared = Declared {
186 tree: structure.tree().into(),
187 fingerprint: structure.fingerprint(),
188 structure: &structure,
189 };
190 let (status, cached) = crate::project::scan(
191 config,
192 Some(state),
193 &Selection::All,
194 &VerifyOptions::default(),
195 cancellation,
196 Some(&mut |progress: crate::project::Progress<'_>| {
197 publish(project(
198 &declared,
199 &progress.view,
200 progress.cached,
201 &progress.checking,
202 OffsetDateTime::now_utc(),
203 ));
204 }),
205 )
206 .await?;
207 Ok(project(
208 &declared,
209 &status,
210 &cached,
211 &BTreeSet::new(),
212 OffsetDateTime::now_utc(),
213 ))
214}
215
216fn profile(profile: &Profile) -> String {
218 match profile {
219 Profile::Agent { backend, model, .. } => format!("agent · {} · {model}", backend.as_str()),
220 other => other.kind().name().to_owned(),
221 }
222}
223
224fn card(
225 instruction: &str,
226 profile: &Profile,
227 plan: Plan,
228 reused: Option<&Execution>,
229 now: OffsetDateTime,
230) -> Card {
231 let result = reused.and_then(Execution::result);
232 Card {
233 instruction: instruction.to_owned(),
234 result: result.map(model::entries),
235 exit_code: match result {
236 Some(ExecutionResult::Runtime(result)) => result.exit_code.value().copied(),
237 _ => None,
238 },
239 error: None,
240 reused: plan == Plan::Reuse,
241 took: reused
242 .and_then(|execution| model::span(execution.started_at, execution.completed_at(), now)),
243 tokens: reused
244 .and_then(|execution| execution.usage.as_deref())
245 .and_then(model::attempt_tokens),
246 model: match profile {
247 Profile::Agent { model, .. } => Some(model.to_string()),
248 _ => None,
249 },
250 }
251}
252
253struct Declared<'s> {
255 structure: &'s Structure,
256 tree: Arc<str>,
257 fingerprint: Fingerprint,
258}
259
260fn project(
263 declared: &Declared<'_>,
264 status: &StatusView,
265 cached: &BTreeMap<EvalId, Claim>,
266 checking: &BTreeSet<&EvalId>,
267 now: OffsetDateTime,
268) -> Current {
269 let mut by_target: BTreeMap<&ArtifactName, Vec<&EvalState>> = BTreeMap::new();
270 for eval in &status.evals {
271 by_target.entry(&eval.target).or_default().push(eval);
272 }
273 let mut parts: BTreeMap<&ArtifactName, Vec<ArtifactName>> = BTreeMap::new();
274 for relation in &declared.structure.relations {
275 if relation.kind == graph_structure::Edge::Part {
276 parts
277 .entry(&relation.target)
278 .or_default()
279 .push(relation.source.clone());
280 }
281 }
282 let mut artifacts: Vec<CurrentArtifact> = status
283 .artifacts
284 .iter()
285 .map(|artifact| {
286 let evals: Vec<CurrentEval> =
287 by_target
288 .get(&artifact.id)
289 .into_iter()
290 .flatten()
291 .map(|eval| {
292 let plan = if checking.contains(&eval.id) {
293 Plan::Check
294 } else {
295 Plan::from(eval.action)
296 };
297 let reused = match cached.get(&eval.id) {
298 Some(Claim::Reuse(execution)) => Some(&**execution),
299 _ => None,
300 };
301 let instruction = declared
302 .structure
303 .evals
304 .get(&eval.id)
305 .and_then(|declared| declared.payload.as_ref())
306 .map_or("", |payload| payload.instruction.as_str());
307 let mut facts = vec![("Profile", profile(&eval.profile))];
308 facts.extend(
309 eval.fingerprint
310 .as_ref()
311 .map(|fingerprint| ("Fingerprint", fingerprint.to_string())),
312 );
313 facts.push(("Eval", eval.id.to_string()));
314 facts.push(("Verify would", eval.action.to_string()));
315 facts.push(("State", eval.state.to_string()));
316 facts.extend(eval.key.as_ref().map(|key| ("Reuse key", key.to_string())));
317 facts.push(("Definition hash", eval.eval_def_hash.to_string()));
318 facts.extend(eval.last.as_ref().map(|last| {
319 ("Last Run", format!("{} · {}", last.run_id, last.verdict))
320 }));
321 if let Some(changes) = &eval.changes {
322 facts.push(("Changes", changes.summary.clone()));
323 facts.extend(
324 changes
325 .files
326 .as_ref()
327 .map(|files| ("Changed files", files.join("\n"))),
328 );
329 facts.extend(
330 changes
331 .dependencies
332 .as_ref()
333 .map(|names| ("Changed dependencies", names.join("\n"))),
334 );
335 }
336 CurrentEval {
337 id: eval.id.clone(),
338 name: crate::monitor::model::tree::local(&eval.id).to_owned(),
339 title: eval.title.clone(),
340 kind: eval.profile.kind(),
341 plan,
342 state: eval.state,
343 reason: if plan == Plan::Check {
344 CHECKING.into()
345 } else {
346 eval.reason.clone()
347 },
348 card: card(instruction, &eval.profile, plan, reused, now),
349 facts,
350 }
351 })
352 .collect();
353 CurrentArtifact {
354 name: artifact.id.clone(),
355 kind: artifact.kind,
356 plan: evals
357 .iter()
358 .map(|eval| eval.plan)
359 .min()
360 .unwrap_or(Plan::Reuse),
361 runs: evals.iter().filter(|eval| eval.plan == Plan::Run).count(),
362 path: artifact.path.clone(),
363 state: artifact.state,
364 reason: artifact.reason.clone(),
365 evals,
366 parts: parts.remove(&artifact.id).unwrap_or_default(),
367 }
368 })
369 .collect();
370 let mut plans: BTreeMap<ArtifactName, Plan> = artifacts
371 .iter()
372 .map(|artifact| (artifact.name.clone(), artifact.plan))
373 .collect();
374 let wholes: Vec<usize> = (0..artifacts.len())
377 .filter(|&at| artifacts[at].evals.is_empty() && !artifacts[at].parts.is_empty())
378 .collect();
379 let mut changed = true;
380 while changed {
381 changed = false;
382 for &at in &wholes {
383 let artifact = &mut artifacts[at];
384 let plan = artifact
385 .parts
386 .iter()
387 .filter_map(|part| plans.get(part).copied())
388 .min()
389 .unwrap_or(Plan::Reuse);
390 if plan != artifact.plan {
391 artifact.plan = plan;
392 plans.insert(artifact.name.clone(), plan);
393 changed = true;
394 }
395 }
396 }
397 let pairs: BTreeSet<(&ArtifactName, &ArtifactName)> = declared
398 .structure
399 .relations
400 .iter()
401 .filter(|relation| {
402 relation.source != relation.target && relation.kind != graph_structure::Edge::Part
403 })
404 .map(|relation| (&relation.source, &relation.target))
405 .collect();
406 let edges = pairs
407 .into_iter()
408 .filter_map(|(from, to)| {
409 Some(CurrentEdge {
410 plan: *plans.get(from)?,
411 from: from.clone(),
412 to: plans.contains_key(to).then(|| to.clone())?,
413 })
414 })
415 .collect();
416 Current {
417 scanned_at: now,
418 fingerprint: declared.fingerprint.clone(),
419 structure: declared.tree.clone(),
420 artifacts,
421 edges,
422 }
423}
424
425#[cfg(test)]
426mod tests;