Skip to main content

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;