1use crate::capabilities::LlmCapabilities;
4use crate::completion::{CompletionRequest, CompletionResponse, StopReason, Usage};
5use crate::context::NamespaceError;
6use crate::ids::{RunId, SpanId};
7use crate::task::Task;
8use crate::MetadataMap;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use time::OffsetDateTime;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct SchemaVersion {
17 pub major: u16,
18 pub minor: u16,
19}
20
21impl SchemaVersion {
22 pub const CURRENT: SchemaVersion = SchemaVersion { major: 1, minor: 0 };
23}
24
25impl std::fmt::Display for SchemaVersion {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 write!(f, "{}.{}", self.major, self.minor)
28 }
29}
30
31impl Serialize for SchemaVersion {
32 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
33 s.serialize_str(&self.to_string())
34 }
35}
36
37impl<'de> Deserialize<'de> for SchemaVersion {
38 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
39 let s = String::deserialize(d)?;
40 let (maj, min) = s
41 .split_once('.')
42 .ok_or_else(|| serde::de::Error::custom("schema version must be `MAJOR.MINOR`"))?;
43 let major = maj.parse().map_err(serde::de::Error::custom)?;
44 let minor = min.parse().map_err(serde::de::Error::custom)?;
45 Ok(SchemaVersion { major, minor })
46 }
47}
48
49#[cfg(feature = "schemars-export")]
50impl schemars::JsonSchema for SchemaVersion {
51 fn schema_name() -> String {
52 "SchemaVersion".into()
53 }
54 fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
55 schemars::schema::SchemaObject {
59 instance_type: Some(schemars::schema::InstanceType::String.into()),
60 string: Some(Box::new(schemars::schema::StringValidation {
61 pattern: Some(r"^\d+\.\d+$".into()),
62 ..Default::default()
63 })),
64 ..Default::default()
65 }
66 .into()
67 }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
73#[cfg_attr(feature = "schemars-export", derive(schemars::JsonSchema))]
74pub struct Event {
75 pub v: SchemaVersion,
76 pub seq: u64,
77 pub run_id: RunId,
78 #[serde(
79 default,
80 skip_serializing_if = "Option::is_none",
81 with = "time::serde::rfc3339::option"
82 )]
83 #[cfg_attr(feature = "schemars-export", schemars(with = "Option<String>"))]
84 pub timestamp: Option<OffsetDateTime>,
85 pub span_id: SpanId,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub parent: Option<u64>,
88 #[serde(flatten)]
91 pub kind: EventKind,
92 #[serde(default, skip_serializing_if = "Vec::is_empty")]
93 pub redactions: Vec<String>,
94}
95
96impl Event {
97 pub fn new(seq: u64, run_id: RunId, span_id: impl Into<SpanId>, kind: EventKind) -> Self {
98 Self {
99 v: SchemaVersion::CURRENT,
100 seq,
101 run_id,
102 timestamp: Some(OffsetDateTime::now_utc()),
103 span_id: span_id.into(),
104 parent: None,
105 kind,
106 redactions: Vec::new(),
107 }
108 }
109
110 pub fn with_parent(mut self, parent: u64) -> Self {
111 self.parent = Some(parent);
112 self
113 }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
119#[cfg_attr(feature = "schemars-export", derive(schemars::JsonSchema))]
120#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
121#[non_exhaustive]
122pub enum EventKind {
123 #[serde(rename = "meta")]
124 Meta(MetaPayload),
125
126 #[serde(rename = "run.started")]
127 RunStarted(RunStartedPayload),
128 #[serde(rename = "run.finished")]
129 RunFinished(RunFinishedPayload),
130
131 #[serde(rename = "turn.started")]
132 TurnStarted(TurnPayload),
133 #[serde(rename = "turn.finished")]
134 TurnFinished(TurnFinishedPayload),
135 #[serde(rename = "turn.revised")]
136 TurnRevised(TurnRevisedPayload),
137
138 #[serde(rename = "llm.request")]
139 LlmRequest(LlmRequestPayload),
140 #[serde(rename = "llm.response")]
141 LlmResponse(LlmResponsePayload),
142 #[serde(rename = "llm.stream.chunk")]
143 LlmStreamChunk(Value),
144 #[serde(rename = "llm.retry")]
145 LlmRetry(LlmRetryPayload),
146 #[serde(rename = "llm.failed")]
147 LlmFailed(LlmFailedPayload),
148
149 #[serde(rename = "tool.call.started")]
150 ToolCallStarted(ToolCallStartedPayload),
151 #[serde(rename = "tool.call.finished")]
152 ToolCallFinished(ToolCallFinishedPayload),
153 #[serde(rename = "tool.call.failed")]
154 ToolCallFailed(ToolCallFailedPayload),
155 #[serde(rename = "tool.approval.requested")]
156 ToolApprovalRequested(Value),
157 #[serde(rename = "tool.approval.decided")]
158 ToolApprovalDecided(Value),
159
160 #[serde(rename = "memory.evicted")]
161 MemoryEvicted(Value),
162 #[serde(rename = "memory.summarized")]
163 MemorySummarized(Value),
164 #[serde(rename = "memory.retrieved")]
165 MemoryRetrieved(Value),
166
167 #[serde(rename = "budget.exceeded")]
168 BudgetExceeded(Value),
169
170 #[serde(rename = "policy.input.checked")]
171 PolicyInputChecked(Value),
172 #[serde(rename = "policy.output.checked")]
173 PolicyOutputChecked(Value),
174 #[serde(rename = "policy.blocked")]
175 PolicyBlocked(Value),
176
177 #[serde(rename = "planner.proposed")]
178 PlannerProposed(Value),
179 #[serde(rename = "planner.revised")]
180 PlannerRevised(Value),
181 #[serde(rename = "planner.committed")]
182 PlannerCommitted(Value),
183
184 #[serde(rename = "critic.assessed")]
185 CriticAssessed(Value),
186 #[serde(rename = "critic.rejected")]
187 CriticRejected(Value),
188 #[serde(rename = "critic.revised")]
189 CriticRevised(Value),
190 #[serde(rename = "critic.failed")]
191 CriticFailed(Value),
192
193 #[serde(rename = "reflection.generated")]
194 ReflectionGenerated(Value),
195 #[serde(rename = "reflection.injected")]
196 ReflectionInjected(Value),
197
198 #[serde(rename = "human.interrupt")]
199 HumanInterrupt(Value),
200 #[serde(rename = "human.inject")]
201 HumanInject(Value),
202
203 #[serde(rename = "user.simulated.message")]
204 UserSimulatedMessage(Value),
205 #[serde(rename = "user.simulated.ended")]
206 UserSimulatedEnded(Value),
207
208 #[serde(rename = "user.log")]
211 UserLog(UserLogPayload),
212
213 #[serde(other)]
215 Unknown,
216}
217
218pub const RESERVED_NAMESPACE_PREFIXES: &[&str] = &[
220 "run.",
221 "turn.",
222 "llm.",
223 "tool.",
224 "memory.",
225 "budget.",
226 "policy.",
227 "planner.",
228 "critic.",
229 "reflection.",
230 "human.",
231 "user.simulated.",
232 "meta.",
233];
234
235impl EventKind {
236 pub fn user_log(namespace: impl Into<String>, data: Value) -> Result<Self, NamespaceError> {
239 let namespace = namespace.into();
240 if namespace.is_empty() {
241 return Err(NamespaceError::Empty);
242 }
243 for reserved in RESERVED_NAMESPACE_PREFIXES {
245 let bare = reserved.trim_end_matches('.');
246 if namespace == bare || namespace.starts_with(reserved) {
247 return Err(NamespaceError::BuiltinCollision(namespace));
248 }
249 }
250 Ok(EventKind::UserLog(UserLogPayload { namespace, data }))
251 }
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
257#[cfg_attr(feature = "schemars-export", derive(schemars::JsonSchema))]
258pub struct MetaPayload {
259 pub schema_version: SchemaVersion,
260 pub harness_version: String,
261 pub task_snapshot: Task,
262 pub llm_capabilities: LlmCapabilities,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
266#[cfg_attr(feature = "schemars-export", derive(schemars::JsonSchema))]
267pub struct RunStartedPayload {
268 #[serde(default, skip_serializing_if = "MetadataMap::is_empty")]
269 pub extra: MetadataMap,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
273#[cfg_attr(feature = "schemars-export", derive(schemars::JsonSchema))]
274pub struct RunFinishedPayload {
275 pub termination: String,
276 pub turns: u32,
277 pub tool_calls: u32,
278 #[serde(default, skip_serializing_if = "MetadataMap::is_empty")]
279 pub extra: MetadataMap,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
283#[cfg_attr(feature = "schemars-export", derive(schemars::JsonSchema))]
284pub struct TurnPayload {
285 pub turn_index: u32,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
289#[cfg_attr(feature = "schemars-export", derive(schemars::JsonSchema))]
290pub struct TurnFinishedPayload {
291 pub turn_index: u32,
292 pub stop_reason: StopReason,
293 pub usage: Usage,
294 pub tool_calls: u32,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
298#[cfg_attr(feature = "schemars-export", derive(schemars::JsonSchema))]
299pub struct TurnRevisedPayload {
300 pub original_seq: u64,
301 pub replacement_seq: u64,
302 pub reason: String,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
306#[cfg_attr(feature = "schemars-export", derive(schemars::JsonSchema))]
307pub struct LlmRequestPayload {
308 pub request: CompletionRequest,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub provider: Option<String>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
314#[cfg_attr(feature = "schemars-export", derive(schemars::JsonSchema))]
315pub struct LlmResponsePayload {
316 pub response: CompletionResponse,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
320#[cfg_attr(feature = "schemars-export", derive(schemars::JsonSchema))]
321pub struct LlmRetryPayload {
322 pub attempt: u32,
323 pub reason: String,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
327#[cfg_attr(feature = "schemars-export", derive(schemars::JsonSchema))]
328pub struct LlmFailedPayload {
329 pub reason: String,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
333#[cfg_attr(feature = "schemars-export", derive(schemars::JsonSchema))]
334pub struct ToolCallStartedPayload {
335 pub tool_name: String,
336 pub tool_use_id: String,
337 pub input: Value,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
341#[cfg_attr(feature = "schemars-export", derive(schemars::JsonSchema))]
342pub struct ToolCallFinishedPayload {
343 pub tool_name: String,
344 pub tool_use_id: String,
345 pub output: Value,
346 #[serde(default)]
347 pub truncated: bool,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
351#[cfg_attr(feature = "schemars-export", derive(schemars::JsonSchema))]
352pub struct ToolCallFailedPayload {
353 pub tool_name: String,
354 pub tool_use_id: String,
355 pub reason: String,
356 #[serde(default)]
357 pub recoverable: bool,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
361#[cfg_attr(feature = "schemars-export", derive(schemars::JsonSchema))]
362pub struct UserLogPayload {
363 pub namespace: String,
364 #[serde(flatten)]
365 pub data: Value,
366}
367
368pub type EventPayload = Value;
371
372pub type EventConstructionError = NamespaceError;
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use crate::ids::ModelId;
379 use crate::message::{Content, Message};
380 use serde_json::json;
381
382 fn sample_request() -> CompletionRequest {
383 CompletionRequest {
384 messages: vec![Message::Assistant {
385 content: vec![
386 Content::text("Let me look around."),
387 Content::ToolUse {
388 id: "tu_1".into(),
389 name: "fs_list".into(),
390 input: json!({"path": "."}),
391 },
392 ],
393 stop_reason: Some(StopReason::ToolUse),
394 meta: MetadataMap::new(),
395 }],
396 tools: Vec::new(),
397 system: None,
398 max_tokens: Some(1024),
399 temperature: None,
400 stop_sequences: Vec::new(),
401 cache_hints: Default::default(),
402 extensions: MetadataMap::new(),
403 }
404 }
405
406 #[test]
407 fn llm_request_event_round_trips_through_jsonl() {
408 let req = sample_request();
409 let event = Event::new(
410 42,
411 RunId::new(),
412 SpanId::from("span_1"),
413 EventKind::LlmRequest(LlmRequestPayload {
414 request: req,
415 provider: Some("anthropic".into()),
416 }),
417 );
418 let bytes = serde_json::to_vec(&event).expect("serialize");
419 let back: Event = serde_json::from_slice(&bytes).expect("deserialize");
420 assert_eq!(back.seq, 42);
421 match back.kind {
422 EventKind::LlmRequest(p) => {
423 assert_eq!(p.provider.as_deref(), Some("anthropic"));
424 assert_eq!(p.request.messages.len(), 1);
425 }
426 other => panic!("expected LlmRequest, got {other:?}"),
427 }
428 }
429
430 #[test]
431 fn llm_response_event_round_trips_through_jsonl() {
432 let response = CompletionResponse {
433 id: "msg_1".into(),
434 model: ModelId::new("claude-sonnet-4-5"),
435 content: vec![Content::text("Hello world")],
436 stop_reason: StopReason::EndTurn,
437 usage: Usage {
438 tokens_input: 25,
439 tokens_output: 15,
440 ..Default::default()
441 },
442 };
443 let event = Event::new(
444 7,
445 RunId::new(),
446 SpanId::from("span_2"),
447 EventKind::LlmResponse(LlmResponsePayload { response }),
448 );
449 let bytes = serde_json::to_vec(&event).expect("serialize");
450 let back: Event = serde_json::from_slice(&bytes).expect("deserialize");
451 match back.kind {
452 EventKind::LlmResponse(p) => {
453 assert_eq!(p.response.id, "msg_1");
454 assert_eq!(p.response.content.len(), 1);
455 assert!(matches!(
456 &p.response.content[0],
457 Content::Text { text } if text == "Hello world"
458 ));
459 }
460 other => panic!("expected LlmResponse, got {other:?}"),
461 }
462 }
463}