返回 CodeWhale
mod.rs
1 //! Typed incremental ModelContext / WorldState fragments.
2 //!
3 //! CodeWhale already assembles prompts most-static → most-volatile for prefix
4 //! caching (`prompts.rs`). This module adds the missing identity layer: each
5 //! volatile concern is a capped, marked fragment with `render_diff` so an
6 //! environment or agent-topology change does not rebuild unrelated material.
7 //!
8 //! The constitution / mode base stays outside WorldState and remains the
9 //! cache-stable prefix. WorldState owns only the mutable mid-session layers.
10
11 mod fragment;
12 mod world_state;
13
14 #[allow(unused_imports)] // public ModelContext surface for TUI-DOG-011 prompt cutover siblings
15 pub use fragment::{
16 DEFAULT_FRAGMENT_MAX_BYTES, FragmentId, FragmentRender, FragmentRole, ModelContextFragment,
17 };
18 pub use world_state::{WorldState, WorldStateSnapshot};
19
20 #[cfg(test)]
21 mod tests {
22 use super::*;
23
24 #[test]
25 fn unchanged_fragment_is_retained_not_reinjected() {
26 let mut state = WorldState::new();
27 let first = ModelContextFragment::new(
28 FragmentId::Workspace,
29 FragmentRole::Workspace,
30 "repo: /tmp/demo\nbranch: main",
31 );
32 let render = state.upsert(first.clone());
33 assert!(matches!(render, FragmentRender::Updated { .. }));
34
35 let again = ModelContextFragment::new(
36 FragmentId::Workspace,
37 FragmentRole::Workspace,
38 "repo: /tmp/demo\nbranch: main",
39 );
40 let render = state.upsert(again);
41 assert_eq!(
42 render,
43 FragmentRender::Unchanged {
44 marker: FragmentId::Workspace.marker().to_string(),
45 content_hash: first.content_hash,
46 }
47 );
48
49 let diff = state.render_diff(None);
50 assert_eq!(diff.updated.len(), 1);
51 assert!(diff.retained.is_empty());
52
53 let previous = state.clone();
54 let diff = state.render_diff(Some(&previous));
55 assert!(diff.updated.is_empty());
56 assert_eq!(
57 diff.retained,
58 vec![FragmentId::Workspace.marker().to_string()]
59 );
60 }
61
62 #[test]
63 fn hard_byte_cap_is_enforced_with_truncation_marker() {
64 let oversized = "x".repeat(DEFAULT_FRAGMENT_MAX_BYTES + 64);
65 let fragment = ModelContextFragment::new(
66 FragmentId::AgentTopology,
67 FragmentRole::AgentTopology,
68 oversized,
69 );
70 assert!(fragment.content.len() <= DEFAULT_FRAGMENT_MAX_BYTES);
71 assert!(
72 fragment.content.contains("[…truncated:"),
73 "truncated marker must remain visible: {}",
74 fragment.content
75 );
76 assert_eq!(fragment.marker, FragmentId::AgentTopology.marker());
77 }
78
79 #[test]
80 fn markers_are_stable_across_rebuilds() {
81 let a = ModelContextFragment::new(
82 FragmentId::Permissions,
83 FragmentRole::Permissions,
84 "approval: ask",
85 );
86 let b = ModelContextFragment::new(
87 FragmentId::Permissions,
88 FragmentRole::Permissions,
89 "approval: auto",
90 );
91 assert_eq!(a.marker, b.marker);
92 assert_eq!(a.marker, "<!-- cw:ctx:permissions -->");
93 assert_ne!(a.content_hash, b.content_hash);
94 }
95
96 #[test]
97 fn world_state_diff_updates_only_changed_fragments() {
98 let mut previous = WorldState::new();
99 previous.upsert(ModelContextFragment::new(
100 FragmentId::Route,
101 FragmentRole::Route,
102 "model: deepseek-v4\nmode: agent",
103 ));
104 previous.upsert(ModelContextFragment::new(
105 FragmentId::TokenBudget,
106 FragmentRole::TokenBudget,
107 "budget: 100000\ncompaction: idle",
108 ));
109
110 let mut next = previous.clone();
111 next.upsert(ModelContextFragment::new(
112 FragmentId::TokenBudget,
113 FragmentRole::TokenBudget,
114 "budget: 100000\ncompaction: pending",
115 ));
116
117 let diff = next.render_diff(Some(&previous));
118 assert_eq!(diff.retained, vec![FragmentId::Route.marker().to_string()]);
119 assert_eq!(diff.updated.len(), 1);
120 assert_eq!(diff.updated[0].id, FragmentId::TokenBudget);
121 assert!(diff.updated[0].content.contains("compaction: pending"));
122 }
123
124 #[test]
125 fn snapshot_renders_constitution_then_world_state_blocks() {
126 let mut state = WorldState::new();
127 state.upsert(ModelContextFragment::new(
128 FragmentId::Workspace,
129 FragmentRole::Workspace,
130 "pwd: /ws",
131 ));
132 state.upsert(ModelContextFragment::new(
133 FragmentId::SkillsTools,
134 FragmentRole::SkillsTools,
135 "skills: 2 available",
136 ));
137
138 let snapshot = WorldStateSnapshot {
139 constitution: "You are CodeWhale.".to_string(),
140 world_state: state,
141 };
142 let blocks = snapshot.to_system_blocks();
143 assert_eq!(blocks.len(), 3);
144 assert_eq!(blocks[0].text, "You are CodeWhale.");
145 assert!(blocks[1].text.starts_with(FragmentId::Workspace.marker()));
146 assert!(blocks[2].text.starts_with(FragmentId::SkillsTools.marker()));
147
148 let text = snapshot.render_text();
149 assert!(text.starts_with("You are CodeWhale."));
150 assert!(text.contains(FragmentId::Workspace.marker()));
151 assert!(text.contains(FragmentId::SkillsTools.marker()));
152 }
153 }
154
154 lines RUST