返回 CodeWhale
world_state.rs
根目录 / crates / runtime / 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(any(test, feature = "test-support"))]
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 // Exported from codewhale-runtime, so clippy now asks for `is_empty`;
45 // the move adds no API (runtime split, behavior-free).
46 #[allow(clippy::len_without_is_empty)]
47 #[must_use]
48 pub fn len(&self) -> usize {
49 self.fragments.len()
50 }
51
52 /// Full render of every fragment in stable `FragmentId` order.
53 #[cfg(any(test, feature = "test-support"))]
54 #[must_use]
55 pub fn render_full(&self) -> String {
56 self.fragments
57 .values()
58 .map(ModelContextFragment::render_marked)
59 .collect::<Vec<_>>()
60 .join("\n\n")
61 }
62
63 /// Diff against a previous WorldState. Unchanged fragments are retained
64 /// (listed, not reinjected into `updated`).
65 #[cfg(any(test, feature = "test-support"))]
66 #[must_use]
67 pub fn render_diff(&self, previous: Option<&WorldState>) -> WorldStateDiff {
68 let Some(previous) = previous else {
69 return WorldStateDiff {
70 updated: self.fragments.values().cloned().collect(),
71 retained: Vec::new(),
72 cleared: Vec::new(),
73 };
74 };
75
76 let mut diff = WorldStateDiff::default();
77 for id in FragmentId::all() {
78 match (previous.fragments.get(id), self.fragments.get(id)) {
79 (Some(prev), Some(next)) => match next.render_diff(Some(prev)) {
80 FragmentRender::Unchanged { marker, .. } => diff.retained.push(marker),
81 FragmentRender::Updated { fragment } => diff.updated.push(fragment),
82 FragmentRender::Cleared { marker } => diff.cleared.push(marker),
83 },
84 (None, Some(next)) => diff.updated.push(next.clone()),
85 (Some(prev), None) => diff.cleared.push(prev.marker.to_string()),
86 (None, None) => {}
87 }
88 }
89 diff
90 }
91
92 /// Convenience builders for the candidate volatile concerns.
93 #[must_use]
94 pub fn with_workspace(mut self, body: impl Into<String>) -> Self {
95 self.upsert(ModelContextFragment::new(
96 FragmentId::Workspace,
97 FragmentRole::Workspace,
98 body,
99 ));
100 self
101 }
102
103 #[must_use]
104 pub fn with_permissions(mut self, body: impl Into<String>) -> Self {
105 self.upsert(ModelContextFragment::new(
106 FragmentId::Permissions,
107 FragmentRole::Permissions,
108 body,
109 ));
110 self
111 }
112
113 #[must_use]
114 pub fn with_route(mut self, body: impl Into<String>) -> Self {
115 self.upsert(ModelContextFragment::new(
116 FragmentId::Route,
117 FragmentRole::Route,
118 body,
119 ));
120 self
121 }
122
123 #[must_use]
124 pub fn with_agent_topology(mut self, body: impl Into<String>) -> Self {
125 self.upsert(ModelContextFragment::new(
126 FragmentId::AgentTopology,
127 FragmentRole::AgentTopology,
128 body,
129 ));
130 self
131 }
132
133 #[must_use]
134 pub fn with_skills_tools(mut self, body: impl Into<String>) -> Self {
135 self.upsert(ModelContextFragment::new(
136 FragmentId::SkillsTools,
137 FragmentRole::SkillsTools,
138 body,
139 ));
140 self
141 }
142
143 #[must_use]
144 pub fn with_token_budget(mut self, body: impl Into<String>) -> Self {
145 self.upsert(ModelContextFragment::new(
146 FragmentId::TokenBudget,
147 FragmentRole::TokenBudget,
148 body,
149 ));
150 self
151 }
152
153 #[must_use]
154 pub fn with_project_instructions(mut self, body: impl Into<String>) -> Self {
155 self.upsert(ModelContextFragment::new(
156 FragmentId::ProjectInstructions,
157 FragmentRole::ProjectInstructions,
158 body,
159 ));
160 self
161 }
162
163 /// Enforce the hard caps for this WorldState. Returns an error if the
164 /// fragment count or any fragment's byte/token size exceeds the core
165 /// ceilings (`MAX_FRAGMENT_BYTES` / `MAX_FRAGMENT_TOKENS`).
166 pub fn validate_caps(&self) -> Result<(), String> {
167 use crate::model_context::fragment::{
168 MAX_FRAGMENT_BYTES, MAX_FRAGMENT_TOKENS, MAX_FRAGMENTS_PER_CONTEXT,
169 };
170 if self.fragments.len() > MAX_FRAGMENTS_PER_CONTEXT {
171 return Err(format!(
172 "too many fragments: {} > {}",
173 self.fragments.len(),
174 MAX_FRAGMENTS_PER_CONTEXT
175 ));
176 }
177 for fragment in self.fragments.values() {
178 if fragment.content.len() > MAX_FRAGMENT_BYTES {
179 return Err(format!(
180 "fragment {:?} exceeds byte ceiling: {} > {}",
181 fragment.id,
182 fragment.content.len(),
183 MAX_FRAGMENT_BYTES
184 ));
185 }
186 let tokens = fragment.content.len().div_ceil(4);
187 if tokens > MAX_FRAGMENT_TOKENS {
188 return Err(format!(
189 "fragment {:?} exceeds token ceiling: {} > {}",
190 fragment.id, tokens, MAX_FRAGMENT_TOKENS
191 ));
192 }
193 }
194 Ok(())
195 }
196 }
197
198 /// Constitution (cache-stable) + WorldState (volatile) assembly point.
199 #[derive(Debug, Clone, PartialEq, Eq)]
200 pub struct WorldStateSnapshot {
201 pub constitution: String,
202 pub world_state: WorldState,
203 }
204
205 impl WorldStateSnapshot {
206 /// Structured blocks: constitution first (cacheable), then each fragment.
207 #[must_use]
208 pub fn to_system_blocks(&self) -> Vec<SystemBlock> {
209 let mut blocks = Vec::with_capacity(1 + self.world_state.len());
210 blocks.push(SystemBlock {
211 block_type: "text".to_string(),
212 text: self.constitution.trim().to_string(),
213 cache_control: None,
214 });
215 for fragment in self.world_state.fragments.values() {
216 blocks.push(SystemBlock {
217 block_type: "text".to_string(),
218 text: fragment.render_marked(),
219 cache_control: None,
220 });
221 }
222 blocks
223 }
224
225 /// Flat text fallback for callers that still expect `SystemPrompt::Text`.
226 #[cfg(any(test, feature = "test-support"))]
227 #[must_use]
228 pub fn render_text(&self) -> String {
229 let world = self.world_state.render_full();
230 if world.is_empty() {
231 self.constitution.trim().to_string()
232 } else {
233 format!("{}\n\n{}", self.constitution.trim(), world)
234 }
235 }
236 }
237
237 lines RUST