Skip to main content

supercode_harness/
context_injection.rs

1//! BP-4 (catalog:91 "Synthetic context-injection blocks", design §1.4:
2//! "harness-spliced reminders/nudges … the ambient nudge class is core"):
3//! the injection REGISTRY behind `core.context_injections`.
4//!
5//! Before BP-4 the key gated exactly one thing — a static, caller-populated
6//! [`ContextInjectionBlock`] list appended once at construction — so a
7//! preset turning it on got nothing, because no preset (and nothing in the
8//! product) ever populated the list. Neither parity preset set the key at
9//! all, which made the gap invisible.
10//!
11//! The registry has three sources, spliced in this order:
12//!
13//! 1. **Built-in blocks** ([`builtin_blocks`]) — derived from the RESOLVED
14//!    config, so a block only appears when the capability it talks about is
15//!    actually armed. This is the "~25 block types" class cx's own
16//!    `context/` library and cc's `<system-reminder>` blocks occupy: ambient
17//!    statements about the harness the model is running inside, which no
18//!    instruction file can know.
19//! 2. **User blocks** — [`crate::Config::context_injection_blocks`], the
20//!    pre-existing embedder-populated list, unchanged.
21//! 3. **Spliced blocks** — [`crate::Agent::inject_context_block`], added
22//!    mid-session (a hook's `additionalContext`, a frontend's nudge, an
23//!    orchestrator's brief). This is the half that makes the mechanism a
24//!    SEAM rather than a startup constant.
25//!
26//! Everything here is a no-op when `core.context_injections` is false (the
27//! default): [`assemble`] returns an empty string and nothing is read.
28
29use crate::config::{Config, ContextInjectionBlock};
30use crate::modules::ModuleId;
31
32/// The built-in ambient blocks armed by `config`, in a stable order.
33///
34/// Each block states something true about THIS resolved configuration that
35/// the model cannot otherwise know, and each is gated on the capability it
36/// describes — a config with none of them armed contributes no blocks at
37/// all, so this is never boilerplate the model has to ignore.
38pub fn builtin_blocks(config: &Config) -> Vec<ContextInjectionBlock> {
39    let mut blocks = Vec::new();
40    let active = |id: ModuleId| config.module_registry && config.module_activation.is_active(id);
41
42    if active(ModuleId::Todos) {
43        blocks.push(ContextInjectionBlock::new(
44            "Task list",
45            "A persistent task list is available through the plan/todo tool. Keep it current: \
46             write the plan out before starting multi-step work, mark each step completed as \
47             you finish it, and add work you discover along the way. The list survives \
48             compaction, so it is the durable record of where this session is.",
49        ));
50    }
51    if active(ModuleId::PlanMode) {
52        blocks.push(ContextInjectionBlock::new(
53            "Plan mode",
54            "This session can enter a read-only planning mode. While it is active, do not edit \
55             files, write files, or run state-changing commands — investigate, then present the \
56             plan and wait for it to be accepted.",
57        ));
58    }
59    if !config.permissions_protected_paths.is_empty() {
60        blocks.push(ContextInjectionBlock::new(
61            "Protected paths",
62            format!(
63                "Writes to these paths are never auto-approved and will stop for the user's \
64                 decision: {}. Prefer a route that doesn't touch them.",
65                config.permissions_protected_paths.join(", ")
66            ),
67        ));
68    }
69    if active(ModuleId::ToolsBackground) {
70        blocks.push(ContextInjectionBlock::new(
71            "Background work",
72            "Long-running commands can be started in the background instead of blocking the \
73             turn. Start them detached, keep working, and read their output when it matters — \
74             never sit on a foreground command waiting for it to finish.",
75        ));
76    }
77    blocks
78}
79
80/// Every block a system prompt should carry, in splice order: built-ins,
81/// then the config's own list, then anything spliced in at runtime.
82/// Empty (and free of any work) when `core.context_injections` is off.
83pub fn blocks(config: &Config, spliced: &[ContextInjectionBlock]) -> Vec<ContextInjectionBlock> {
84    if !config.context_injections {
85        return Vec::new();
86    }
87    let mut out = builtin_blocks(config);
88    out.extend(config.context_injection_blocks.iter().cloned());
89    out.extend(spliced.iter().cloned());
90    out
91}
92
93/// Render blocks as the `\n\n# {name}\n{content}` sections the assembly site
94/// appends to the system prompt — the exact shape P4e's static list used, so
95/// a config that only set `context_injection_blocks` renders identically.
96pub fn render(blocks: &[ContextInjectionBlock]) -> String {
97    let mut out = String::new();
98    for block in blocks {
99        out.push_str(&format!("\n\n# {}\n{}", block.name, block.content));
100    }
101    out
102}
103
104/// [`blocks`] + [`render`] — the whole injection contribution to a system
105/// prompt.
106pub fn assemble(config: &Config, spliced: &[ContextInjectionBlock]) -> String {
107    render(&blocks(config, spliced))
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::configfile::{resolve, ResolveOptions};
114
115    fn resolved(preset: &str) -> Config {
116        let toml = crate::presets::lookup(preset).unwrap();
117        resolve(toml, None, &ResolveOptions { strict: true })
118            .unwrap_or_else(|e| panic!("{preset} resolves: {e}"))
119            .config
120    }
121
122    /// The gate still means what it said: off ⇒ nothing, not even built-ins.
123    #[test]
124    fn the_gate_off_contributes_nothing() {
125        let mut config = resolved("cc-parity");
126        config.context_injections = false;
127        assert!(assemble(&config, &[]).is_empty());
128    }
129
130    /// Both parity presets arm the key AND get real built-in blocks out of
131    /// it — the registry is not an empty seam under the presets.
132    #[test]
133    fn both_presets_arm_real_builtin_blocks() {
134        for preset in ["cc-parity", "cx-parity"] {
135            let config = resolved(preset);
136            assert!(
137                config.context_injections,
138                "{preset} must set core.context_injections"
139            );
140            let blocks = blocks(&config, &[]);
141            assert!(
142                !blocks.is_empty(),
143                "{preset}: the registry produced no blocks"
144            );
145            // Every preset arms todos and protected paths.
146            let names: Vec<&str> = blocks.iter().map(|b| b.name.as_str()).collect();
147            assert!(names.contains(&"Task list"), "{preset}: {names:?}");
148            assert!(names.contains(&"Protected paths"), "{preset}: {names:?}");
149        }
150    }
151
152    /// A block only appears when its capability is armed — cx-parity has
153    /// `plan_mode` off, so it must not be told about plan mode.
154    #[test]
155    fn builtin_blocks_track_the_module_set() {
156        let cc: Vec<String> = blocks(&resolved("cc-parity"), &[])
157            .into_iter()
158            .map(|b| b.name)
159            .collect();
160        let cx: Vec<String> = blocks(&resolved("cx-parity"), &[])
161            .into_iter()
162            .map(|b| b.name)
163            .collect();
164        assert!(cc.iter().any(|n| n == "Plan mode"));
165        assert!(
166            !cx.iter().any(|n| n == "Plan mode"),
167            "cx-parity has plan_mode off"
168        );
169    }
170
171    /// Built-ins, user blocks and spliced blocks all land, in that order.
172    #[test]
173    fn three_sources_splice_in_order() {
174        let mut config = resolved("cc-parity");
175        config.context_injection_blocks = vec![ContextInjectionBlock::new("User", "user body")];
176        let spliced = [ContextInjectionBlock::new("Spliced", "spliced body")];
177        let text = assemble(&config, &spliced);
178        let builtin_at = text.find("# Task list").unwrap();
179        let user_at = text.find("# User").unwrap();
180        let spliced_at = text.find("# Spliced").unwrap();
181        assert!(builtin_at < user_at && user_at < spliced_at);
182        assert!(text.contains("spliced body"));
183    }
184}