返回 CodeWhale
context.rs
根目录 / crates / tui / src / core / runtime_contract / context.rs
1 use std::collections::BTreeSet;
2 use std::path::PathBuf;
3
4 use serde::{Deserialize, Serialize};
5
6 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
7 #[serde(rename_all = "snake_case")]
8 pub enum ContextSourceKind {
9 Constitution,
10 RepositoryLaw,
11 ScopedRepositoryLaw,
12 Instruction,
13 Skill,
14 Hook,
15 Mcp,
16 Memory,
17 ModelProfile,
18 CapabilityProfile,
19 }
20
21 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22 #[serde(rename_all = "snake_case")]
23 pub enum ContextPriority {
24 Required,
25 High,
26 Normal,
27 Optional,
28 }
29
30 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31 pub struct ContextSourceReceipt {
32 pub id: String,
33 pub kind: ContextSourceKind,
34 pub priority: ContextPriority,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub path: Option<PathBuf>,
37 pub bytes: u64,
38 pub estimated_tokens: u64,
39 pub content_hash: String,
40 pub included: bool,
41 pub reason: String,
42 }
43
44 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45 pub struct ResourcesLoadedEvent {
46 pub event_id: String,
47 pub sources: Vec<ContextSourceReceipt>,
48 pub assembly_ms: u64,
49 pub prompt_tokens: u64,
50 pub schema_tokens: u64,
51 }
52
53 impl ResourcesLoadedEvent {
54 #[must_use]
55 pub fn included_tokens(&self) -> u64 {
56 self.sources
57 .iter()
58 .filter(|source| source.included)
59 .map(|source| source.estimated_tokens)
60 .sum()
61 }
62
63 pub fn validate_required_sources(&self) -> Result<(), Vec<String>> {
64 let missing = self
65 .sources
66 .iter()
67 .filter(|source| source.priority == ContextPriority::Required && !source.included)
68 .map(|source| source.id.clone())
69 .collect::<Vec<_>>();
70 if missing.is_empty() {
71 Ok(())
72 } else {
73 Err(missing)
74 }
75 }
76 }
77
78 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79 pub struct ContinuitySet {
80 #[serde(default)]
81 pub constraints: BTreeSet<String>,
82 #[serde(default)]
83 pub approvals: BTreeSet<String>,
84 #[serde(default)]
85 pub failed_checks: BTreeSet<String>,
86 #[serde(default)]
87 pub edited_paths: BTreeSet<PathBuf>,
88 #[serde(default)]
89 pub pending_work: BTreeSet<String>,
90 }
91
92 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93 pub struct CondensationEvent {
94 pub event_id: String,
95 pub source_start_event_id: String,
96 pub source_end_event_id: String,
97 pub summary_hash: String,
98 pub provider: String,
99 pub model: String,
100 pub reason: String,
101 pub continuity: ContinuitySet,
102 pub input_tokens: u64,
103 pub output_tokens: u64,
104 }
105
106 impl CondensationEvent {
107 pub fn validate(&self) -> Result<(), String> {
108 for (label, value) in [
109 ("event_id", self.event_id.as_str()),
110 ("source_start_event_id", self.source_start_event_id.as_str()),
111 ("source_end_event_id", self.source_end_event_id.as_str()),
112 ("summary_hash", self.summary_hash.as_str()),
113 ("provider", self.provider.as_str()),
114 ("model", self.model.as_str()),
115 ("reason", self.reason.as_str()),
116 ] {
117 if value.trim().is_empty() {
118 return Err(format!("condensation {label} cannot be empty"));
119 }
120 }
121 Ok(())
122 }
123 }
124
125 #[cfg(test)]
126 mod tests {
127 use super::*;
128
129 #[test]
130 fn required_law_cannot_be_silently_dropped() {
131 let event = ResourcesLoadedEvent {
132 event_id: "resources-1".to_string(),
133 sources: vec![ContextSourceReceipt {
134 id: "AGENTS.md".to_string(),
135 kind: ContextSourceKind::RepositoryLaw,
136 priority: ContextPriority::Required,
137 path: Some(PathBuf::from("AGENTS.md")),
138 bytes: 100,
139 estimated_tokens: 25,
140 content_hash: "hash".to_string(),
141 included: false,
142 reason: "budget".to_string(),
143 }],
144 assembly_ms: 1,
145 prompt_tokens: 0,
146 schema_tokens: 0,
147 };
148 assert_eq!(
149 event.validate_required_sources().unwrap_err(),
150 vec!["AGENTS.md"]
151 );
152 }
153
154 #[test]
155 fn condensation_preserves_distinct_continuity_domains() {
156 let continuity = ContinuitySet {
157 constraints: BTreeSet::from(["do not deploy".to_string()]),
158 approvals: BTreeSet::from(["edit src only".to_string()]),
159 failed_checks: BTreeSet::from(["cargo test".to_string()]),
160 edited_paths: BTreeSet::from([PathBuf::from("src/lib.rs")]),
161 pending_work: BTreeSet::from(["rerun test".to_string()]),
162 };
163 let json = serde_json::to_value(&continuity).unwrap();
164 assert_eq!(json["constraints"][0], "do not deploy");
165 assert_eq!(json["failed_checks"][0], "cargo test");
166 assert_eq!(json["pending_work"][0], "rerun test");
167 }
168 }
169
169 lines RUST