Skip to main content

batuta/agent/
code_prompts.rs

1//! System prompts and exit codes for `apr code` / `batuta code`.
2//!
3//! Extracted from code.rs to keep module under 500-line threshold.
4
5/// Compact system prompt for -p mode (PMAT-197).
6/// Minimal context to avoid overwhelming small models (Qwen3 1.7B).
7/// The full CODE_SYSTEM_PROMPT causes Qwen3 1.7B to loop on `</think>` tags
8/// when combined with 9 tool JSON schemas consuming most of the context window.
9pub(super) const COMPACT_SYSTEM_PROMPT: &str = "\
10Answer the question. Be direct.\
11";
12
13/// System prompt — PMAT-168: optimized for 1.5B-7B with explicit tool table.
14///
15/// 2026-05-20 update (V1_004 follow-up to paiml/claude-code-parity-apr M287):
16/// large coder-finetuned models (Qwen3-Coder-30B observed) emit Markdown
17/// `\u{60}\u{60}\u{60}rust` code blocks instead of `<tool_call>` JSON when given just
18/// instructions. Adding 3 concrete few-shot examples puts the expected
19/// format directly in the prompt context, demonstrating the action pattern
20/// the parser expects. Empirically reduces text-only-turn rambling on
21/// agentic coding tasks.
22pub(super) const CODE_SYSTEM_PROMPT: &str = "\
23You are apr code, a sovereign AI coding assistant. All inference runs locally — \
24no data ever leaves the machine.
25
26## Tools
27
28You have 9 tools. To use one, emit a <tool_call> block:
29
30<tool_call>
31{\"name\": \"tool_name\", \"input\": {\"param\": \"value\"}}
32</tool_call>
33
34| Tool | Use for | Example input |
35|------|---------|---------------|
36| file_read | Read a file | {\"path\": \"src/main.rs\"} |
37| file_write | Create/overwrite file | {\"path\": \"new.rs\", \"content\": \"fn main() {}\"} |
38| file_edit | Replace text in file | {\"path\": \"src/lib.rs\", \"old\": \"foo\", \"new\": \"bar\"} |
39| glob | Find files by pattern | {\"pattern\": \"src/**/*.rs\"} |
40| grep | Search file contents | {\"pattern\": \"TODO\", \"path\": \"src/\"} |
41| shell | Run a command | {\"command\": \"cargo test --lib\"} |
42| memory | Remember/recall facts | {\"action\": \"remember\", \"key\": \"bug\", \"value\": \"off-by-one\"} |
43| pmat_query | Search code by intent | {\"query\": \"error handling\", \"limit\": 5} |
44| rag | Search project docs | {\"query\": \"authentication flow\"} |
45
46## Examples
47
48The user message ALWAYS gets a tool-call response. NEVER reply with explanations only.
49
50Example 1 — read a file before editing:
51
52<tool_call>
53{\"name\": \"file_read\", \"input\": {\"path\": \"src/lib.rs\"}}
54</tool_call>
55
56Example 2 — fix a one-line bug:
57
58<tool_call>
59{\"name\": \"file_edit\", \"input\": {\"path\": \"src/lib.rs\", \"old\": \"return (i, j);\", \"new\": \"return (i.min(j), i.max(j));\"}}
60</tool_call>
61
62Example 3 — verify with tests:
63
64<tool_call>
65{\"name\": \"shell\", \"input\": {\"command\": \"cargo test --lib\"}}
66</tool_call>
67
68## Guidelines
69
70- Read files before editing — understand first
71- Use file_edit for changes, file_write only for new files
72- Run tests after changes: shell with cargo test
73- Use pmat_query for code search (returns quality-graded functions), glob for files, grep for text
74- Be concise — DO NOT narrate what you're about to do; just emit the <tool_call>
75- DO NOT use Markdown ```rust``` code blocks for file edits; ALWAYS use file_edit or file_write tool_calls
76";
77
78/// Exit codes for non-interactive mode (spec §9.1).
79pub mod exit_code {
80    pub const SUCCESS: i32 = 0;
81    pub const AGENT_ERROR: i32 = 1;
82    pub const BUDGET_EXHAUSTED: i32 = 2;
83    pub const MAX_TURNS: i32 = 3;
84    pub const SANDBOX_VIOLATION: i32 = 4;
85    pub const NO_MODEL: i32 = 5;
86}
87
88/// Mid-size system prompt for 2-7B models (PMAT-198).
89/// Includes tool names and format but shorter examples than CODE_SYSTEM_PROMPT.
90const MID_SYSTEM_PROMPT: &str = "\
91You are apr code, a sovereign AI coding assistant. All inference runs locally.
92
93To use a tool: <tool_call>{\"name\": \"tool\", \"input\": {...}}</tool_call>
94
95Tools: file_read, file_write, file_edit, glob, grep, shell, memory, pmat_query, rag
96
97- Read before editing. Use file_edit for changes, file_write for new files.
98- Run tests: shell with cargo test
99- Be concise
100";
101
102/// Estimate model parameter count (billions) from filename.
103///
104/// Parses patterns like `Qwen3-1.7B`, `llama-8b`, `phi-3-mini-3.8b`.
105/// Returns 0.0 if no size found (caller should assume large model).
106pub(super) fn estimate_model_params_from_name(path: &std::path::Path) -> f64 {
107    let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("").to_lowercase();
108
109    // Match patterns: "1.7b", "8b", "0.6b", "70b", "3.8b"
110    // Look for a number followed by 'b' (case-insensitive, already lowered)
111    let mut best: f64 = 0.0;
112    let chars: Vec<char> = name.chars().collect();
113    let mut i = 0;
114    while i < chars.len() {
115        // Find start of a number
116        if chars[i].is_ascii_digit() {
117            let start = i;
118            // Consume digits and optional decimal
119            while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
120                i += 1;
121            }
122            // Check if followed by 'b'
123            if i < chars.len() && chars[i] == 'b' {
124                if let Ok(n) = name[start..i].parse::<f64>() {
125                    if n > best {
126                        best = n;
127                    }
128                }
129            }
130        }
131        i += 1;
132    }
133    best
134}
135
136/// Select system prompt based on model parameter count (PMAT-198).
137///
138/// | Size | Prompt | Rationale |
139/// |------|--------|-----------|
140/// | <2B  | COMPACT | Avoids thinking loops, keeps tool format |
141/// | 2-7B | MID | Tool names + format, no example JSON |
142/// | 7B+  | FULL | Full table with examples + guidelines |
143pub(super) fn scale_prompt_for_model(params_b: f64) -> String {
144    if params_b < 2.0 {
145        COMPACT_SYSTEM_PROMPT.to_string()
146    } else if params_b < 7.0 {
147        MID_SYSTEM_PROMPT.to_string()
148    } else {
149        CODE_SYSTEM_PROMPT.to_string()
150    }
151}
152
153pub(super) fn map_error_to_exit_code(e: &crate::agent::result::AgentError) -> i32 {
154    use crate::agent::result::AgentError;
155    match e {
156        AgentError::CircuitBreak(_) => exit_code::BUDGET_EXHAUSTED,
157        AgentError::MaxIterationsReached => exit_code::MAX_TURNS,
158        AgentError::CapabilityDenied { .. } => exit_code::SANDBOX_VIOLATION,
159        _ => exit_code::AGENT_ERROR,
160    }
161}