1use std::collections::BTreeMap;
4
5use serde::de::{self, MapAccess, SeqAccess, Visitor};
6use serde::Deserialize;
7use serde_json::{Map, Number, Value};
8
9use crate::budget::BudgetUsageSnapshot;
10use crate::checkpoint::{
11 canonical_json_bytes, CheckpointError, CheckpointResult, CheckpointStatus, CHECKPOINT_SCHEMA,
12 RUN_DEFINITION_SCHEMA,
13};
14use crate::runtime::state::{
15 validate_checkpoint, validate_extension_state_size, Checkpoint, EventOutboxEntry,
16 ExtensionStateEntry, OperationJournalEntry,
17};
18use crate::types::{CycleRecord, Message, ModelCallRecord};
19
20const KNOWN_FIELDS: &[&str] = &[
21 "schema_version",
22 "run_definition_schema",
23 "run_definition",
24 "checkpoint_key",
25 "task_id",
26 "root_run_id",
27 "trace_id",
28 "run_definition_digest",
29 "resume_attempt",
30 "cycle_index",
31 "status",
32 "messages",
33 "cycles",
34 "model_calls",
35 "shared_state",
36 "budget_usage",
37 "event_cursor",
38 "event_outbox",
39 "extension_state",
40 "model_call_journal",
41 "tool_journal",
42 "revision",
43 "claim_token",
44 "claimed_cycle",
45 "lease_expires_at_ms",
46 "terminal_result",
47 "terminal_acknowledged",
48];
49
50pub fn checkpoint_to_value(
51 checkpoint: &Checkpoint,
52 max_extension_state_bytes: u64,
53) -> CheckpointResult<Value> {
54 validate_checkpoint(checkpoint)?;
55 let extension_bytes =
56 validate_extension_state_size(&checkpoint.extension_state, max_extension_state_bytes);
57 extension_bytes?;
58
59 let mut object = Map::new();
60 object.insert(
61 "schema_version".to_string(),
62 Value::String(CHECKPOINT_SCHEMA.to_string()),
63 );
64 object.insert(
65 "run_definition_schema".to_string(),
66 Value::String(RUN_DEFINITION_SCHEMA.to_string()),
67 );
68 object.insert(
69 "run_definition".to_string(),
70 checkpoint.run_definition.clone(),
71 );
72 object.insert(
73 "checkpoint_key".to_string(),
74 Value::String(checkpoint.checkpoint_key.clone()),
75 );
76 object.insert(
77 "task_id".to_string(),
78 Value::String(checkpoint.task_id.clone()),
79 );
80 object.insert(
81 "root_run_id".to_string(),
82 Value::String(checkpoint.root_run_id.clone()),
83 );
84 object.insert(
85 "trace_id".to_string(),
86 Value::String(checkpoint.trace_id.clone()),
87 );
88 object.insert(
89 "run_definition_digest".to_string(),
90 Value::String(checkpoint.run_definition_digest.clone()),
91 );
92 object.insert(
93 "resume_attempt".to_string(),
94 Value::from(checkpoint.resume_attempt),
95 );
96 object.insert(
97 "cycle_index".to_string(),
98 Value::from(checkpoint.cycle_index),
99 );
100 object.insert(
101 "status".to_string(),
102 Value::String(checkpoint.status.as_str().to_string()),
103 );
104 object.insert(
105 "messages".to_string(),
106 Value::Array(checkpoint.messages.iter().map(Message::to_dict).collect()),
107 );
108 object.insert(
109 "cycles".to_string(),
110 Value::Array(checkpoint.cycles.iter().map(CycleRecord::to_dict).collect()),
111 );
112 object.insert(
113 "model_calls".to_string(),
114 serde_json::to_value(&checkpoint.model_calls).map_err(|error| {
115 CheckpointError::new("checkpoint_model_calls_invalid", error.to_string())
116 })?,
117 );
118 object.insert(
119 "shared_state".to_string(),
120 Value::Object(checkpoint.shared_state.clone().into_iter().collect()),
121 );
122 object.insert(
123 "budget_usage".to_string(),
124 checkpoint
125 .budget_usage
126 .as_ref()
127 .map(serde_json::to_value)
128 .transpose()
129 .map_err(|error| CheckpointError::new("checkpoint_json_invalid", error.to_string()))?
130 .unwrap_or(Value::Null),
131 );
132 object.insert(
133 "event_cursor".to_string(),
134 checkpoint
135 .event_cursor
136 .as_ref()
137 .map(serde_json::to_value)
138 .transpose()
139 .map_err(|error| CheckpointError::new("checkpoint_json_invalid", error.to_string()))?
140 .unwrap_or(Value::Null),
141 );
142 object.insert(
143 "event_outbox".to_string(),
144 Value::Array(
145 checkpoint
146 .event_outbox
147 .iter()
148 .map(EventOutboxEntry::to_value)
149 .collect(),
150 ),
151 );
152 object.insert(
153 "extension_state".to_string(),
154 Value::Object(
155 checkpoint
156 .extension_state
157 .iter()
158 .map(|(namespace, entry)| (namespace.clone(), entry.to_value()))
159 .collect(),
160 ),
161 );
162 object.insert(
163 "model_call_journal".to_string(),
164 Value::Array(
165 checkpoint
166 .model_call_journal
167 .iter()
168 .map(OperationJournalEntry::to_value)
169 .collect(),
170 ),
171 );
172 object.insert(
173 "tool_journal".to_string(),
174 Value::Array(
175 checkpoint
176 .tool_journal
177 .iter()
178 .map(OperationJournalEntry::to_value)
179 .collect(),
180 ),
181 );
182 object.insert("revision".to_string(), Value::from(checkpoint.revision));
183 object.insert(
184 "claim_token".to_string(),
185 checkpoint
186 .claim_token
187 .clone()
188 .map_or(Value::Null, Value::String),
189 );
190 object.insert(
191 "claimed_cycle".to_string(),
192 checkpoint.claimed_cycle.map_or(Value::Null, Value::from),
193 );
194 object.insert(
195 "lease_expires_at_ms".to_string(),
196 checkpoint
197 .lease_expires_at_ms
198 .map_or(Value::Null, Value::from),
199 );
200 object.insert(
201 "terminal_result".to_string(),
202 checkpoint.terminal_result.clone().unwrap_or(Value::Null),
203 );
204 object.insert(
205 "terminal_acknowledged".to_string(),
206 Value::Bool(checkpoint.terminal_acknowledged),
207 );
208 Ok(Value::Object(object))
209}
210
211pub fn checkpoint_from_value(
212 payload: &Value,
213 max_extension_state_bytes: u64,
214) -> CheckpointResult<Checkpoint> {
215 let object = payload.as_object().ok_or_else(|| {
216 CheckpointError::new(
217 "checkpoint_payload_invalid",
218 "checkpoint v3 payload must be an object",
219 )
220 })?;
221 if let Some(field) = object
222 .keys()
223 .find(|field| !KNOWN_FIELDS.contains(&field.as_str()))
224 {
225 return Err(CheckpointError::new(
226 "checkpoint_unknown_field",
227 format!("checkpoint contains unknown field: {field}"),
228 ));
229 }
230 if object.get("schema_version").and_then(Value::as_str) != Some(CHECKPOINT_SCHEMA) {
231 return Err(CheckpointError::new(
232 "checkpoint_schema_unsupported",
233 "checkpoint schema_version is not vv-agent.checkpoint.v3",
234 ));
235 }
236 let run_definition_schema = required_string(
237 object,
238 "run_definition_schema",
239 "checkpoint_definition_schema_unsupported",
240 )?;
241 if run_definition_schema != RUN_DEFINITION_SCHEMA {
242 return Err(CheckpointError::new(
243 "checkpoint_definition_schema_unsupported",
244 "run_definition_schema is unsupported",
245 ));
246 }
247 let run_definition = object.get("run_definition").cloned().ok_or_else(|| {
248 CheckpointError::new(
249 "checkpoint_definition_invalid",
250 "run_definition is required",
251 )
252 })?;
253 if let Some(field) = KNOWN_FIELDS
254 .iter()
255 .find(|field| !object.contains_key(**field))
256 {
257 return Err(CheckpointError::new(
258 "checkpoint_field_invalid",
259 format!("required checkpoint field {field} is missing"),
260 ));
261 }
262 let checkpoint = Checkpoint {
263 schema_version: CHECKPOINT_SCHEMA.to_string(),
264 run_definition_schema: run_definition_schema.to_string(),
265 run_definition,
266 checkpoint_key: required_string(object, "checkpoint_key", "checkpoint_key_invalid")?
267 .to_string(),
268 task_id: required_string(object, "task_id", "checkpoint_value_invalid")?.to_string(),
269 root_run_id: required_string(object, "root_run_id", "checkpoint_value_invalid")?
270 .to_string(),
271 trace_id: required_string(object, "trace_id", "checkpoint_value_invalid")?.to_string(),
272 run_definition_digest: required_string(
273 object,
274 "run_definition_digest",
275 "checkpoint_digest_invalid",
276 )?
277 .to_string(),
278 resume_attempt: required_u64(
279 object,
280 "resume_attempt",
281 "checkpoint_resume_attempt_invalid",
282 )?,
283 cycle_index: required_u64(object, "cycle_index", "checkpoint_cycle_invalid")?,
284 status: parse_status(object, "status")?,
285 messages: parse_messages(object.get("messages"))?,
286 cycles: parse_cycles(object.get("cycles"))?,
287 model_calls: parse_model_calls(object.get("model_calls"))?,
288 shared_state: parse_object_map(object, "shared_state", "checkpoint_shared_state_invalid")?,
289 budget_usage: parse_optional_budget(object.get("budget_usage"))?,
290 event_cursor: parse_optional(object.get("event_cursor"), "event_cursor")?,
291 event_outbox: parse_array(object, "event_outbox")?
292 .iter()
293 .map(EventOutboxEntry::from_value)
294 .collect::<CheckpointResult<Vec<_>>>()?,
295 extension_state: parse_extensions(object.get("extension_state"))?,
296 model_call_journal: parse_array(object, "model_call_journal")?
297 .iter()
298 .map(OperationJournalEntry::from_value)
299 .collect::<CheckpointResult<Vec<_>>>()?,
300 tool_journal: parse_array(object, "tool_journal")?
301 .iter()
302 .map(OperationJournalEntry::from_value)
303 .collect::<CheckpointResult<Vec<_>>>()?,
304 revision: required_u64(object, "revision", "checkpoint_revision_invalid")?,
305 claim_token: parse_optional_string(object, "claim_token")?,
306 claimed_cycle: parse_optional_u64(object, "claimed_cycle")?,
307 lease_expires_at_ms: parse_optional_u64(object, "lease_expires_at_ms")?,
308 terminal_result: parse_optional_value(object, "terminal_result")?,
309 terminal_acknowledged: object
310 .get("terminal_acknowledged")
311 .and_then(Value::as_bool)
312 .ok_or_else(|| {
313 CheckpointError::new(
314 "checkpoint_status_invalid",
315 "terminal_acknowledged must be a boolean",
316 )
317 })?,
318 };
319 validate_checkpoint(&checkpoint)?;
320 crate::runtime::state::validate_extension_state_size(
321 &checkpoint.extension_state,
322 max_extension_state_bytes,
323 )?;
324 Ok(checkpoint)
325}
326
327pub fn checkpoint_to_json(
328 checkpoint: &Checkpoint,
329 max_extension_state_bytes: u64,
330) -> CheckpointResult<String> {
331 let value = checkpoint_to_value(checkpoint, max_extension_state_bytes)?;
332 let bytes = canonical_json_bytes(&value, "checkpoint v3")?;
333 String::from_utf8(bytes).map_err(|error| {
334 CheckpointError::new(
335 "checkpoint_canonicalization_invalid",
336 format!("checkpoint canonical JSON is not UTF-8: {error}"),
337 )
338 })
339}
340
341pub fn checkpoint_from_json(
342 payload: &str,
343 max_extension_state_bytes: u64,
344) -> CheckpointResult<Checkpoint> {
345 let value = strict_json_value(payload)?;
346 checkpoint_from_value(&value, max_extension_state_bytes)
347}
348
349fn parse_status(object: &Map<String, Value>, field: &str) -> CheckpointResult<CheckpointStatus> {
350 let value = required_string(object, field, "checkpoint_status_invalid")?;
351 serde_json::from_value(Value::String(value.to_string())).map_err(|_| {
352 CheckpointError::new(
353 "checkpoint_status_invalid",
354 format!("unknown checkpoint status {value}"),
355 )
356 })
357}
358
359fn parse_messages(value: Option<&Value>) -> CheckpointResult<Vec<Message>> {
360 let values = value.and_then(Value::as_array).ok_or_else(|| {
361 CheckpointError::new("checkpoint_messages_invalid", "messages must be an array")
362 })?;
363 values
364 .iter()
365 .map(|value| {
366 Message::from_dict(value)
367 .map_err(|error| CheckpointError::new("checkpoint_messages_invalid", error))
368 })
369 .collect()
370}
371
372fn parse_cycles(value: Option<&Value>) -> CheckpointResult<Vec<CycleRecord>> {
373 let values = value.and_then(Value::as_array).ok_or_else(|| {
374 CheckpointError::new("checkpoint_cycles_invalid", "cycles must be an array")
375 })?;
376 values
377 .iter()
378 .map(|value| {
379 CycleRecord::from_dict(value)
380 .map_err(|error| CheckpointError::new("checkpoint_cycles_invalid", error))
381 })
382 .collect()
383}
384
385fn parse_model_calls(value: Option<&Value>) -> CheckpointResult<Vec<ModelCallRecord>> {
386 let values = value.and_then(Value::as_array).ok_or_else(|| {
387 CheckpointError::new(
388 "checkpoint_model_calls_invalid",
389 "model_calls must be an array",
390 )
391 })?;
392 values
393 .iter()
394 .cloned()
395 .map(|value| {
396 serde_json::from_value(value).map_err(|error| {
397 CheckpointError::new("checkpoint_model_calls_invalid", error.to_string())
398 })
399 })
400 .collect()
401}
402
403fn parse_object_map(
404 object: &Map<String, Value>,
405 field: &str,
406 code: &str,
407) -> CheckpointResult<BTreeMap<String, Value>> {
408 object
409 .get(field)
410 .and_then(Value::as_object)
411 .cloned()
412 .map(|map| map.into_iter().collect())
413 .ok_or_else(|| CheckpointError::new(code, format!("{field} must be an object")))
414}
415
416fn parse_optional_budget(value: Option<&Value>) -> CheckpointResult<Option<BudgetUsageSnapshot>> {
417 match value {
418 None | Some(Value::Null) => Ok(None),
419 Some(value) => serde_json::from_value(value.clone())
420 .map(Some)
421 .map_err(|error| {
422 CheckpointError::new("checkpoint_budget_usage_invalid", error.to_string())
423 }),
424 }
425}
426
427fn parse_optional<T>(value: Option<&Value>, field: &str) -> CheckpointResult<Option<T>>
428where
429 T: serde::de::DeserializeOwned,
430{
431 match value {
432 None | Some(Value::Null) => Ok(None),
433 Some(value) => serde_json::from_value(value.clone())
434 .map(Some)
435 .map_err(|error| {
436 CheckpointError::new("checkpoint_field_invalid", format!("{field}: {error}"))
437 }),
438 }
439}
440
441fn parse_extensions(
442 value: Option<&Value>,
443) -> CheckpointResult<BTreeMap<String, ExtensionStateEntry>> {
444 let object = value.and_then(Value::as_object).ok_or_else(|| {
445 CheckpointError::new(
446 "checkpoint_extension_state_invalid",
447 "extension_state must be an object",
448 )
449 })?;
450 object
451 .iter()
452 .map(|(namespace, value)| Ok((namespace.clone(), ExtensionStateEntry::from_value(value)?)))
453 .collect()
454}
455
456fn parse_optional_string(
457 object: &Map<String, Value>,
458 field: &str,
459) -> CheckpointResult<Option<String>> {
460 match object.get(field) {
461 None | Some(Value::Null) => Ok(None),
462 Some(Value::String(value)) => Ok(Some(value.clone())),
463 Some(_) => Err(CheckpointError::new(
464 "checkpoint_claim_invalid",
465 format!("{field} must be a string or null"),
466 )),
467 }
468}
469
470fn parse_optional_u64(object: &Map<String, Value>, field: &str) -> CheckpointResult<Option<u64>> {
471 match object.get(field) {
472 None | Some(Value::Null) => Ok(None),
473 Some(Value::Number(number)) => number.as_u64().map(Some).ok_or_else(|| {
474 CheckpointError::new(
475 "checkpoint_integer_invalid",
476 format!("{field} must be a non-negative integer"),
477 )
478 }),
479 Some(_) => Err(CheckpointError::new(
480 "checkpoint_integer_invalid",
481 format!("{field} must be an integer or null"),
482 )),
483 }
484}
485
486fn parse_optional_value(
487 object: &Map<String, Value>,
488 field: &str,
489) -> CheckpointResult<Option<Value>> {
490 Ok(object.get(field).filter(|value| !value.is_null()).cloned())
491}
492
493fn parse_array<'a>(
494 object: &'a Map<String, Value>,
495 field: &str,
496) -> CheckpointResult<&'a Vec<Value>> {
497 object.get(field).and_then(Value::as_array).ok_or_else(|| {
498 CheckpointError::new(
499 "checkpoint_field_invalid",
500 format!("{field} must be an array"),
501 )
502 })
503}
504
505fn required_string<'a>(
506 object: &'a Map<String, Value>,
507 field: &str,
508 code: &str,
509) -> CheckpointResult<&'a str> {
510 object
511 .get(field)
512 .and_then(Value::as_str)
513 .ok_or_else(|| CheckpointError::new(code, format!("{field} must be a string")))
514}
515
516fn required_u64(object: &Map<String, Value>, field: &str, code: &str) -> CheckpointResult<u64> {
517 object.get(field).and_then(Value::as_u64).ok_or_else(|| {
518 CheckpointError::new(code, format!("{field} must be a non-negative integer"))
519 })
520}
521
522fn strict_json_value(payload: &str) -> CheckpointResult<Value> {
523 let mut deserializer = serde_json::Deserializer::from_str(payload);
524 let StrictValue(value) = StrictValue::deserialize(&mut deserializer)
525 .map_err(|error| CheckpointError::new("checkpoint_json_invalid", error.to_string()))?;
526 deserializer
527 .end()
528 .map_err(|error| CheckpointError::new("checkpoint_json_invalid", error.to_string()))?;
529 Ok(value)
530}
531
532struct StrictValue(Value);
533
534impl<'de> Deserialize<'de> for StrictValue {
535 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
536 where
537 D: serde::Deserializer<'de>,
538 {
539 deserializer.deserialize_any(StrictValueVisitor)
540 }
541}
542
543struct StrictValueVisitor;
544
545impl<'de> Visitor<'de> for StrictValueVisitor {
546 type Value = StrictValue;
547
548 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
549 formatter.write_str("a JSON value without duplicate object keys")
550 }
551
552 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
553 Ok(StrictValue(Value::Bool(value)))
554 }
555
556 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
557 Ok(StrictValue(Value::Number(Number::from(value))))
558 }
559
560 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
561 Ok(StrictValue(Value::Number(Number::from(value))))
562 }
563
564 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
565 where
566 E: de::Error,
567 {
568 Number::from_f64(value)
569 .map(Value::Number)
570 .map(StrictValue)
571 .ok_or_else(|| E::custom("non-finite JSON number"))
572 }
573
574 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
575 Ok(StrictValue(Value::String(value.to_string())))
576 }
577
578 fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
579 Ok(StrictValue(Value::String(value)))
580 }
581
582 fn visit_none<E>(self) -> Result<Self::Value, E> {
583 Ok(StrictValue(Value::Null))
584 }
585
586 fn visit_unit<E>(self) -> Result<Self::Value, E> {
587 Ok(StrictValue(Value::Null))
588 }
589
590 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
591 where
592 A: SeqAccess<'de>,
593 {
594 let mut values = Vec::with_capacity(sequence.size_hint().unwrap_or(0));
595 while let Some(StrictValue(value)) = sequence.next_element()? {
596 values.push(value);
597 }
598 Ok(StrictValue(Value::Array(values)))
599 }
600
601 fn visit_map<A>(self, mut object: A) -> Result<Self::Value, A::Error>
602 where
603 A: MapAccess<'de>,
604 {
605 let mut values = Map::new();
606 while let Some(key) = object.next_key::<String>()? {
607 if values.contains_key(&key) {
608 return Err(de::Error::custom(format!(
609 "duplicate JSON object key {key}"
610 )));
611 }
612 let StrictValue(value) = object.next_value()?;
613 values.insert(key, value);
614 }
615 Ok(StrictValue(Value::Object(values)))
616 }
617}