Skip to main content

roder_core/
instructions.rs

1use roder_api::inference::{
2    InstructionBundle, ModelHarnessProfile, ModelInstructionOverlay, RuntimeProfile,
3};
4
5pub const RODER_INSTRUCTIONS: &str = r#"You are Roder, a Rust-native coding agent running inside a terminal TUI on a user's computer.
6
7Roder is inspired by OpenAI Codex and the original Gode agent harness. Within this context, "Roder" refers to this open-source coding-agent harness and TUI, not a language model.
8
9## How You Work
10
11- Be precise, safe, and helpful.
12- Keep responses concise and direct unless the user asks for detail.
13- Prefer actionable guidance and concrete next steps.
14- Continue working until the user's coding task is genuinely handled.
15- Use the tools provided by the harness to inspect files, search the workspace, and make progress.
16
17## Workspace And Tools
18
19- Treat the current workspace as the user's repository.
20- When searching for text or files, prefer fast targeted search. If a search tool is available, use it before broad manual inspection.
21- Read relevant files before making assumptions about the codebase.
22- Keep edits scoped to the user's request and consistent with existing project patterns.
23- The available tool set depends on how this Roder thread is configured. Do not claim access to tools that are not exposed in the current turn.
24- Roder exposes tools through its Responses namespace and tool search. Use the available tool names directly; do not assume a legacy functions namespace or that only the most recently used tool is available.
25- When discovery tools are available, use `discovery.list`, `discovery.search`, or `discovery.read` before using unfamiliar tools, MCP servers, skills, commands, plugins, subagents, or file-backed artifact surfaces. Reading a discovery item promotes its detailed schema or instructions for the thread.
26
27## Editing Constraints
28
29- Default to ASCII when editing or creating files. Only introduce non-ASCII when clearly justified or when the file already uses it.
30- Add succinct comments only when they clarify non-obvious logic.
31- Prefer dedicated file-editing tools (`apply_patch`, `edit`, `multi_edit`, or `write_file`) for source changes when they are available. Use shell commands for editing only when the requested edit cannot be expressed with the available editing tools or those tools fail.
32- Do not revert changes you did not make unless the user explicitly asks.
33- You may be in a dirty git worktree. Ignore unrelated work from other agents or the user.
34- Do not use destructive operations such as hard resets or deleting user work unless explicitly requested.
35
36## Validation
37
38- When you change code, run the most relevant tests or build commands available for the touched area.
39- Start with focused checks, then broaden when confidence increases.
40- If you cannot run a useful validation command, say exactly what was not verified and why.
41
42## Communication
43
44- Before making tool calls, send a brief preamble message to the user explaining what you are about to do.
45- Group related tool calls under one concise preamble instead of narrating every trivial read separately.
46- Keep preambles to 1-2 sentences focused on immediate, tangible next steps; for quick updates, aim for 8-12 words.
47- Build on prior context in later preambles so the user can follow progress and understand the next action.
48- Explain what changed and why in plain engineering language.
49- If an operation fails, surface the key error and the likely next debugging step.
50- Avoid dumping large files or logs into the response; summarize and reference paths where useful."#;
51
52pub fn default_instructions() -> InstructionBundle {
53    let mut system = RODER_INSTRUCTIONS.to_string();
54    if cfg!(target_os = "windows") {
55        system.push_str(WINDOWS_SYSTEM_INSTRUCTIONS);
56    }
57    InstructionBundle {
58        system: Some(system),
59        developer: None,
60        developer_context: None,
61    }
62}
63
64const WINDOWS_SYSTEM_INSTRUCTIONS: &str = r#"
65
66## Windows Runtime
67
68- You are running on Windows.
69- Prefer PowerShell commands and PowerShell syntax for shell operations. This shell guidance does not supersede the instruction to prefer dedicated file-editing tools for source changes.
70- Use Windows paths and commands when referring to files, processes, environment variables, and filesystem operations."#;
71
72const NON_INTERACTIVE_INSTRUCTIONS: &str = r#"## Runtime Profile
73
74This turn is running in a non-interactive profile. Do not wait for unavailable user clarification. Assume reasonable defaults, state assumptions briefly when needed, and continue to a concrete final result."#;
75
76const EVAL_INSTRUCTIONS: &str = r#"## Eval Runtime Profile
77
78This turn is running in eval mode. Do not wait for user clarification unless explicit fixture answers are available. Assume reasonable defaults, keep progress observable through tools and events, and reach a final answer only after the task has been handled."#;
79
80const TASK_LEDGER_REQUIRED_INSTRUCTIONS: &str = r#"## Task Ledger Required
81
82This eval task is decomposed work. The first tool call must be `task_ledger.update`; do not call shell, search, web, file, or edit tools before the ledger exists. Keep exactly one item in progress and include evidence when marking items completed."#;
83
84const PLAN_MODE_INSTRUCTIONS: &str = r#"## Plan Mode
85
86You are in plan mode. Do not make file changes or run implementation commands yet. Inspect and discuss as needed, then present a concrete implementation plan to the user.
87
88When the plan is ready for approval, call `exit_plan_mode` with:
89- `summary`: the user-visible plan, in Markdown.
90- `next_steps`: concise implementation steps.
91- `target_mode`: `default` unless the user explicitly asked for a more permissive mode.
92
93After the user approves, the harness exits plan mode and the turn may continue with implementation. If the user rejects, keep discussing and revise the plan."#;
94
95const LITERAL_TOOL_OUTPUTS_OVERLAY: &str = r#"## Model Harness Profile
96
97Tool outputs are literal evidence from the harness. Prefer exact filenames, command output, and structured tool results over inferred state."#;
98
99const INTUITIVE_CONTEXT_OVERLAY: &str = r#"## Model Harness Profile
100
101Use the provided context as the current working set. Ask for or inspect missing files before assuming project structure outside the visible evidence."#;
102
103/// Prepends host-supplied thread instructions to the developer slot so they layer directly under
104/// the harness system prompt while harness addenda (runtime profile, plan mode, overlays) append
105/// after them.
106pub fn apply_thread_developer_instructions(
107    mut instructions: InstructionBundle,
108    addition: &str,
109) -> InstructionBundle {
110    let addition = addition.trim();
111    if addition.is_empty() {
112        return instructions;
113    }
114    instructions.developer = Some(match instructions.developer {
115        Some(existing) if !existing.trim().is_empty() => format!("{addition}\n\n{existing}"),
116        _ => addition.to_string(),
117    });
118    instructions
119}
120
121/**
122 * Sets the per-turn developer-context slot. Supplied on turn/start only and
123 * never persisted, so a context delivered on turn N is absent on turn N+1
124 * unless the host sends it again. Providers render the slot after all stable
125 * instruction content — through a provider-native per-turn channel (e.g. a
126 * trailing system-role message) where available — so cached stable-prefix
127 * blocks survive per-turn changes.
128 */
129pub fn apply_turn_developer_context(
130    mut instructions: InstructionBundle,
131    context: &str,
132) -> InstructionBundle {
133    let context = context.trim();
134    if context.is_empty() {
135        return instructions;
136    }
137    instructions.developer_context = Some(context.to_string());
138    instructions
139}
140
141pub fn apply_runtime_profile(
142    mut instructions: InstructionBundle,
143    profile: RuntimeProfile,
144) -> InstructionBundle {
145    let addition = match profile {
146        RuntimeProfile::Interactive => return instructions,
147        RuntimeProfile::NonInteractive => NON_INTERACTIVE_INSTRUCTIONS,
148        RuntimeProfile::Eval => EVAL_INSTRUCTIONS,
149    };
150    instructions.developer = Some(match instructions.developer {
151        Some(existing) if !existing.trim().is_empty() => format!("{existing}\n\n{addition}"),
152        _ => addition.to_string(),
153    });
154    instructions
155}
156
157pub fn apply_task_ledger_required(mut instructions: InstructionBundle) -> InstructionBundle {
158    instructions.developer = Some(match instructions.developer {
159        Some(existing) if !existing.trim().is_empty() => {
160            format!("{existing}\n\n{TASK_LEDGER_REQUIRED_INSTRUCTIONS}")
161        }
162        _ => TASK_LEDGER_REQUIRED_INSTRUCTIONS.to_string(),
163    });
164    instructions
165}
166
167pub fn apply_plan_mode(mut instructions: InstructionBundle) -> InstructionBundle {
168    instructions.developer = Some(match instructions.developer {
169        Some(existing) if !existing.trim().is_empty() => {
170            format!("{existing}\n\n{PLAN_MODE_INSTRUCTIONS}")
171        }
172        _ => PLAN_MODE_INSTRUCTIONS.to_string(),
173    });
174    instructions
175}
176
177pub fn apply_model_instruction_overlay(
178    mut instructions: InstructionBundle,
179    profile: &ModelHarnessProfile,
180) -> InstructionBundle {
181    let addition = match profile.instruction_overlay {
182        ModelInstructionOverlay::Standard => return instructions,
183        ModelInstructionOverlay::LiteralToolOutputs => LITERAL_TOOL_OUTPUTS_OVERLAY,
184        ModelInstructionOverlay::IntuitiveContext => INTUITIVE_CONTEXT_OVERLAY,
185    };
186    instructions.developer = Some(match instructions.developer {
187        Some(existing) if !existing.trim().is_empty() => format!("{existing}\n\n{addition}"),
188        _ => addition.to_string(),
189    });
190    instructions
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn base_instructions_name_lazy_discovery_tools() {
199        let instructions = default_instructions();
200        let system = instructions.system.expect("system instructions");
201        assert!(system.contains("discovery.list"));
202        assert!(system.contains("discovery.search"));
203        assert!(system.contains("discovery.read"));
204        assert!(system.contains("promotes its detailed schema"));
205    }
206
207    #[test]
208    fn windows_instructions_match_host_platform() {
209        let instructions = default_instructions();
210        let system = instructions.system.expect("system instructions");
211        assert_eq!(
212            system.contains("You are running on Windows."),
213            cfg!(target_os = "windows")
214        );
215        assert_eq!(
216            system.contains("Prefer PowerShell commands"),
217            cfg!(target_os = "windows")
218        );
219        assert_eq!(
220            system.contains(
221                "does not supersede the instruction to prefer dedicated file-editing tools"
222            ),
223            cfg!(target_os = "windows")
224        );
225    }
226
227    #[test]
228    fn base_instructions_prefer_edit_tools_for_source_changes() {
229        let instructions = default_instructions();
230        let system = instructions.system.expect("system instructions");
231        assert!(system.contains("Prefer dedicated file-editing tools"));
232        assert!(system.contains("apply_patch"));
233    }
234
235    #[test]
236    fn plan_mode_instructions_tell_model_to_request_approval() {
237        let instructions = apply_plan_mode(InstructionBundle::default());
238        let developer = instructions.developer.expect("developer instructions");
239        assert!(developer.contains("You are in plan mode"));
240        assert!(developer.contains("exit_plan_mode"));
241        assert!(developer.contains("After the user approves"));
242    }
243
244    #[test]
245    fn thread_developer_instructions_prepend_to_developer_slot() {
246        let instructions = apply_runtime_profile(
247            apply_thread_developer_instructions(
248                default_instructions(),
249                "You are embedded in a host app.",
250            ),
251            RuntimeProfile::NonInteractive,
252        );
253        let developer = instructions.developer.expect("developer instructions");
254        assert!(developer.starts_with("You are embedded in a host app."));
255        assert!(developer.contains("non-interactive profile"));
256        assert!(
257            instructions
258                .system
259                .expect("system")
260                .starts_with("You are Roder")
261        );
262
263        let unchanged = apply_thread_developer_instructions(default_instructions(), "   ");
264        assert_eq!(unchanged.developer, None);
265    }
266
267    #[test]
268    fn turn_developer_context_fills_dedicated_slot_after_thread_instructions() {
269        let instructions = apply_turn_developer_context(
270            apply_runtime_profile(
271                apply_thread_developer_instructions(
272                    default_instructions(),
273                    "You are embedded in a host app.",
274                ),
275                RuntimeProfile::NonInteractive,
276            ),
277            "Connected accounts: example-service.",
278        );
279        let developer = instructions.developer.expect("developer instructions");
280        assert!(developer.starts_with("You are embedded in a host app."));
281        assert!(!developer.contains("Connected accounts"));
282        assert_eq!(
283            instructions.developer_context.as_deref(),
284            Some("Connected accounts: example-service.")
285        );
286
287        let unchanged = apply_turn_developer_context(default_instructions(), "   ");
288        assert_eq!(unchanged.developer_context, None);
289    }
290
291    #[test]
292    fn base_instructions_include_intermediary_message_guidance() {
293        let instructions = default_instructions();
294        let system = instructions.system.expect("system instructions");
295        assert!(system.starts_with("You are Roder"));
296        assert!(system.contains("Before making tool calls, send a brief preamble message"));
297        assert!(system.contains("Group related tool calls under one concise preamble"));
298        assert!(system.contains("Build on prior context in later preambles"));
299    }
300}