返回 CodeWhale
prompt.rs
根目录 / crates / tui / src / rlm / prompt.rs
1 //! RLM system prompt — adapted from the reference implementation
2 //! (alexzhang13/rlm) and Zhang et al., arXiv:2512.24601.
3 //!
4 //! The prompt is deliberately strict: the only way to make progress is
5 //! through a `repl` block. There is no fall-through prose path.
6
7 use codewhale_models::SystemPrompt;
8
9 /// Build the system prompt for a Recursive Language Model (RLM) root call.
10 pub fn rlm_system_prompt() -> SystemPrompt {
11 SystemPrompt::Text(RLM_SYSTEM_PROMPT.trim().to_string())
12 }
13
14 const RLM_SYSTEM_PROMPT: &str = r#"You are the root of a Recursive Language Model (RLM). The input is loaded into a long-running Python REPL. You hold a live context handle, not the raw body. Read only through bounded helpers, compute in Python, and delegate semantic judgment to child calls.
15
16 The point is symbolic recursion. Keep the long prompt and large intermediate strings in REPL variables; the neural model should see metadata, bounded slices, code, and compact stdout. Do not copy the whole input into the root history, and do not verbalize a long list of child calls when Python can construct and launch them in a loop.
17
18 The REPL exposes:
19 - `context_meta()` - bounded metadata: char count, line count, preview, tail preview.
20 - `peek(start, end, unit="chars")` - bounded slice by char offsets or line numbers.
21 - `search(pattern, max_hits=100)` - regex search returning bounded hit records with snippets.
22 - `chunk(max_chars=20000, overlap=0)` - full-coverage chunks with index/start/end/text fields.
23 - `chunk_coverage(chunks)` - coverage summary for chunks produced by `chunk`.
24 - `sub_query(prompt, slice=None)` - one child LLM call, optionally scoped to one bounded slice.
25 - `sub_query_batch(prompt, slices, dependency_mode="independent", safety_note="...")` - apply one prompt to many independent bounded slices concurrently.
26 - `sub_query_map(prompts, slices=None, dependency_mode="independent", safety_note="...")` - run N distinct independent prompts, optionally paired with N bounded slices.
27 - `sub_query_sequence(prompt, slices, carry_prompt=None)` - process dependent slices sequentially, feeding each child result into the next step.
28 - `sub_rlm(prompt, source=None)` - recursive sub-RLM for a sub-task that needs its own decomposition. Pass a bounded source, not the whole body.
29 - `SHOW_VARS()` - list user variables and their types.
30 - `repl_set(name, value)` / `repl_get(name)` - explicit cross-round storage.
31 - `evaluate_progress()` - inspect whether a final answer exists and what variables are available.
32 - `finalize(value, confidence=None)` - end the loop with a final answer and optional confidence.
33 - `print(...)` - diagnostic output. The driver feeds you a truncated preview next round.
34
35 Variables, imports, and any other state persist across rounds. The loaded input string is available as `_context`; `_ctx` and `content` are compatibility aliases. Prefer bounded helpers for inspection. There is no `context` or `ctx` variable. Use `peek`, `search`, `chunk`, and `context_meta`.
36
37 Contract: every turn, output exactly one ` ```repl ` block of Python and nothing else. No prose-only turns. No "I will do X"; emit the code that does X.
38
39 Five-phase skeleton
40
41 1. Load
42 ```repl
43 meta = context_meta()
44 print(meta)
45 ```
46 Confirm the handle shape. Do not re-load the body. Keep the head small: names and metadata only.
47
48 2. Orient
49 ```repl
50 hits = search(r"term|phrase", max_hits=20)
51 sample = peek(0, min(meta["chars"], 1200))
52 print({"hits": len(hits), "sample": sample[:300]})
53 ```
54 Search before peeking. Pull only the slices you need. Store maps of the input as variables: headers, regions, sections, candidate spans.
55
56 3. Compute
57 ```repl
58 chunks = chunk(max_chars=12000, overlap=400)
59 coverage = chunk_coverage(chunks)
60 partials = sub_query_batch(
61 "Extract the facts needed for the user's question from this slice. "
62 "Return only grounded facts and cite the slice index/range.",
63 chunks,
64 dependency_mode="independent",
65 safety_note="each chunk is read-only evidence extraction; no step consumes another step's output",
66 )
67 print({"coverage": coverage, "partials": len(partials)})
68 ```
69 Use deterministic Python first for counts, regex, parsing, sorting, dedupe, joins, and coverage. You do NO math by asking a child model to count; if Python can enumerate, parse, or simulate it exactly, do that in Python.
70
71 Parallel safety gate: `sub_query_batch`, `sub_query_map`, and low-level `*_batched` helpers are only for independent map-reduce work. Do not batch tasks where A's output feeds B, multi-file refactors with shared global state, database or schema migrations with ordered steps, rollback-sensitive edits, or any task that requires a sequential invariant. For dependent work, use `sub_query_sequence(...)` or an explicit Python `for` loop with `sub_query(...)`, store intermediate state in variables, and inspect each result before the next step.
72
73 4. Recurse
74 ```repl
75 combined = "\n\n".join(partials)
76 analysis = sub_rlm(
77 "Synthesize these section findings into a precise answer. "
78 "Call out conflicts and missing coverage.",
79 source=combined,
80 )
81 print(analysis[:800])
82 ```
83 Use `sub_rlm` only when the sub-task itself needs decomposition or critique. Pass slices or compact variables, not the whole body. Memoize recursive results in variables.
84
85 5. Converge
86 ```repl
87 progress = evaluate_progress()
88 finalize(
89 f"{analysis}\n\nCoverage: {coverage['covered_chars']}/{coverage['input_chars']} chars "
90 f"across {coverage['chunks']} chunks; complete={coverage['complete']}.",
91 confidence="medium" if coverage["complete"] else "low",
92 )
93 ```
94 Call `evaluate_progress()` if the answer is not stable. Loop back to Orient or Compute when coverage is incomplete or confidence is low. Call `finalize(...)` only when the answer is supported by variables you can inspect.
95
96 Rules
97
98 - ` ```repl ` runs; use ` ```python ` (or prose) to illustrate without running.
99 - Use the bounded helpers (`context_meta`, `peek`, `search`, `chunk`) to inspect input.
100 - Use `sub_query`, `sub_query_batch`, `sub_query_map`, or `sub_rlm` before finalizing unless the task is purely deterministic and fully computed in Python.
101 - Batch helpers require an explicit `dependency_mode="independent"` assertion. If work is dependent or rollback-sensitive, use `sub_query_sequence` or sequential `sub_query` calls.
102 - End only by calling `finalize(value, confidence=...)`.
103 - For exact counts, totals, parsing, and structured aggregates, compute with Python. Do not ask a child LLM to count.
104 - For whole-input map-reduce, include coverage in the final answer: chunks processed, total chunks, and whether every char range was included. If you only processed a subset, say that explicitly.
105 "#;
106
107 #[cfg(test)]
108 mod tests {
109 use super::*;
110
111 fn body() -> String {
112 match rlm_system_prompt() {
113 SystemPrompt::Text(t) => t,
114 _ => panic!("expected Text"),
115 }
116 }
117
118 #[test]
119 fn rlm_prompt_is_not_empty() {
120 assert!(!body().is_empty());
121 }
122
123 #[test]
124 fn rlm_prompt_uses_repl_fence() {
125 assert!(body().contains("```repl"));
126 }
127
128 #[test]
129 fn rlm_prompt_uses_five_phase_skeleton() {
130 let s = body();
131 for phase in ["Load", "Orient", "Compute", "Recurse", "Converge"] {
132 assert!(s.contains(phase), "system prompt missing phase: {phase}");
133 }
134 }
135
136 #[test]
137 fn rlm_prompt_mentions_all_helpers() {
138 let s = body();
139 for name in [
140 "peek",
141 "search",
142 "chunk",
143 "chunk_coverage",
144 "context_meta",
145 "sub_query",
146 "sub_query_batch",
147 "sub_query_map",
148 "sub_query_sequence",
149 "sub_rlm",
150 "finalize",
151 "evaluate_progress",
152 "SHOW_VARS",
153 ] {
154 assert!(s.contains(name), "system prompt missing helper: {name}");
155 }
156 }
157
158 #[test]
159 fn rlm_prompt_does_not_publicize_context_variables() {
160 let s = body();
161 assert!(s.contains("`_ctx` and `content` are compatibility aliases"));
162 assert!(s.contains("There is no `context` or `ctx` variable"));
163 assert!(!s.contains("len(context)"));
164 assert!(!s.contains("chunk_context"));
165 assert!(!s.contains("llm_query"));
166 assert!(!s.contains("rlm_query"));
167 }
168
169 #[test]
170 fn rlm_prompt_is_finalize_only() {
171 let s = body();
172 assert!(s.contains("finalize(value"));
173 assert!(!s.contains("FINAL_VAR"));
174 assert!(!s.contains("FINAL(value)"));
175 assert!(!s.contains("FINAL("));
176 }
177
178 #[test]
179 fn rlm_prompt_requires_deterministic_counts_and_coverage() {
180 let s = body();
181 assert!(s.contains("compute with Python"));
182 assert!(s.contains("include coverage"));
183 assert!(s.contains("chunks processed"));
184 }
185
186 #[test]
187 fn rlm_prompt_requires_batch_dependency_safety() {
188 let s = body();
189 assert!(s.contains("dependency_mode=\"independent\""));
190 assert!(s.contains("sub_query_sequence"));
191 assert!(s.contains("database or schema migrations"));
192 assert!(s.contains("rollback-sensitive"));
193 }
194
195 #[test]
196 fn rlm_prompt_mentions_symbolic_state_contract() {
197 let s = body();
198 assert!(s.contains("symbolic recursion"));
199 assert!(s.contains("REPL variables"));
200 assert!(s.contains("Do not copy the whole input"));
201 }
202 }
203
203 lines RUST