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::Run, runs) => format!("would run ({runs} evals)"),
83 (plan, _) => plan.word().into(),
84 }
85 }
86}
87
88impl From<VerifyAction> for Plan {
89 fn from(action: VerifyAction) -> Self {
90 match action {
91 VerifyAction::Execute => Self::Run,
92 VerifyAction::Blocked => Self::Blocked,
93 VerifyAction::Wait => Self::Wait,
94 VerifyAction::Reuse => Self::Reuse,
95 }
96 }
97}
98
99const CHECKING: &str = "Still being checked: a fingerprint its key needs, or the plan of an eval it waits for, is not known yet.";
101
102#[derive(Debug, Clone)]
104pub struct CurrentEval {
105 pub id: EvalId,
106 pub name: String,
108 pub title: String,
109 pub kind: ProfileKind,
110 pub plan: Plan,
111 pub state: EvalCondition,
112 pub reason: String,
114 pub card: Card,
116 pub facts: Vec<(&'static str, String)>,
118}
119
120#[derive(Debug, Clone)]
122pub struct CurrentArtifact {
123 pub name: ArtifactName,
124 pub kind: ArtifactKind,
125 pub plan: Plan,
126 pub runs: usize,
128 pub path: PathBuf,
129 pub state: ArtifactCondition,
130 pub reason: String,
131 pub evals: Vec<CurrentEval>,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct CurrentEdge {
137 pub from: ArtifactName,
138 pub to: ArtifactName,
139 pub plan: Plan,
140}
141
142#[derive(Debug, Clone)]
144pub struct Current {
145 pub scanned_at: OffsetDateTime,
146 pub fingerprint: Fingerprint,
147 pub structure: Arc<str>,
149 pub artifacts: Vec<CurrentArtifact>,
151 pub edges: Vec<CurrentEdge>,
152}
153
154pub async fn scan(
159 repo: &Path,
160 state: &Path,
161 cancellation: CancellationToken,
162 publish: impl FnMut(Current) + Send,
163) -> Result<Current, String> {
164 let current = read(repo, state, cancellation, publish).await;
165 crate::platform::release_free_memory();
168 current
169}
170
171async fn read(
172 repo: &Path,
173 state: &Path,
174 cancellation: CancellationToken,
175 mut publish: impl FnMut(Current) + Send,
176) -> Result<Current, String> {
177 let config = crate::config::read_workspace_config(repo).map_err(|error| error.to_string())?;
179 let structure = graph_structure::extract(&config);
180 let declared = Declared {
182 tree: structure.tree().into(),
183 fingerprint: structure.fingerprint(),
184 structure: &structure,
185 };
186 let (status, cached) = crate::project::scan(
187 config,
188 Some(state),
189 &Selection::All,
190 &VerifyOptions::default(),
191 cancellation,
192 Some(&mut |progress: crate::project::Progress<'_>| {
193 publish(project(
194 &declared,
195 &progress.view,
196 progress.cached,
197 &progress.checking,
198 OffsetDateTime::now_utc(),
199 ));
200 }),
201 )
202 .await?;
203 Ok(project(
204 &declared,
205 &status,
206 &cached,
207 &BTreeSet::new(),
208 OffsetDateTime::now_utc(),
209 ))
210}
211
212fn profile(profile: &Profile) -> String {
214 match profile {
215 Profile::Agent { backend, model, .. } => format!("agent · {} · {model}", backend.as_str()),
216 other => other.kind().name().to_owned(),
217 }
218}
219
220fn card(
221 instruction: &str,
222 profile: &Profile,
223 plan: Plan,
224 reused: Option<&Execution>,
225 now: OffsetDateTime,
226) -> Card {
227 let result = reused.and_then(Execution::result);
228 Card {
229 instruction: instruction.to_owned(),
230 result: result.map(model::entries),
231 exit_code: match result {
232 Some(ExecutionResult::Runtime(result)) => result.exit_code.value().copied(),
233 _ => None,
234 },
235 error: None,
236 reused: plan == Plan::Reuse,
237 took: reused
238 .and_then(|execution| model::span(execution.started_at, execution.completed_at(), now)),
239 tokens: reused
240 .and_then(|execution| execution.usage.as_deref())
241 .and_then(model::attempt_tokens),
242 model: match profile {
243 Profile::Agent { model, .. } => Some(model.to_string()),
244 _ => None,
245 },
246 }
247}
248
249struct Declared<'s> {
251 structure: &'s Structure,
252 tree: Arc<str>,
253 fingerprint: Fingerprint,
254}
255
256fn project(
259 declared: &Declared<'_>,
260 status: &StatusView,
261 cached: &BTreeMap<EvalId, Claim>,
262 checking: &BTreeSet<&EvalId>,
263 now: OffsetDateTime,
264) -> Current {
265 let mut by_target: BTreeMap<&ArtifactName, Vec<&EvalState>> = BTreeMap::new();
266 for eval in &status.evals {
267 by_target.entry(&eval.target).or_default().push(eval);
268 }
269 let artifacts: Vec<CurrentArtifact> = status
270 .artifacts
271 .iter()
272 .map(|artifact| {
273 let evals: Vec<CurrentEval> =
274 by_target
275 .get(&artifact.id)
276 .into_iter()
277 .flatten()
278 .map(|eval| {
279 let plan = if checking.contains(&eval.id) {
280 Plan::Check
281 } else {
282 Plan::from(eval.action)
283 };
284 let reused = match cached.get(&eval.id) {
285 Some(Claim::Reuse(execution)) => Some(&**execution),
286 _ => None,
287 };
288 let instruction = declared
289 .structure
290 .evals
291 .get(&eval.id)
292 .and_then(|declared| declared.payload.as_ref())
293 .map_or("", |payload| payload.instruction.as_str());
294 let mut facts = vec![("Profile", profile(&eval.profile))];
295 facts.extend(
296 eval.fingerprint
297 .as_ref()
298 .map(|fingerprint| ("Fingerprint", fingerprint.to_string())),
299 );
300 facts.push(("Eval", eval.id.to_string()));
301 facts.push(("Verify would", eval.action.to_string()));
302 facts.push(("State", eval.state.to_string()));
303 facts.extend(eval.key.as_ref().map(|key| ("Reuse key", key.to_string())));
304 facts.push(("Definition hash", eval.eval_def_hash.to_string()));
305 facts.extend(eval.last.as_ref().map(|last| {
306 ("Last Run", format!("{} · {}", last.run_id, last.verdict))
307 }));
308 if let Some(changes) = &eval.changes {
309 facts.push(("Changes", changes.summary.clone()));
310 facts.extend(
311 changes
312 .files
313 .as_ref()
314 .map(|files| ("Changed files", files.join("\n"))),
315 );
316 facts.extend(
317 changes
318 .dependencies
319 .as_ref()
320 .map(|names| ("Changed dependencies", names.join("\n"))),
321 );
322 }
323 CurrentEval {
324 id: eval.id.clone(),
325 name: crate::monitor::model::tree::local(&eval.id).to_owned(),
326 title: eval.title.clone(),
327 kind: eval.profile.kind(),
328 plan,
329 state: eval.state,
330 reason: if plan == Plan::Check {
331 CHECKING.into()
332 } else {
333 eval.reason.clone()
334 },
335 card: card(instruction, &eval.profile, plan, reused, now),
336 facts,
337 }
338 })
339 .collect();
340 CurrentArtifact {
341 name: artifact.id.clone(),
342 kind: artifact.kind,
343 plan: evals
344 .iter()
345 .map(|eval| eval.plan)
346 .min()
347 .unwrap_or(Plan::Reuse),
348 runs: evals.iter().filter(|eval| eval.plan == Plan::Run).count(),
349 path: artifact.path.clone(),
350 state: artifact.state,
351 reason: artifact.reason.clone(),
352 evals,
353 }
354 })
355 .collect();
356 let plans: BTreeMap<&ArtifactName, Plan> = artifacts
357 .iter()
358 .map(|artifact| (&artifact.name, artifact.plan))
359 .collect();
360 let pairs: BTreeSet<(&ArtifactName, &ArtifactName)> = declared
361 .structure
362 .relations
363 .iter()
364 .filter(|relation| relation.source != relation.target)
365 .map(|relation| (&relation.source, &relation.target))
366 .collect();
367 let edges = pairs
368 .into_iter()
369 .filter_map(|(from, to)| {
370 Some(CurrentEdge {
371 plan: *plans.get(from)?,
372 from: from.clone(),
373 to: plans.contains_key(to).then(|| to.clone())?,
374 })
375 })
376 .collect();
377 Current {
378 scanned_at: now,
379 fingerprint: declared.fingerprint.clone(),
380 structure: declared.tree.clone(),
381 artifacts,
382 edges,
383 }
384}
385
386#[cfg(test)]
387mod tests;