| 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 crate::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 | - Use the bounded helpers (`context_meta`, `peek`, `search`, `chunk`) to inspect input. |
| 99 | - 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. |
| 100 | - 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. |
| 101 | - End only by calling `finalize(value, confidence=...)`. |
| 102 | - For exact counts, totals, parsing, and structured aggregates, compute with Python. Do not ask a child LLM to count. |
| 103 | - 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. |
| 104 | "#; |
| 105 | |
| 106 | #[cfg(test)] |
| 107 | mod tests { |
| 108 | use super::*; |
| 109 | |
| 110 | fn body() -> String { |
| 111 | match rlm_system_prompt() { |
| 112 | SystemPrompt::Text(t) => t, |
| 113 | _ => panic!("expected Text"), |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | #[test] |
| 118 | fn rlm_prompt_is_not_empty() { |
| 119 | assert!(!body().is_empty()); |
| 120 | } |
| 121 | |
| 122 | #[test] |
| 123 | fn rlm_prompt_uses_repl_fence() { |
| 124 | assert!(body().contains("```repl")); |
| 125 | } |
| 126 | |
| 127 | #[test] |
| 128 | fn rlm_prompt_uses_five_phase_skeleton() { |
| 129 | let s = body(); |
| 130 | for phase in ["Load", "Orient", "Compute", "Recurse", "Converge"] { |
| 131 | assert!(s.contains(phase), "system prompt missing phase: {phase}"); |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | #[test] |
| 136 | fn rlm_prompt_mentions_all_helpers() { |
| 137 | let s = body(); |
| 138 | for name in [ |
| 139 | "peek", |
| 140 | "search", |
| 141 | "chunk", |
| 142 | "chunk_coverage", |
| 143 | "context_meta", |
| 144 | "sub_query", |
| 145 | "sub_query_batch", |
| 146 | "sub_query_map", |
| 147 | "sub_query_sequence", |
| 148 | "sub_rlm", |
| 149 | "finalize", |
| 150 | "evaluate_progress", |
| 151 | "SHOW_VARS", |
| 152 | ] { |
| 153 | assert!(s.contains(name), "system prompt missing helper: {name}"); |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | #[test] |
| 158 | fn rlm_prompt_does_not_publicize_context_variables() { |
| 159 | let s = body(); |
| 160 | assert!(s.contains("`_ctx` and `content` are compatibility aliases")); |
| 161 | assert!(s.contains("There is no `context` or `ctx` variable")); |
| 162 | assert!(!s.contains("len(context)")); |
| 163 | assert!(!s.contains("chunk_context")); |
| 164 | assert!(!s.contains("llm_query")); |
| 165 | assert!(!s.contains("rlm_query")); |
| 166 | } |
| 167 | |
| 168 | #[test] |
| 169 | fn rlm_prompt_is_finalize_only() { |
| 170 | let s = body(); |
| 171 | assert!(s.contains("finalize(value")); |
| 172 | assert!(!s.contains("FINAL_VAR")); |
| 173 | assert!(!s.contains("FINAL(value)")); |
| 174 | assert!(!s.contains("FINAL(")); |
| 175 | } |
| 176 | |
| 177 | #[test] |
| 178 | fn rlm_prompt_requires_deterministic_counts_and_coverage() { |
| 179 | let s = body(); |
| 180 | assert!(s.contains("compute with Python")); |
| 181 | assert!(s.contains("include coverage")); |
| 182 | assert!(s.contains("chunks processed")); |
| 183 | } |
| 184 | |
| 185 | #[test] |
| 186 | fn rlm_prompt_requires_batch_dependency_safety() { |
| 187 | let s = body(); |
| 188 | assert!(s.contains("dependency_mode=\"independent\"")); |
| 189 | assert!(s.contains("sub_query_sequence")); |
| 190 | assert!(s.contains("database or schema migrations")); |
| 191 | assert!(s.contains("rollback-sensitive")); |
| 192 | } |
| 193 | |
| 194 | #[test] |
| 195 | fn rlm_prompt_mentions_symbolic_state_contract() { |
| 196 | let s = body(); |
| 197 | assert!(s.contains("symbolic recursion")); |
| 198 | assert!(s.contains("REPL variables")); |
| 199 | assert!(s.contains("Do not copy the whole input")); |
| 200 | } |
| 201 | } |
| 202 |