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
//! 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))
}