1use super::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum ContextCheckpointTaskStatusV1 {
9 Planned,
11 InProgress,
13 Blocked,
15 Review,
17 Completed,
19 Abandoned,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum ContextCheckpointDecisionStatusV1 {
27 Proposed,
29 Accepted,
31 Rejected,
33 Superseded,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum ContextCheckpointFileRoleV1 {
41 Input,
43 Reference,
45 Modified,
47 Created,
49 Deleted,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
58#[serde(deny_unknown_fields)]
59pub struct ContextCheckpointSessionStateV1 {
60 pub identity: SessionIdentityV1,
62 pub state: ContextSessionStateV1,
64}
65
66impl ContextCheckpointSessionStateV1 {
67 pub fn try_new(
69 identity: SessionIdentityV1,
70 state: ContextSessionStateV1,
71 ) -> Result<Self, ValidationError> {
72 let checkpoint_state = Self { identity, state };
73 checkpoint_state.validate()?;
74 Ok(checkpoint_state)
75 }
76
77 pub fn validate(&self) -> Result<(), ValidationError> {
79 self.identity.validate()?;
80 validate_checkpoint_identifier(self.identity.session_id.as_str(), "session_id")?;
81 validate_checkpoint_identifier(self.identity.task_id.as_str(), "session task_id")?;
82 validate_checkpoint_identifier(self.identity.run_id.as_str(), "session run_id")?;
83 validate_checkpoint_identifier(self.identity.trace_id.as_str(), "session trace_id")?;
84 validate_checkpoint_identifier(self.identity.agent_id.as_str(), "session agent_id")?;
85 validate_checkpoint_identifier(self.identity.project_id.as_str(), "session project_id")?;
86 if let Some(parent_task_id) = &self.identity.parent_task_id {
87 validate_checkpoint_identifier(parent_task_id.as_str(), "session parent_task_id")?;
88 }
89 if let Some(active_plan_id) = &self.state.active_plan_id {
90 validate_checkpoint_identifier(active_plan_id.as_str(), "session active_plan_id")?;
91 }
92 if let Some(receipt_id) = &self.state.receipt_id {
93 validate_checkpoint_identifier(receipt_id.as_str(), "session receipt_id")?;
94 }
95 validate_checkpoint_reference(
96 self.identity.project_root_ref.as_str(),
97 "session project_root_ref",
98 )?;
99 if let Some(reference) = &self.identity.project_revision_ref {
100 validate_checkpoint_reference(reference.as_str(), "session project_revision_ref")?;
101 }
102 self.state.validate()?;
103 if let Some(abort_reason) = &self.state.abort_reason {
104 validate_checkpoint_reference(abort_reason.as_str(), "session abort_reason")?;
105 }
106 Ok(())
107 }
108}
109
110validated_deserialize!(ContextCheckpointSessionStateV1 {
111 identity: SessionIdentityV1,
112 state: ContextSessionStateV1,
113});
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
117#[serde(deny_unknown_fields)]
118pub struct ContextCheckpointLearningStateV1 {
119 pub state_ref: ProtocolReference,
121 pub state_digest: Sha256Digest,
123 pub revision: u64,
125}
126
127impl ContextCheckpointLearningStateV1 {
128 pub fn try_new(
130 state_ref: ProtocolReference,
131 state_digest: Sha256Digest,
132 revision: u64,
133 ) -> Result<Self, ValidationError> {
134 let state = Self {
135 state_ref,
136 state_digest,
137 revision,
138 };
139 state.validate()?;
140 Ok(state)
141 }
142
143 pub fn validate(&self) -> Result<(), ValidationError> {
145 validate_checkpoint_reference(self.state_ref.as_str(), "learning state_ref")
146 }
147}
148
149validated_deserialize!(ContextCheckpointLearningStateV1 {
150 state_ref: ProtocolReference,
151 state_digest: Sha256Digest,
152 revision: u64,
153});
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
160#[serde(rename_all = "snake_case")]
161pub enum ContextCheckpointEncryptionAlgorithmV1 {
162 Aes256Gcm,
164 XChaCha20Poly1305,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
170#[serde(deny_unknown_fields)]
171pub struct ContextCheckpointEncryptionMetadataV1 {
172 pub algorithm: ContextCheckpointEncryptionAlgorithmV1,
174 pub key_ref: ProtocolReference,
176 pub ciphertext_digest: Sha256Digest,
178}
179
180impl ContextCheckpointEncryptionMetadataV1 {
181 pub fn try_new(
183 algorithm: ContextCheckpointEncryptionAlgorithmV1,
184 key_ref: ProtocolReference,
185 ciphertext_digest: Sha256Digest,
186 ) -> Result<Self, ValidationError> {
187 let metadata = Self {
188 algorithm,
189 key_ref,
190 ciphertext_digest,
191 };
192 metadata.validate()?;
193 Ok(metadata)
194 }
195
196 pub fn validate(&self) -> Result<(), ValidationError> {
198 validate_checkpoint_reference(self.key_ref.as_str(), "encryption key_ref")
199 }
200}
201
202validated_deserialize!(ContextCheckpointEncryptionMetadataV1 {
203 algorithm: ContextCheckpointEncryptionAlgorithmV1,
204 key_ref: ProtocolReference,
205 ciphertext_digest: Sha256Digest,
206});
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
210#[serde(deny_unknown_fields)]
211pub struct ContextCheckpointIdentityV1 {
212 pub checkpoint_id: ContextCheckpointIdV1,
214 pub parent_checkpoint_id: Option<ContextCheckpointIdV1>,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub parent_device_id: Option<ContextCheckpointDeviceIdV1>,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub parent_branch_id: Option<ContextCheckpointBranchIdV1>,
222 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub parent_device_sequence: Option<u64>,
225 pub branch_id: ContextCheckpointBranchIdV1,
227 pub device_id: ContextCheckpointDeviceIdV1,
229 pub device_sequence: u64,
231}
232
233impl ContextCheckpointIdentityV1 {
234 pub fn try_new(
236 checkpoint_id: ContextCheckpointIdV1,
237 parent_checkpoint_id: Option<ContextCheckpointIdV1>,
238 parent_device_id: Option<ContextCheckpointDeviceIdV1>,
239 parent_branch_id: Option<ContextCheckpointBranchIdV1>,
240 parent_device_sequence: Option<u64>,
241 branch_id: ContextCheckpointBranchIdV1,
242 device_id: ContextCheckpointDeviceIdV1,
243 device_sequence: u64,
244 ) -> Result<Self, ValidationError> {
245 let identity = Self {
246 checkpoint_id,
247 parent_checkpoint_id,
248 parent_device_id,
249 parent_branch_id,
250 parent_device_sequence,
251 branch_id,
252 device_id,
253 device_sequence,
254 };
255 identity.validate()?;
256 Ok(identity)
257 }
258
259 pub fn validate(&self) -> Result<(), ValidationError> {
261 if self.device_sequence == 0 {
262 return Err(ValidationError::new(
263 "ContextCheckpointIdentityV1 device_sequence must start at 1",
264 ));
265 }
266 match (
267 &self.parent_checkpoint_id,
268 &self.parent_device_id,
269 &self.parent_branch_id,
270 self.parent_device_sequence,
271 ) {
272 (Some(parent), Some(_), Some(_), Some(_)) if parent == &self.checkpoint_id => {
273 return Err(ValidationError::new(
274 "ContextCheckpointIdentityV1 parent_checkpoint_id must differ from checkpoint_id",
275 ));
276 }
277 (Some(_), Some(parent_device), Some(_), Some(parent_sequence)) => {
278 if parent_sequence == 0 {
279 return Err(ValidationError::new(
280 "ContextCheckpointIdentityV1 parent_device_sequence must start at 1",
281 ));
282 }
283 let expected = parent_sequence.checked_add(1).ok_or_else(|| {
284 ValidationError::new(
285 "ContextCheckpointIdentityV1 parent_device_sequence cannot overflow",
286 )
287 })?;
288 if parent_device == &self.device_id && self.device_sequence != expected {
289 return Err(ValidationError::new(
290 "ContextCheckpointIdentityV1 same-device sequence must directly follow parent",
291 ));
292 }
293 }
294 (None, None, None, None) if self.device_sequence != 1 => {
295 return Err(ValidationError::new(
296 "ContextCheckpointIdentityV1 root checkpoints must use device_sequence 1",
297 ));
298 }
299 (None, None, None, None) => {}
300 _ => {
301 return Err(ValidationError::new(
302 "ContextCheckpointIdentityV1 parent identity fields must be all present or all absent",
303 ));
304 }
305 }
306 Ok(())
307 }
308}
309
310validated_deserialize!(ContextCheckpointIdentityV1 {
311 checkpoint_id: ContextCheckpointIdV1,
312 parent_checkpoint_id: Option<ContextCheckpointIdV1>,
313 parent_device_id: Option<ContextCheckpointDeviceIdV1>,
314 parent_branch_id: Option<ContextCheckpointBranchIdV1>,
315 parent_device_sequence: Option<u64>,
316 branch_id: ContextCheckpointBranchIdV1,
317 device_id: ContextCheckpointDeviceIdV1,
318 device_sequence: u64,
319});
320
321#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
323#[serde(deny_unknown_fields)]
324pub struct ContextCheckpointLineageV1 {
325 pub project_id: ProjectId,
327 pub workspace_id: WorkspaceId,
329 pub tenant_id: TenantId,
331 pub task_id: TaskId,
333 pub plan_id: Option<PlanId>,
335 pub receipt_ids: Vec<ReceiptId>,
337 pub context_ir_digest: Option<Sha256Digest>,
339 pub hosted_index_digest: Option<Sha256Digest>,
341 pub evidence_refs: Vec<ProtocolReference>,
343 pub knowledge_refs: Vec<ProtocolReference>,
345 pub gotcha_refs: Vec<ProtocolReference>,
347 pub snapshot_refs: Vec<ProtocolReference>,
349}
350
351impl ContextCheckpointLineageV1 {
352 pub fn validate(&self) -> Result<(), ValidationError> {
354 validate_checkpoint_identifier(self.project_id.as_str(), "project_id")?;
355 validate_checkpoint_identifier(self.workspace_id.as_str(), "workspace_id")?;
356 validate_checkpoint_identifier(self.tenant_id.as_str(), "tenant_id")?;
357 validate_checkpoint_identifier(self.task_id.as_str(), "task_id")?;
358 if let Some(plan_id) = &self.plan_id {
359 validate_checkpoint_identifier(plan_id.as_str(), "plan_id")?;
360 }
361 if self.receipt_ids.len() > MAX_CONTEXT_CHECKPOINT_LINEAGE_REFS {
362 return Err(ValidationError::new(format!(
363 "receipt_ids exceeds the {MAX_CONTEXT_CHECKPOINT_LINEAGE_REFS} item limit"
364 )));
365 }
366 for receipt_id in &self.receipt_ids {
367 validate_checkpoint_identifier(receipt_id.as_str(), "receipt_ids")?;
368 }
369 require_sorted_unique(&self.receipt_ids, "receipt_ids", ReceiptId::as_str)?;
370 for (refs, field) in [
371 (&self.evidence_refs, "evidence_refs"),
372 (&self.knowledge_refs, "knowledge_refs"),
373 (&self.gotcha_refs, "gotcha_refs"),
374 (&self.snapshot_refs, "snapshot_refs"),
375 ] {
376 validate_reference_list(refs, field)?;
377 }
378 Ok(())
379 }
380}
381
382validated_deserialize!(ContextCheckpointLineageV1 {
383 project_id: ProjectId,
384 workspace_id: WorkspaceId,
385 tenant_id: TenantId,
386 task_id: TaskId,
387 plan_id: Option<PlanId>,
388 receipt_ids: Vec<ReceiptId>,
389 context_ir_digest: Option<Sha256Digest>,
390 hosted_index_digest: Option<Sha256Digest>,
391 evidence_refs: Vec<ProtocolReference>,
392 knowledge_refs: Vec<ProtocolReference>,
393 gotcha_refs: Vec<ProtocolReference>,
394 snapshot_refs: Vec<ProtocolReference>,
395});
396
397#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
406#[serde(deny_unknown_fields)]
407pub struct ContextCheckpointArtifactLineageV1 {
408 pub task_ref: Sha256Digest,
410 pub plan_ref: Option<Sha256Digest>,
412 pub receipt_refs: Vec<Sha256Digest>,
414}
415
416impl ContextCheckpointArtifactLineageV1 {
417 pub fn validate(&self) -> Result<(), ValidationError> {
419 if self.plan_ref.is_none() && !self.receipt_refs.is_empty() {
420 return Err(ValidationError::new(
421 "receipt_refs require an explicit plan_ref",
422 ));
423 }
424 if self.receipt_refs.len() > MAX_CONTEXT_CHECKPOINT_LINEAGE_REFS {
425 return Err(ValidationError::new(format!(
426 "receipt_refs exceeds the {MAX_CONTEXT_CHECKPOINT_LINEAGE_REFS} item limit"
427 )));
428 }
429 require_sorted_unique(&self.receipt_refs, "receipt_refs", Sha256Digest::as_str)
430 }
431}
432
433validated_deserialize!(ContextCheckpointArtifactLineageV1 {
434 task_ref: Sha256Digest,
435 plan_ref: Option<Sha256Digest>,
436 receipt_refs: Vec<Sha256Digest>,
437});
438
439#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
442#[serde(deny_unknown_fields)]
443pub struct ContextCheckpointLineageV2 {
444 pub project_id: ProjectId,
445 pub workspace_id: WorkspaceId,
446 pub tenant_id: TenantId,
447 pub task_id: TaskId,
448 pub plan_id: Option<PlanId>,
449 pub receipt_ids: Vec<ReceiptId>,
450 pub artifact_lineage: ContextCheckpointArtifactLineageV1,
451 pub context_ir_digest: Option<Sha256Digest>,
452 pub hosted_index_digest: Option<Sha256Digest>,
453 pub evidence_refs: Vec<ProtocolReference>,
454 pub knowledge_refs: Vec<ProtocolReference>,
455 pub gotcha_refs: Vec<ProtocolReference>,
456 pub snapshot_refs: Vec<ProtocolReference>,
457}
458
459impl ContextCheckpointLineageV2 {
460 pub fn legacy(&self) -> ContextCheckpointLineageV1 {
462 ContextCheckpointLineageV1 {
463 project_id: self.project_id.clone(),
464 workspace_id: self.workspace_id.clone(),
465 tenant_id: self.tenant_id.clone(),
466 task_id: self.task_id.clone(),
467 plan_id: self.plan_id.clone(),
468 receipt_ids: self.receipt_ids.clone(),
469 context_ir_digest: self.context_ir_digest.clone(),
470 hosted_index_digest: self.hosted_index_digest.clone(),
471 evidence_refs: self.evidence_refs.clone(),
472 knowledge_refs: self.knowledge_refs.clone(),
473 gotcha_refs: self.gotcha_refs.clone(),
474 snapshot_refs: self.snapshot_refs.clone(),
475 }
476 }
477
478 pub fn validate(&self) -> Result<(), ValidationError> {
480 self.legacy().validate()?;
481 self.artifact_lineage.validate()?;
482 if self.plan_id.is_some() != self.artifact_lineage.plan_ref.is_some() {
483 return Err(ValidationError::new(
484 "plan_id and artifact_lineage.plan_ref must be present together",
485 ));
486 }
487 if self.plan_id.is_none()
488 && (!self.receipt_ids.is_empty() || !self.artifact_lineage.receipt_refs.is_empty())
489 {
490 return Err(ValidationError::new(
491 "receipts require paired plan_id and plan_ref",
492 ));
493 }
494 if self.receipt_ids.len() != self.artifact_lineage.receipt_refs.len() {
495 return Err(ValidationError::new(
496 "receipt_ids and artifact_lineage.receipt_refs must pair exactly",
497 ));
498 }
499 Ok(())
500 }
501}
502
503validated_deserialize!(ContextCheckpointLineageV2 {
504 project_id: ProjectId,
505 workspace_id: WorkspaceId,
506 tenant_id: TenantId,
507 task_id: TaskId,
508 plan_id: Option<PlanId>,
509 receipt_ids: Vec<ReceiptId>,
510 artifact_lineage: ContextCheckpointArtifactLineageV1,
511 context_ir_digest: Option<Sha256Digest>,
512 hosted_index_digest: Option<Sha256Digest>,
513 evidence_refs: Vec<ProtocolReference>,
514 knowledge_refs: Vec<ProtocolReference>,
515 gotcha_refs: Vec<ProtocolReference>,
516 snapshot_refs: Vec<ProtocolReference>,
517});
518
519#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
521#[serde(deny_unknown_fields)]
522pub struct ContextCheckpointTaskV1 {
523 pub task_id: TaskId,
525 pub title: ContextCheckpointTextV1,
527 pub status: ContextCheckpointTaskStatusV1,
529 pub plan_id: Option<PlanId>,
531}
532
533impl ContextCheckpointTaskV1 {
534 pub fn validate(&self) -> Result<(), ValidationError> {
536 validate_checkpoint_identifier(self.task_id.as_str(), "task.task_id")?;
537 if let Some(plan_id) = &self.plan_id {
538 validate_checkpoint_identifier(plan_id.as_str(), "task.plan_id")?;
539 }
540 Ok(())
541 }
542}
543
544validated_deserialize!(ContextCheckpointTaskV1 {
545 task_id: TaskId,
546 title: ContextCheckpointTextV1,
547 status: ContextCheckpointTaskStatusV1,
548 plan_id: Option<PlanId>,
549});
550
551#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
553#[serde(deny_unknown_fields)]
554pub struct ContextCheckpointProgressV1 {
555 pub completed_steps: u32,
557 pub total_steps: u32,
559 #[serde(deserialize_with = "deserialize_milliunit")]
561 pub confidence_milliunits: u16,
562 pub summary: ContextCheckpointTextV1,
564}
565
566impl ContextCheckpointProgressV1 {
567 pub fn validate(&self) -> Result<(), ValidationError> {
569 if self.total_steps == 0 || self.total_steps > MAX_CONTEXT_CHECKPOINT_STEPS {
570 return Err(ValidationError::new(format!(
571 "total_steps must be within 1..={MAX_CONTEXT_CHECKPOINT_STEPS}"
572 )));
573 }
574 if self.completed_steps > self.total_steps {
575 return Err(ValidationError::new(
576 "completed_steps must not exceed total_steps",
577 ));
578 }
579 validate_milliunit(self.confidence_milliunits, "confidence_milliunits")
580 }
581}
582
583validated_deserialize!(ContextCheckpointProgressV1 {
584 completed_steps: u32,
585 total_steps: u32,
586 confidence_milliunits: u16,
587 summary: ContextCheckpointTextV1,
588});
589
590#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
592#[serde(deny_unknown_fields)]
593pub struct ContextCheckpointDecisionV1 {
594 pub decision_id: DecisionId,
596 pub statement: ContextCheckpointTextV1,
598 pub rationale: ContextCheckpointTextV1,
600 pub status: ContextCheckpointDecisionStatusV1,
602 pub evidence_refs: Vec<ProtocolReference>,
604}
605
606impl ContextCheckpointDecisionV1 {
607 pub fn validate(&self) -> Result<(), ValidationError> {
609 validate_checkpoint_identifier(self.decision_id.as_str(), "decision_id")?;
610 if self.evidence_refs.len() > MAX_CONTEXT_CHECKPOINT_DECISION_REFS {
611 return Err(ValidationError::new(format!(
612 "decision evidence_refs exceeds the {MAX_CONTEXT_CHECKPOINT_DECISION_REFS} item limit"
613 )));
614 }
615 for reference in &self.evidence_refs {
616 validate_checkpoint_reference(reference.as_str(), "decision evidence_refs")?;
617 }
618 require_sorted_unique(
619 &self.evidence_refs,
620 "decision evidence_refs",
621 ProtocolReference::as_str,
622 )
623 }
624}
625
626validated_deserialize!(ContextCheckpointDecisionV1 {
627 decision_id: DecisionId,
628 statement: ContextCheckpointTextV1,
629 rationale: ContextCheckpointTextV1,
630 status: ContextCheckpointDecisionStatusV1,
631 evidence_refs: Vec<ProtocolReference>,
632});
633
634#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
636#[serde(deny_unknown_fields)]
637pub struct ContextCheckpointFileV1 {
638 pub source_id: SourceId,
640 pub role: ContextCheckpointFileRoleV1,
642 pub content_digest: Sha256Digest,
644}
645
646impl ContextCheckpointFileV1 {
647 pub fn validate(&self) -> Result<(), ValidationError> {
649 validate_checkpoint_identifier(self.source_id.as_str(), "file source_id")
650 }
651}
652
653validated_deserialize!(ContextCheckpointFileV1 {
654 source_id: SourceId,
655 role: ContextCheckpointFileRoleV1,
656 content_digest: Sha256Digest,
657});
658
659#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
661#[serde(deny_unknown_fields)]
662pub struct ContextCheckpointPolicyPinV1 {
663 pub policy_id: PolicyId,
665 pub policy_digest: Sha256Digest,
667}
668
669impl ContextCheckpointPolicyPinV1 {
670 pub fn validate(&self) -> Result<(), ValidationError> {
672 validate_checkpoint_identifier(self.policy_id.as_str(), "policy_pins.policy_id")
673 }
674}
675
676validated_deserialize!(ContextCheckpointPolicyPinV1 {
677 policy_id: PolicyId,
678 policy_digest: Sha256Digest,
679});
680
681#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
683#[serde(deny_unknown_fields)]
684pub struct ContextCheckpointPackagePinV1 {
685 pub package_id: PackageId,
687 pub version: SemanticVersion,
689 pub package_digest: Sha256Digest,
691}
692
693impl ContextCheckpointPackagePinV1 {
694 pub fn validate(&self) -> Result<(), ValidationError> {
696 validate_checkpoint_identifier(self.package_id.as_str(), "package_pins.package_id")
697 }
698}
699
700validated_deserialize!(ContextCheckpointPackagePinV1 {
701 package_id: PackageId,
702 version: SemanticVersion,
703 package_digest: Sha256Digest,
704});
705
706#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
708#[serde(deny_unknown_fields)]
709pub struct ContextCheckpointLiveStateV1 {
710 pub schema_version: u32,
712 pub task: ContextCheckpointTaskV1,
714 pub progress: ContextCheckpointProgressV1,
716 pub decisions: Vec<ContextCheckpointDecisionV1>,
718 pub findings: Vec<ContextCheckpointTextV1>,
720 pub next_steps: Vec<ContextCheckpointTextV1>,
722 pub handoff_summary: ContextCheckpointTextV1,
724 pub files: Vec<ContextCheckpointFileV1>,
726 pub profile_id: Option<ProfileId>,
728 pub policy_pins: Vec<ContextCheckpointPolicyPinV1>,
730 pub package_pins: Vec<ContextCheckpointPackagePinV1>,
732 #[serde(default, skip_serializing_if = "Option::is_none")]
734 pub session_state: Option<ContextCheckpointSessionStateV1>,
735 #[serde(default, skip_serializing_if = "Option::is_none")]
737 pub learning_state: Option<ContextCheckpointLearningStateV1>,
738}
739
740impl ContextCheckpointLiveStateV1 {
741 pub const SCHEMA_VERSION: u32 = 1;
743
744 pub fn validate(&self) -> Result<(), ValidationError> {
746 validate_schema_version(self.schema_version)?;
747 self.task.validate()?;
748 self.progress.validate()?;
749
750 require_capacity(
751 self.decisions.len(),
752 MAX_CONTEXT_CHECKPOINT_DECISIONS,
753 "decisions",
754 )?;
755 for decision in &self.decisions {
756 decision.validate()?;
757 }
758 require_sorted_unique(&self.decisions, "decisions", |decision| {
759 decision.decision_id.as_str()
760 })?;
761
762 require_capacity(
763 self.findings.len(),
764 MAX_CONTEXT_CHECKPOINT_FINDINGS,
765 "findings",
766 )?;
767 require_unique(&self.findings, "findings", ContextCheckpointTextV1::as_str)?;
768 require_capacity(
769 self.next_steps.len(),
770 MAX_CONTEXT_CHECKPOINT_NEXT_STEPS,
771 "next_steps",
772 )?;
773 require_unique(
774 &self.next_steps,
775 "next_steps",
776 ContextCheckpointTextV1::as_str,
777 )?;
778
779 require_capacity(self.files.len(), MAX_CONTEXT_CHECKPOINT_FILES, "files")?;
780 for file in &self.files {
781 file.validate()?;
782 }
783 require_sorted_unique(&self.files, "files", |file| file.source_id.as_str())?;
784
785 if let Some(profile_id) = &self.profile_id {
786 validate_checkpoint_identifier(profile_id.as_str(), "profile_id")?;
787 }
788
789 require_capacity(
790 self.policy_pins.len(),
791 MAX_CONTEXT_CHECKPOINT_POLICY_PINS,
792 "policy_pins",
793 )?;
794 for pin in &self.policy_pins {
795 pin.validate()?;
796 }
797 require_sorted_unique(&self.policy_pins, "policy_pins", |pin| {
798 pin.policy_id.as_str()
799 })?;
800
801 require_capacity(
802 self.package_pins.len(),
803 MAX_CONTEXT_CHECKPOINT_PACKAGE_PINS,
804 "package_pins",
805 )?;
806 for pin in &self.package_pins {
807 pin.validate()?;
808 }
809 require_sorted_unique(&self.package_pins, "package_pins", |pin| {
810 pin.package_id.as_str()
811 })?;
812 if let Some(session_state) = &self.session_state {
813 session_state.validate()?;
814 }
815 if let Some(learning_state) = &self.learning_state {
816 learning_state.validate()?;
817 }
818 Ok(())
819 }
820}
821
822validated_deserialize!(ContextCheckpointLiveStateV1 {
823 schema_version: u32,
824 task: ContextCheckpointTaskV1,
825 progress: ContextCheckpointProgressV1,
826 decisions: Vec<ContextCheckpointDecisionV1>,
827 findings: Vec<ContextCheckpointTextV1>,
828 next_steps: Vec<ContextCheckpointTextV1>,
829 handoff_summary: ContextCheckpointTextV1,
830 files: Vec<ContextCheckpointFileV1>,
831 profile_id: Option<ProfileId>,
832 policy_pins: Vec<ContextCheckpointPolicyPinV1>,
833 package_pins: Vec<ContextCheckpointPackagePinV1>,
834 session_state: Option<ContextCheckpointSessionStateV1>,
835 learning_state: Option<ContextCheckpointLearningStateV1>,
836});
837
838#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
845#[serde(deny_unknown_fields)]
846pub struct ContextCheckpointCarrierBindingV1 {
847 pub carrier_schema_id: String,
849 pub logical_state_schema_id: String,
851 pub workspace_id: WorkspaceId,
853 pub state_digest: Sha256Digest,
855 pub envelope_digest: Sha256Digest,
857}
858
859impl ContextCheckpointCarrierBindingV1 {
860 pub fn try_new(
862 workspace_id: WorkspaceId,
863 state_digest: Sha256Digest,
864 envelope_digest: Sha256Digest,
865 ) -> Result<Self, ValidationError> {
866 let binding = Self {
867 carrier_schema_id: CONTEXT_CHECKPOINT_CARRIER_SCHEMA_ID.to_owned(),
868 logical_state_schema_id: CONTEXT_CHECKPOINT_CARRIER_LOGICAL_STATE_SCHEMA_ID.to_owned(),
869 workspace_id,
870 state_digest,
871 envelope_digest,
872 };
873 binding.validate()?;
874 Ok(binding)
875 }
876
877 pub fn validate(&self) -> Result<(), ValidationError> {
879 if self.carrier_schema_id != CONTEXT_CHECKPOINT_CARRIER_SCHEMA_ID {
880 return Err(ValidationError::new(format!(
881 "carrier_schema_id must be {CONTEXT_CHECKPOINT_CARRIER_SCHEMA_ID}"
882 )));
883 }
884 if self.logical_state_schema_id != CONTEXT_CHECKPOINT_CARRIER_LOGICAL_STATE_SCHEMA_ID {
885 return Err(ValidationError::new(format!(
886 "logical_state_schema_id must be {CONTEXT_CHECKPOINT_CARRIER_LOGICAL_STATE_SCHEMA_ID}"
887 )));
888 }
889 validate_canonical_uuid(self.workspace_id.as_str(), "carrier workspace_id")
891 }
892}
893
894validated_deserialize!(ContextCheckpointCarrierBindingV1 {
895 carrier_schema_id: String,
896 logical_state_schema_id: String,
897 workspace_id: WorkspaceId,
898 state_digest: Sha256Digest,
899 envelope_digest: Sha256Digest,
900});