kernel/artifacts/
provenance.rs1use crate::records::{JsonValue, ParamSpec};
6
7use super::artifact::Artifact;
8
9pub struct Provenance;
11
12impl Provenance {
13 pub fn line(artifact: &Artifact, schema: &[ParamSpec]) -> String {
15 let mut parts = vec![artifact.model.clone()];
16 for (key, value) in param_pairs(&artifact.params, schema) {
17 parts.push(format!("{key} {value}"));
18 }
19 parts.push(Self::duration(artifact.duration_ms));
20 parts.join(" ยท ")
21 }
22
23 pub fn duration(ms: i64) -> String {
25 if ms < 1000 {
26 return format!("{ms} ms");
27 }
28 if ms < 60_000 {
29 return format!("{:.1}s", ms as f64 / 1000.0);
30 }
31 let minutes = ms / 60_000;
32 let seconds = (ms % 60_000) / 1000;
33 if seconds == 0 {
34 format!("{minutes}m")
35 } else {
36 format!("{minutes}m {seconds}s")
37 }
38 }
39
40 pub fn details(artifact: &Artifact, schema: &[ParamSpec]) -> String {
43 let mut lines = vec![
44 format!("model: {}", artifact.model),
45 format!("runtime: {}", artifact.runtime),
46 format!("capability: {}", artifact.capability.as_ref()),
47 ];
48 lines.extend(prompt_and_param_lines(&artifact.params, schema));
49 lines.push(format!(
50 "duration: {}",
51 Self::duration(artifact.duration_ms)
52 ));
53 lines.push(format!("job: {}", artifact.job_id));
54 lines.join("\n")
55 }
56
57 pub fn failure_details(
59 model: &str,
60 error: &str,
61 job_id: Option<&str>,
62 params: &JsonValue,
63 schema: &[ParamSpec],
64 ) -> String {
65 let mut lines = vec![format!("model: {model}"), format!("error: {error}")];
66 if let Some(job_id) = job_id {
67 lines.push(format!("job: {job_id}"));
68 }
69 lines.extend(prompt_and_param_lines(params, schema));
70 lines.join("\n")
71 }
72
73 pub fn prompt(params: &JsonValue) -> Option<String> {
75 params
76 .as_object()
77 .and_then(|fields| fields.get("prompt"))
78 .and_then(JsonValue::as_str)
79 .map(str::to_owned)
80 }
81}
82
83fn prompt_and_param_lines(params: &JsonValue, schema: &[ParamSpec]) -> Vec<String> {
84 let mut lines = Vec::new();
85 if let Some(prompt) = Provenance::prompt(params) {
86 lines.push(format!("prompt: {prompt}"));
87 }
88 for (key, value) in param_pairs(params, schema) {
89 lines.push(format!("{key}: {value}"));
90 }
91 lines
92}
93
94fn param_pairs(params: &JsonValue, schema: &[ParamSpec]) -> Vec<(String, String)> {
95 let Some(fields) = params.as_object() else {
96 return Vec::new();
97 };
98 let mut keys: Vec<String> = schema
99 .iter()
100 .map(|spec| spec.key.clone())
101 .filter(|key| fields.contains_key(key))
102 .collect();
103 let mut extras: Vec<String> = fields
104 .keys()
105 .filter(|key| *key != "prompt" && !keys.contains(key))
106 .cloned()
107 .collect();
108 extras.sort();
109 keys.append(&mut extras);
110 keys.into_iter()
111 .filter(|key| key != "prompt")
113 .filter_map(|key| {
114 let value = fields.get(&key)?;
115 let rendered = scalar(value)?;
116 Some((key, rendered))
117 })
118 .collect()
119}
120
121fn scalar(value: &JsonValue) -> Option<String> {
122 match value {
123 JsonValue::Int(raw) => Some(raw.to_string()),
124 JsonValue::Double(raw) => Some(raw.to_string()),
125 JsonValue::String(raw) => Some(raw.clone()),
126 JsonValue::Bool(raw) => Some(raw.to_string()),
127 _ => None,
128 }
129}