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