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}