返回 CodeWhale
world_state.rs
根目录 / crates / tui / src / model_context / world_state.rs
1 //! WorldState: ordered collection of ModelContext fragments with diff render.
2
3 use std::collections::BTreeMap;
4
5 use codewhale_models::SystemBlock;
6
7 use super::fragment::{FragmentId, FragmentRender, FragmentRole, ModelContextFragment};
8
9 /// Incremental render of WorldState against a previous snapshot.
10 #[cfg(test)]
11 #[derive(Debug, Clone, PartialEq, Eq, Default)]
12 pub struct WorldStateDiff {
13 /// Fragments whose content hash changed (or are new).
14 pub updated: Vec<ModelContextFragment>,
15 /// Markers that matched the previous snapshot byte-for-byte.
16 pub retained: Vec<String>,
17 /// Markers present previously and now cleared.
18 pub cleared: Vec<String>,
19 }
20
21 /// Mutable mid-session context layer living below the constitution prefix.
22 #[derive(Debug, Clone, PartialEq, Eq, Default)]
23 pub struct WorldState {
24 fragments: BTreeMap<FragmentId, ModelContextFragment>,
25 }
26
27 impl WorldState {
28 #[must_use]
29 pub fn new() -> Self {
30 Self::default()
31 }
32
33 /// Insert or replace a fragment. Returns retain-unchanged when the hash
34 /// matches the previous value for that id.
35 pub fn upsert(&mut self, fragment: ModelContextFragment) -> FragmentRender {
36 let previous = self.fragments.get(&fragment.id).cloned();
37 let render = fragment.render_diff(previous.as_ref());
38 if matches!(render, FragmentRender::Updated { .. }) {
39 self.fragments.insert(fragment.id, fragment);
40 }
41 render
42 }
43
44 #[must_use]
45 pub fn len(&self) -> usize {
46 self.fragments.len()
47 }
48
49 /// Full render of every fragment in stable `FragmentId` order.
50 #[cfg(test)]
51 #[must_use]
52 pub fn render_full(&self) -> String {
53 self.fragments
54 .values()
55 .map(ModelContextFragment::render_marked)
56 .collect::<Vec<_>>()
57 .join("\n\n")
58 }
59
60 /// Diff against a previous WorldState. Unchanged fragments are retained
61 /// (listed, not reinjected into `updated`).
62 #[cfg(test)]
63 #[must_use]
64 pub fn render_diff(&self, previous: Option<&WorldState>) -> WorldStateDiff {
65 let Some(previous) = previous else {
66 return WorldStateDiff {
67 updated: self.fragments.values().cloned().collect(),
68 retained: Vec::new(),
69 cleared: Vec::new(),
70 };
71 };
72
73 let mut diff = WorldStateDiff::default();
74 for id in FragmentId::all() {
75 match (previous.fragments.get(id), self.fragments.get(id)) {
76 (Some(prev), Some(next)) => match next.render_diff(Some(prev)) {
77 FragmentRender::Unchanged { marker, .. } => diff.retained.push(marker),
78 FragmentRender::Updated { fragment } => diff.updated.push(fragment),
79 FragmentRender::Cleared { marker } => diff.cleared.push(marker),
80 },
81 (None, Some(next)) => diff.updated.push(next.clone()),
82 (Some(prev), None) => diff.cleared.push(prev.marker.to_string()),
83 (None, None) => {}
84 }
85 }
86 diff
87 }
88
89 /// Convenience builders for the candidate volatile concerns.
90 #[must_use]
91 pub fn with_workspace(mut self, body: impl Into<String>) -> Self {
92 self.upsert(ModelContextFragment::new(
93 FragmentId::Workspace,
94 FragmentRole::Workspace,
95 body,
96 ));
97 self
98 }
99
100 #[must_use]
101 pub fn with_permissions(mut self, body: impl Into<String>) -> Self {
102 self.upsert(ModelContextFragment::new(
103 FragmentId::Permissions,
104 FragmentRole::Permissions,
105 body,
106 ));
107 self
108 }
109
110 #[must_use]
111 pub fn with_route(mut self, body: impl Into<String>) -> Self {
112 self.upsert(ModelContextFragment::new(
113 FragmentId::Route,
114 FragmentRole::Route,
115 body,
116 ));
117 self
118 }
119
120 #[must_use]
121 pub fn with_agent_topology(mut self, body: impl Into<String>) -> Self {
122 self.upsert(ModelContextFragment::new(
123 FragmentId::AgentTopology,
124 FragmentRole::AgentTopology,
125 body,
126 ));
127 self
128 }
129
130 #[must_use]
131 pub fn with_skills_tools(mut self, body: impl Into<String>) -> Self {
132 self.upsert(ModelContextFragment::new(
133 FragmentId::SkillsTools,
134 FragmentRole::SkillsTools,
135 body,
136 ));
137 self
138 }
139
140 #[must_use]
141 pub fn with_token_budget(mut self, body: impl Into<String>) -> Self {
142 self.upsert(ModelContextFragment::new(
143 FragmentId::TokenBudget,
144 FragmentRole::TokenBudget,
145 body,
146 ));
147 self
148 }
149
150 #[must_use]
151 pub fn with_project_instructions(mut self, body: impl Into<String>) -> Self {
152 self.upsert(ModelContextFragment::new(
153 FragmentId::ProjectInstructions,
154 FragmentRole::ProjectInstructions,
155 body,
156 ));
157 self
158 }
159
160 /// Enforce the hard caps for this WorldState. Returns an error if the
161 /// fragment count or any fragment's byte/token size exceeds the core
162 /// ceilings (`MAX_FRAGMENT_BYTES` / `MAX_FRAGMENT_TOKENS`).
163 pub fn validate_caps(&self) -> Result<(), String> {
164 use crate::model_context::fragment::{
165 MAX_FRAGMENT_BYTES, MAX_FRAGMENT_TOKENS, MAX_FRAGMENTS_PER_CONTEXT,
166 };
167 if self.fragments.len() > MAX_FRAGMENTS_PER_CONTEXT {
168 return Err(format!(
169 "too many fragments: {} > {}",
170 self.fragments.len(),
171 MAX_FRAGMENTS_PER_CONTEXT
172 ));
173 }
174 for fragment in self.fragments.values() {
175 if fragment.content.len() > MAX_FRAGMENT_BYTES {
176 return Err(format!(
177 "fragment {:?} exceeds byte ceiling: {} > {}",
178 fragment.id,
179 fragment.content.len(),
180 MAX_FRAGMENT_BYTES
181 ));
182 }
183 let tokens = fragment.content.len().div_ceil(4);
184 if tokens > MAX_FRAGMENT_TOKENS {
185 return Err(format!(
186 "fragment {:?} exceeds token ceiling: {} > {}",
187 fragment.id, tokens, MAX_FRAGMENT_TOKENS
188 ));
189 }
190 }
191 Ok(())
192 }
193 }
194
195 /// Constitution (cache-stable) + WorldState (volatile) assembly point.
196 #[derive(Debug, Clone, PartialEq, Eq)]
197 pub struct WorldStateSnapshot {
198 pub constitution: String,
199 pub world_state: WorldState,
200 }
201
202 impl WorldStateSnapshot {
203 /// Structured blocks: constitution first (cacheable), then each fragment.
204 #[must_use]
205 pub fn to_system_blocks(&self) -> Vec<SystemBlock> {
206 let mut blocks = Vec::with_capacity(1 + self.world_state.len());
207 blocks.push(SystemBlock {
208 block_type: "text".to_string(),
209 text: self.constitution.trim().to_string(),
210 cache_control: None,
211 });
212 for fragment in self.world_state.fragments.values() {
213 blocks.push(SystemBlock {
214 block_type: "text".to_string(),
215 text: fragment.render_marked(),
216 cache_control: None,
217 });
218 }
219 blocks
220 }
221
222 /// Flat text fallback for callers that still expect `SystemPrompt::Text`.
223 #[cfg(test)]
224 #[must_use]
225 pub fn render_text(&self) -> String {
226 let world = self.world_state.render_full();
227 if world.is_empty() {
228 self.constitution.trim().to_string()
229 } else {
230 format!("{}\n\n{}", self.constitution.trim(), world)
231 }
232 }
233 }
234
234 lines RUST