lean_ctx_protocol/
decision.rs1use crate::common::{
4 DecisionId, PlanId, TaskId, ValidationError, deserialize_schema_version,
5 validate_schema_version,
6};
7use crate::evidence::EvidenceRefV1;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum DecisionKind {
15 Routing,
16 Scheduling,
17 CapabilitySelection,
18 ContextSelection,
19 Policy,
20 Fallback,
21 Retry,
22 Stop,
23 Other,
24}
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28#[serde(deny_unknown_fields)]
29pub struct DecisionRecordV1 {
30 #[serde(deserialize_with = "deserialize_schema_version")]
31 pub schema_version: u32,
32 pub decision_id: DecisionId,
33 pub task_id: TaskId,
34 pub plan_id: PlanId,
35 pub decision_kind: DecisionKind,
36 pub input_refs: Vec<String>,
37 pub constraint_refs: Vec<String>,
38 pub selected_result: Value,
39 pub rationale_code: String,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub rationale_ref: Option<String>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub policy_ref: Option<String>,
44 pub decision_system_name: String,
45 pub decision_system_version: String,
46 pub evidence_refs: Vec<EvidenceRefV1>,
47 pub observed_at: String,
48 pub signature: String,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub supersedes: Option<DecisionId>,
51}
52
53impl DecisionRecordV1 {
54 pub fn validate(&self) -> Result<(), ValidationError> {
56 validate_schema_version(self.schema_version)
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 fn id<T>(value: &str) -> T
65 where
66 T: TryFrom<String>,
67 <T as TryFrom<String>>::Error: std::fmt::Debug,
68 {
69 T::try_from(value.to_owned()).expect("identifier should be valid")
70 }
71
72 #[test]
73 fn serialization_round_trip() {
74 let decision = DecisionRecordV1 {
75 schema_version: 1,
76 decision_id: id("decision-1"),
77 task_id: id("task-1"),
78 plan_id: id("plan-1"),
79 decision_kind: DecisionKind::Routing,
80 input_refs: vec!["input:task".to_owned()],
81 constraint_refs: vec!["constraint:budget".to_owned()],
82 selected_result: serde_json::json!({ "model": "model-1" }),
83 rationale_code: "lowest_cost_within_slo".to_owned(),
84 rationale_ref: Some("rationale:1".to_owned()),
85 policy_ref: Some("policy:model".to_owned()),
86 decision_system_name: "scheduler".to_owned(),
87 decision_system_version: "1.0.0".to_owned(),
88 evidence_refs: vec![],
89 observed_at: "2026-08-09T12:00:00Z".to_owned(),
90 signature: "signature".to_owned(),
91 supersedes: None,
92 };
93 let json = serde_json::to_string(&decision).expect("decision should serialize");
94 let decoded: DecisionRecordV1 =
95 serde_json::from_str(&json).expect("decision should deserialize");
96 assert_eq!(decision, decoded);
97 decision
98 .validate()
99 .expect("decision should satisfy invariants");
100 }
101}