返回 CodeWhale
mod.rs
根目录 / crates / tui / src / model_context / 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 #[allow(unused_imports)] // WorldStateDiff consumed once hosts call render_diff in production
19 pub use world_state::{WorldState, WorldStateDiff, WorldStateSnapshot};
20
21 #[cfg(test)]
22 mod tests {
23 use super::*;
24
25 #[test]
26 fn unchanged_fragment_is_retained_not_reinjected() {
27 let mut state = WorldState::new();
28 let first = ModelContextFragment::new(
29 FragmentId::Workspace,
30 FragmentRole::Workspace,
31 "repo: /tmp/demo\nbranch: main",
32 );
33 let render = state.upsert(first.clone());
34 assert!(matches!(render, FragmentRender::Updated { .. }));
35
36 let again = ModelContextFragment::new(
37 FragmentId::Workspace,
38 FragmentRole::Workspace,
39 "repo: /tmp/demo\nbranch: main",
40 );
41 let render = state.upsert(again);
42 assert_eq!(
43 render,
44 FragmentRender::Unchanged {
45 marker: FragmentId::Workspace.marker().to_string(),
46 content_hash: first.content_hash,
47 }
48 );
49
50 let diff = state.render_diff(None);
51 assert_eq!(diff.updated.len(), 1);
52 assert!(diff.retained.is_empty());
53
54 let previous = state.clone();
55 let diff = state.render_diff(Some(&previous));
56 assert!(diff.updated.is_empty());
57 assert_eq!(
58 diff.retained,
59 vec![FragmentId::Workspace.marker().to_string()]
60 );
61 }
62
63 #[test]
64 fn hard_byte_cap_is_enforced_with_truncation_marker() {
65 let oversized = "x".repeat(DEFAULT_FRAGMENT_MAX_BYTES + 64);
66 let fragment = ModelContextFragment::new(
67 FragmentId::AgentTopology,
68 FragmentRole::AgentTopology,
69 oversized,
70 );
71 assert!(fragment.content.len() <= DEFAULT_FRAGMENT_MAX_BYTES);
72 assert!(
73 fragment.content.contains("[…truncated:"),
74 "truncated marker must remain visible: {}",
75 fragment.content
76 );
77 assert_eq!(fragment.marker, FragmentId::AgentTopology.marker());
78 }
79
80 #[test]
81 fn markers_are_stable_across_rebuilds() {
82 let a = ModelContextFragment::new(
83 FragmentId::Permissions,
84 FragmentRole::Permissions,
85 "approval: ask",
86 );
87 let b = ModelContextFragment::new(
88 FragmentId::Permissions,
89 FragmentRole::Permissions,
90 "approval: auto",
91 );
92 assert_eq!(a.marker, b.marker);
93 assert_eq!(a.marker, "<!-- cw:ctx:permissions -->");
94 assert_ne!(a.content_hash, b.content_hash);
95 }
96
97 #[test]
98 fn world_state_diff_updates_only_changed_fragments() {
99 let mut previous = WorldState::new();
100 previous.upsert(ModelContextFragment::new(
101 FragmentId::Route,
102 FragmentRole::Route,
103 "model: deepseek-v4\nmode: agent",
104 ));
105 previous.upsert(ModelContextFragment::new(
106 FragmentId::TokenBudget,
107 FragmentRole::TokenBudget,
108 "budget: 100000\ncompaction: idle",
109 ));
110
111 let mut next = previous.clone();
112 next.upsert(ModelContextFragment::new(
113 FragmentId::TokenBudget,
114 FragmentRole::TokenBudget,
115 "budget: 100000\ncompaction: pending",
116 ));
117
118 let diff = next.render_diff(Some(&previous));
119 assert_eq!(diff.retained, vec![FragmentId::Route.marker().to_string()]);
120 assert_eq!(diff.updated.len(), 1);
121 assert_eq!(diff.updated[0].id, FragmentId::TokenBudget);
122 assert!(diff.updated[0].content.contains("compaction: pending"));
123 }
124
125 #[test]
126 fn snapshot_renders_constitution_then_world_state_blocks() {
127 let mut state = WorldState::new();
128 state.upsert(ModelContextFragment::new(
129 FragmentId::Workspace,
130 FragmentRole::Workspace,
131 "pwd: /ws",
132 ));
133 state.upsert(ModelContextFragment::new(
134 FragmentId::SkillsTools,
135 FragmentRole::SkillsTools,
136 "skills: 2 available",
137 ));
138
139 let snapshot = WorldStateSnapshot {
140 constitution: "You are CodeWhale.".to_string(),
141 world_state: state,
142 };
143 let blocks = snapshot.to_system_blocks();
144 assert_eq!(blocks.len(), 3);
145 assert_eq!(blocks[0].text, "You are CodeWhale.");
146 assert!(blocks[1].text.starts_with(FragmentId::Workspace.marker()));
147 assert!(blocks[2].text.starts_with(FragmentId::SkillsTools.marker()));
148
149 let text = snapshot.render_text();
150 assert!(text.starts_with("You are CodeWhale."));
151 assert!(text.contains(FragmentId::Workspace.marker()));
152 assert!(text.contains(FragmentId::SkillsTools.marker()));
153 }
154 }
155
155 lines RUST