1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
//! Durable harness event ledger types.
use crate::{
ActorId, AgentId, ContextPack, Message, ModelName, PolicyDecision, RunId, ToolCall, ToolCallId,
ToolProposal, ToolResult, TurnId,
};
use serde::{Deserialize, Serialize};
/// Token usage reported by a model provider.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ModelUsage {
/// Prompt-side tokens used by the request.
pub input_tokens: u32,
/// Completion-side tokens emitted by the model.
pub output_tokens: u32,
}
/// One durable event with host-supplied ordering metadata.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RecordedEvent {
/// Per-run sequence number, contiguous from zero.
pub seq: u64,
/// Host-supplied wall-clock timestamp in milliseconds since Unix epoch.
pub occurred_at_ms: u64,
/// Recorded run fact.
pub event: HarnessEvent,
}
/// Durable event log entries. Transcript, metrics, replay, and audit views are derived from this log.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "event")]
pub enum HarnessEvent {
/// Binds a fresh state machine to one run and agent.
RunStarted {
/// Durable run identifier shared by every later event.
run_id: RunId,
/// Agent identity selected by the host for this run.
agent_id: AgentId,
},
/// Records the exact bounded context selected for a model turn.
ContextBuilt {
/// Run receiving the context.
run_id: RunId,
/// New turn identifier; concluded turn identifiers cannot be reused.
turn_id: TurnId,
/// Lane-labeled context that must pass budget validation.
context: ContextPack,
},
/// Records that a model request crossed the provider boundary.
ModelRequested {
/// Run making the request.
run_id: RunId,
/// Turn whose context is being submitted.
turn_id: TurnId,
/// Monotonic model step expected by the run state.
step: u32,
/// Host-selected model used for the request.
model: ModelName,
},
/// Records the normalized result returned by the pending model request.
ModelResponded {
/// Run receiving the response.
run_id: RunId,
/// Turn of the pending model request.
turn_id: TurnId,
/// Model step of the pending request.
step: u32,
/// Normalized model-authored message.
output: Message,
/// Unvalidated model proposals; an empty list concludes the turn.
proposed_calls: Vec<ToolProposal>,
/// Provider-reported token usage for the response.
usage: ModelUsage,
},
/// Records a model proposal after host validation and effect classification.
ToolCallProposed {
/// Run containing the proposal.
run_id: RunId,
/// Turn that produced the proposal.
turn_id: TurnId,
/// Host-validated call; its tool and input must match a pending proposal.
call: ToolCall,
},
/// Records the policy decision for the pending validated call.
PolicyEvaluated {
/// Run containing the call.
run_id: RunId,
/// Pending call evaluated by policy.
call_id: ToolCallId,
/// Durable allow, approval, or denial decision.
decision: PolicyDecision,
},
/// Records who granted a pending approval.
ApprovalGranted {
/// Run containing the call.
run_id: RunId,
/// Pending call approved for execution.
call_id: ToolCallId,
/// Human or host actor that granted approval.
actor_id: ActorId,
},
/// Records who denied a pending approval and why.
ApprovalDenied {
/// Run containing the call.
run_id: RunId,
/// Pending call denied before execution.
call_id: ToolCallId,
/// Human or host actor that denied approval.
actor_id: ActorId,
/// Durable denial reason for audit and continuation context.
reason: String,
},
/// Records that the host began executing the approved call.
ToolStarted {
/// Run containing the call.
run_id: RunId,
/// Approved call that crossed the execution boundary.
call_id: ToolCallId,
},
/// Records the structured result returned by the running call.
ToolFinished {
/// Run containing the call.
run_id: RunId,
/// Result whose call id must match the running call.
result: ToolResult,
},
/// Records that the running call failed without a result.
ToolFailed {
/// Run containing the call.
run_id: RunId,
/// Running call that failed.
call_id: ToolCallId,
/// Durable host-reported failure reason.
reason: String,
},
/// Terminates a concluded turn as a successful run.
RunFinished {
/// Run that completed.
run_id: RunId,
},
/// Terminates any started, nonterminal run as failed.
RunFailed {
/// Run that failed.
run_id: RunId,
/// Durable terminal failure reason.
reason: String,
},
}
impl HarnessEvent {
/// Returns the owning run id without validating event order or phase.
pub fn run_id(&self) -> &RunId {
match self {
Self::RunStarted { run_id, .. }
| Self::ContextBuilt { run_id, .. }
| Self::ModelRequested { run_id, .. }
| Self::ModelResponded { run_id, .. }
| Self::ToolCallProposed { run_id, .. }
| Self::PolicyEvaluated { run_id, .. }
| Self::ApprovalGranted { run_id, .. }
| Self::ApprovalDenied { run_id, .. }
| Self::ToolStarted { run_id, .. }
| Self::ToolFinished { run_id, .. }
| Self::ToolFailed { run_id, .. }
| Self::RunFinished { run_id }
| Self::RunFailed { run_id, .. } => run_id,
}
}
/// Returns the stable snake-case event name used in transition diagnostics.
pub fn name(&self) -> &'static str {
match self {
Self::RunStarted { .. } => "run_started",
Self::ContextBuilt { .. } => "context_built",
Self::ModelRequested { .. } => "model_requested",
Self::ModelResponded { .. } => "model_responded",
Self::ToolCallProposed { .. } => "tool_call_proposed",
Self::PolicyEvaluated { .. } => "policy_evaluated",
Self::ApprovalGranted { .. } => "approval_granted",
Self::ApprovalDenied { .. } => "approval_denied",
Self::ToolStarted { .. } => "tool_started",
Self::ToolFinished { .. } => "tool_finished",
Self::ToolFailed { .. } => "tool_failed",
Self::RunFinished { .. } => "run_finished",
Self::RunFailed { .. } => "run_failed",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{EffectClass, ToolCallId, ToolName};
use serde_json::json;
#[test]
fn harness_events_round_trip_as_json() {
let event = HarnessEvent::ToolCallProposed {
run_id: RunId::new("run_1").unwrap(),
turn_id: TurnId::new("turn_1").unwrap(),
call: ToolCall {
id: ToolCallId::new("call_1").unwrap(),
tool: ToolName::new("file.read").unwrap(),
effect: EffectClass::ReadOnly,
input: json!({ "path": "README.md" }),
},
};
let encoded = serde_json::to_string(&event).unwrap();
let decoded: HarnessEvent = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded, event);
}
#[test]
fn recorded_events_round_trip_as_json() {
let event = RecordedEvent {
seq: 0,
occurred_at_ms: 1_700_000_000_000,
event: HarnessEvent::RunStarted {
run_id: RunId::new("run_1").unwrap(),
agent_id: AgentId::new("agent_1").unwrap(),
},
};
let encoded = serde_json::to_string(&event).unwrap();
let decoded: RecordedEvent = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded, event);
}
}