runtime/run/harness.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! A run whose turn an official harness takes: every line the city
7//! writes for it, and the completion its answer freezes as
8//! (`crates/runtime/spec/Run.lean` ยง8-52).
9//!
10//! The harness runs its own tools, so the city writes what it decided -
11//! the opening, the cancel, the tree it committed, the answer to its own
12//! prompt, the freeze - and books what the harness reported as it went.
13//! The order of those lines and the ending are proved in
14//! `crates/agent_protocols/spec/Harness/Session.lean`; the types here
15//! hold the order, because `conclude` and `abandon` take the run by value
16//! and a frozen run has nothing left to write with.
17
18use kernel::event::record::{CancelReceived, HarnessAnswered, HarnessReported, HarnessStop};
19use kernel::{AxError, Completion, EventKind, EventRef, Evidence, Ledger, Payload, TimeMs};
20
21use crate::handoff::Handoff;
22
23use super::charter::Charter;
24
25/// Why the city cut a harness's turn short.
26///
27/// Both become the one `cancel_received` and the one `session/cancel`;
28/// they part only at the freeze, because a run the ceiling stopped hit
29/// something and a run a halt stopped was stopped by somebody.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Cut {
32 /// A halt of a scope that holds the room; a person's cancel of the
33 /// run is one.
34 Halt,
35 /// The wall-clock ceiling the building's rules set.
36 Deadline,
37}
38
39/// What a harness's answer leaves for the run to write: the tree as it
40/// stood when the harness stopped, what it answered, and the handoff.
41pub struct Conclusion<'c> {
42 /// The `checkpoint_committed` payload `storage::Checkpoint::wave_pre`
43 /// handed back for the harness's tree.
44 pub committed: Payload,
45 pub answered: HarnessAnswered,
46 pub handoff: &'c Handoff,
47}
48
49/// A harness run between its opening and its freeze.
50pub struct HarnessRun<'a> {
51 charter: Charter<'a>,
52 /// The first cut the city made, which the ending reads.
53 cut: Option<Cut>,
54}
55
56impl<'a> HarnessRun<'a> {
57 /// Writes the dispatch pair, as a model run's dispatch does.
58 ///
59 /// # Errors
60 /// Propagates the clock and the ledger.
61 pub fn open(
62 charter: Charter<'a>,
63 ledger: &mut dyn Ledger,
64 now: &mut dyn FnMut() -> Result<TimeMs, AxError>,
65 ) -> Result<HarnessRun<'a>, AxError> {
66 charter.open(ledger, now)?;
67 Ok(HarnessRun { charter, cut: None })
68 }
69
70 /// Books one thing the harness reported, after it said it.
71 ///
72 /// # Errors
73 /// Propagates a payload that will not encode and the ledger.
74 pub fn report(
75 &self,
76 ledger: &mut dyn Ledger,
77 reported: &HarnessReported,
78 t: TimeMs,
79 ) -> Result<(), AxError> {
80 ledger.append(
81 self.charter
82 .line(EventKind::HarnessReported, Payload::of(reported)?, t),
83 )?;
84 Ok(())
85 }
86
87 /// Books the cancel the city is about to send, the first time the
88 /// turn is cut; a later cut writes nothing and the first one is the
89 /// one the ending reads.
90 ///
91 /// # Errors
92 /// Propagates the ledger.
93 pub fn cancel(&mut self, ledger: &mut dyn Ledger, cut: Cut, t: TimeMs) -> Result<(), AxError> {
94 if self.cut.is_some() {
95 return Ok(());
96 }
97 ledger.append(self.charter.line(
98 EventKind::CancelReceived,
99 Payload::of(&CancelReceived {})?,
100 t,
101 ))?;
102 self.cut = Some(cut);
103 Ok(())
104 }
105
106 /// The harness answered: the tree, then the answer, then the freeze,
107 /// and the completion the answer and the first cut decide.
108 ///
109 /// # Errors
110 /// Propagates the ledger, and refuses a `t` at the end of `u64`.
111 pub fn conclude(
112 self,
113 ledger: &mut dyn Ledger,
114 conclusion: Conclusion<'_>,
115 t: TimeMs,
116 ) -> Result<Completion, AxError> {
117 let Conclusion {
118 committed,
119 answered,
120 handoff,
121 } = conclusion;
122 ledger.append(
123 self.charter
124 .line(EventKind::CheckpointCommitted, committed, t),
125 )?;
126 let cited = ledger.append(self.charter.line(
127 EventKind::HarnessAnswered,
128 Payload::of(&answered)?,
129 t,
130 ))?;
131 let completion = ending(&answered, self.cut, cited)?;
132 self.charter.close(ledger, handoff, &completion, t)?;
133 Ok(completion)
134 }
135
136 /// The session ended with no answer: there is nothing to record as
137 /// one, so the run freezes cancelled.
138 ///
139 /// # Errors
140 /// Propagates the ledger, and refuses a `t` at the end of `u64`.
141 pub fn abandon(
142 self,
143 ledger: &mut dyn Ledger,
144 handoff: &Handoff,
145 t: TimeMs,
146 ) -> Result<Completion, AxError> {
147 self.charter
148 .close(ledger, handoff, &Completion::Cancelled, t)?;
149 Ok(Completion::Cancelled)
150 }
151}
152
153/// How an answer ends the run: `Session.lean`'s `ending`.
154///
155/// An answer with nothing in it is not evidence that work finished, the
156/// reading `lifecycle::concluded` gives an empty model reply.
157fn ending(
158 answered: &HarnessAnswered,
159 cut: Option<Cut>,
160 cited: EventRef,
161) -> Result<Completion, AxError> {
162 Ok(match answered.stop {
163 HarnessStop::EndTurn if answered.text.trim().is_empty() => Completion::Limit,
164 HarnessStop::EndTurn => Completion::Done(Evidence::new(vec![cited])?),
165 HarnessStop::Cancelled => match cut {
166 Some(Cut::Deadline) => Completion::Limit,
167 Some(Cut::Halt) | None => Completion::Cancelled,
168 },
169 HarnessStop::MaxTokens | HarnessStop::MaxTurnRequests | HarnessStop::Refusal => {
170 Completion::Limit
171 }
172 })
173}
174
175#[cfg(test)]
176#[allow(clippy::unwrap_used, clippy::indexing_slicing, reason = "test code")]
177mod tests;