kernel/event/record/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//! What an official harness reported during a run and what it answered
7//! the city, in this city's own words.
8//!
9//! The words are the city's and not ACP's (kernel D9): a spelling
10//! the Ledger wrote cannot change, and ACP's words follow upstream. The
11//! assembly maps each ACP reading onto these types with an exhaustive
12//! `match`, so a report upstream adds stops the build at the mapping.
13//!
14//! Neither payload carries `#[serde(default)]`: both kinds were written
15//! in this shape from their first line, so a line that lacks a key is
16//! refused rather than guessed at.
17
18use serde::{Deserialize, Serialize};
19
20/// `harness_reported`: one thing the harness reported during a run.
21///
22/// The `report` key says which. A fold that answers "what did the city
23/// do" never reads this kind; only a view that answers "what did the
24/// harness say" does.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
27#[serde(tag = "report", rename_all = "snake_case")]
28pub enum HarnessReported {
29 /// A piece of the answer to the city.
30 Said {
31 /// The piece, as the harness sent it.
32 text: String,
33 },
34 /// A piece of the harness's reasoning.
35 Thought {
36 /// The piece, as the harness sent it.
37 text: String,
38 },
39 /// A tool call the harness started, in its own words.
40 ToolCall {
41 /// The harness's id for the call.
42 call: String,
43 /// The title the harness gave the call.
44 title: String,
45 /// The harness's own word for what kind of call it is.
46 kind: String,
47 },
48 /// The status of a tool call the harness started changed.
49 ToolCallStatus {
50 /// The harness's id for the call.
51 call: String,
52 /// The harness's own word for the new status.
53 status: String,
54 },
55 /// A report this city has no reading for; only its name is kept.
56 Other {
57 /// The name of the report as the harness spelled it.
58 variant: String,
59 },
60 /// The harness asked the city for a permission.
61 PermissionAsked {
62 /// What the harness asked to do.
63 title: String,
64 /// The options it offered, in the order it offered them.
65 options: Vec<HarnessPermit>,
66 },
67 /// The city answered a permission the harness asked for.
68 PermissionAnswered {
69 /// The id of the option the city chose; `None` is the answer
70 /// `cancelled`.
71 chosen: Option<String>,
72 },
73}
74
75/// One option a harness offered when it asked for a permission.
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
78pub struct HarnessPermit {
79 /// The harness's id for the option; the city answers with it.
80 pub id: String,
81 /// The name the harness shows for the option.
82 pub name: String,
83 /// What choosing the option grants or refuses.
84 pub kind: HarnessPermitKind,
85}
86
87/// What choosing a permission option grants or refuses.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
90#[serde(rename_all = "snake_case")]
91pub enum HarnessPermitKind {
92 /// Allowed this once.
93 AllowOnce,
94 /// Allowed from now on.
95 AllowAlways,
96 /// Refused this once.
97 RejectOnce,
98 /// Refused from now on.
99 RejectAlways,
100}
101
102/// `harness_answered`: the harness's answer to the city's prompt.
103///
104/// Written when the stop reason arrives, before the run freezes, for
105/// every stop reason. An `end_turn` answer with text is evidence of
106/// `Completion::Done` (`crates/kernel/spec/Completion.lean` ยง8-20).
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
109pub struct HarnessAnswered {
110 /// Why the harness stopped.
111 pub stop: HarnessStop,
112 /// The answer pieces of this turn, joined in order; written even
113 /// when empty.
114 pub text: String,
115}
116
117/// Why a harness stopped answering the city's prompt.
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
119#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
120#[serde(rename_all = "snake_case")]
121pub enum HarnessStop {
122 /// The harness finished its turn.
123 EndTurn,
124 /// The model ran out of output tokens.
125 MaxTokens,
126 /// The harness reached its limit of model requests for one turn.
127 MaxTurnRequests,
128 /// The model refused.
129 Refusal,
130 /// The city cancelled the turn.
131 Cancelled,
132}
133
134#[cfg(test)]
135#[allow(clippy::unwrap_used, reason = "test code")]
136mod tests;