1use serde::{Deserialize, Serialize};
14use serde_json::{Map, Value};
15
16use crate::budget::BudgetUse;
17use crate::error::{AxCode, AxError};
18use crate::event::{EventKind, EventRef};
19
20pub(crate) const CITABLE: [EventKind; 3] = [
25 EventKind::ToolResult,
26 EventKind::ModelReturned,
27 EventKind::HarnessAnswered,
28];
29
30#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct Evidence(Vec<EventRef>);
35
36impl Evidence {
37 pub fn new(refs: Vec<EventRef>) -> Result<Evidence, AxError> {
38 if refs.is_empty() {
39 return Err(AxError::failure(
40 AxCode::EvidenceMissing,
41 "construct evidence",
42 "empty ref list",
43 )
44 .with_recovery(format!("cite at least one {} event", citable_words()?)));
45 }
46 if let Some(bad) = refs.iter().find(|r| !CITABLE.contains(&r.kind())) {
47 return Err(AxError::failure(
48 AxCode::EvidenceMissing,
49 "construct evidence",
50 format!("ref kind {:?}", bad.kind()),
51 )
52 .with_recovery(format!("evidence kinds are {} only", citable_words()?)));
53 }
54 Ok(Evidence(refs))
55 }
56
57 pub fn refs(&self) -> &[EventRef] {
58 &self.0
59 }
60}
61
62pub(crate) fn citable_words() -> Result<String, AxError> {
65 let words = CITABLE
66 .iter()
67 .map(|kind| {
68 serde_json::to_value(kind)
69 .map(|word| word.to_string())
70 .map_err(|err| {
71 AxError::failure(
72 AxCode::InvalidArgs,
73 "spell a citable event kind",
74 format!("{kind:?}"),
75 )
76 .with_recovery(format!(
77 "JSON refused this event kind ({err}); cite an event whose kind \
78 `sprawling replay` prints"
79 ))
80 })
81 })
82 .collect::<Result<Vec<String>, AxError>>()?;
83 Ok(words.join(", "))
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum Completion {
90 Done(Evidence),
91 Limit,
92 Cancelled,
93}
94
95impl Completion {
96 pub fn name(&self) -> &'static str {
97 match self {
98 Completion::Done(_) => "done",
99 Completion::Limit => "limit",
100 Completion::Cancelled => "cancelled",
101 }
102 }
103
104 pub fn extend_payload(&self, map: &mut Map<String, Value>) -> Result<(), AxError> {
108 map.insert(
109 "completion".to_owned(),
110 Value::String(self.name().to_owned()),
111 );
112 if let Completion::Done(evidence) = self {
113 let mut cited = Vec::new();
114 for r in evidence.refs() {
115 let mut entry = Map::new();
116 entry.insert("seq".to_owned(), Value::Number(r.seq().value().into()));
117 entry.insert(
118 "kind".to_owned(),
119 serde_json::to_value(r.kind()).map_err(|err| {
120 AxError::failure(
121 AxCode::InvalidArgs,
122 "encode evidence",
123 format!("event kind at seq {}", r.seq().value()),
124 )
125 .with_recovery(format!(
126 "JSON refused this event kind ({err}); cite an event whose kind \
127 `sprawling replay --seq {}` prints",
128 r.seq().value()
129 ))
130 })?,
131 );
132 cited.push(Value::Object(entry));
133 }
134 map.insert("evidence".to_owned(), Value::Array(cited));
135 }
136 Ok(())
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
153#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
154pub struct PlannedProgress {
155 pub done: u32,
156 pub blocked: u32,
157 pub total: u32,
158 pub done_ppb: u64,
162 pub blocked_ppb: u64,
164}
165
166impl PlannedProgress {
167 pub fn ratio(&self) -> (u32, u32) {
170 (self.done, self.total)
171 }
172
173 pub fn weighted(&self) -> (u64, u64) {
176 (self.done_ppb, crate::share::WHOLE_PPB)
177 }
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
184#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
185pub struct UnplannedProgress {
186 pub steps: u32,
187 pub budget: BudgetUse,
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(rename_all = "snake_case")]
193#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
194pub enum Progress {
195 Planned(PlannedProgress),
196 Unplanned(UnplannedProgress),
197}
198
199#[cfg(test)]
200#[allow(
201 clippy::unwrap_used,
202 clippy::expect_used,
203 clippy::panic,
204 clippy::indexing_slicing,
205 reason = "test code"
206)]
207mod tests {
208 use super::*;
209 use crate::event::{EventDraft, EventRecord, Payload, RunId, Seq, TimeMs};
210 use crate::ledger::GENESIS_PREV;
211
212 fn evidence_ref(kind: EventKind) -> EventRef {
213 let draft = EventDraft {
214 run: RunId::CITY,
215 t: TimeMs::new(0),
216 who: "city".into(),
217 addr: None,
218 kind,
219 data: Payload::empty(),
220 ig: false,
221 };
222 EventRecord::from_draft(draft, Seq::FIRST, GENESIS_PREV).to_ref()
223 }
224
225 #[test]
226 fn empty_or_wrong_kind_evidence_cannot_be_built() {
227 assert_eq!(
228 Evidence::new(vec![]).unwrap_err().code(),
229 &AxCode::EvidenceMissing
230 );
231 let err = Evidence::new(vec![evidence_ref(EventKind::RunStarted)]).unwrap_err();
232 assert_eq!(err.code(), &AxCode::EvidenceMissing);
233 }
234
235 #[test]
236 fn a_harness_answer_is_evidence() {
237 assert!(Evidence::new(vec![evidence_ref(EventKind::HarnessAnswered)]).is_ok());
238 let err = Evidence::new(vec![evidence_ref(EventKind::HarnessReported)]).unwrap_err();
239 assert_eq!(err.code(), &AxCode::EvidenceMissing);
240 let recovery = err.recovery();
241 let at = |word: &str| recovery.find(word).unwrap();
242 assert!(at("tool_result") < at("model_returned"));
243 assert!(at("model_returned") < at("harness_answered"));
244 }
245
246 #[test]
247 fn done_payload_cites_its_evidence() {
248 let done =
249 Completion::Done(Evidence::new(vec![evidence_ref(EventKind::ModelReturned)]).unwrap());
250 let mut map = Map::new();
251 done.extend_payload(&mut map).unwrap();
252 assert_eq!(map["completion"], "done");
253 assert_eq!(map["evidence"][0]["kind"], "model_returned");
254 let mut map = Map::new();
255 Completion::Limit.extend_payload(&mut map).unwrap();
256 assert_eq!(map["completion"], "limit");
257 assert!(!map.contains_key("evidence"));
258 }
259
260 #[test]
261 fn only_planned_progress_owns_a_ratio() {
262 let planned = PlannedProgress {
263 done: 3,
264 blocked: 1,
265 total: 9,
266 done_ppb: 250_000_000,
267 blocked_ppb: 0,
268 };
269 assert_eq!(planned.ratio(), (3, 9));
270 assert_eq!(
271 planned.weighted(),
272 (250_000_000, crate::share::WHOLE_PPB),
273 "a quarter of the plan behind three of nine rows: the two \
274 figures are not the same reading"
275 );
276 let unplanned = UnplannedProgress {
279 steps: 12,
280 budget: BudgetUse::default(),
281 };
282 assert_eq!(unplanned.steps, 12);
283 }
284}