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
177/// Inject the agent-swarm reminder into the developer instructions while
178/// agent-swarm mode is active, so any app-server/SDK client (not just the TUI)
179/// nudges the model toward the `agent_swarm` fanout tool.
180pub fn apply_agent_swarm_mode(mut instructions: InstructionBundle) -> InstructionBundle {
181    let reminder = roder_api::subagents::AGENT_SWARM_MODE_REMINDER;
182    instructions.developer = Some(match instructions.developer {
183        Some(existing) if !existing.trim().is_empty() => format!("{existing}\n\n{reminder}"),
184        _ => reminder.to_string(),
185    });
186    instructions
187}
188
189pub fn apply_model_instruction_overlay(
190    mut instructions: InstructionBundle,
191    profile: &ModelHarnessProfile,
192) -> InstructionBundle {
193    let addition = match profile.instruction_overlay {
194        ModelInstructionOverlay::Standard => return instructions,
195        ModelInstructionOverlay::LiteralToolOutputs => LITERAL_TOOL_OUTPUTS_OVERLAY,
196        ModelInstructionOverlay::IntuitiveContext => INTUITIVE_CONTEXT_OVERLAY,
197    };
198    instructions.developer = Some(match instructions.developer {
199        Some(existing) if !existing.trim().is_empty() => format!("{existing}\n\n{addition}"),
200        _ => addition.to_string(),
201    });
202    instructions
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn agent_swarm_mode_injects_reminder_into_developer_instructions() {
211        let injected = apply_agent_swarm_mode(InstructionBundle::default());
212        let developer = injected.developer.expect("developer instructions");
213        assert!(developer.contains("agent_swarm"));
214        assert!(developer.contains("{{item}}"));
215
216        // Appends to existing developer instructions rather than replacing them.
217        let with_existing = apply_agent_swarm_mode(InstructionBundle {
218            developer: Some("existing dev rules".to_string()),
219            ..InstructionBundle::default()
220        });
221        let developer = with_existing.developer.expect("developer instructions");
222        assert!(developer.starts_with("existing dev rules"));
223        assert!(developer.contains("agent_swarm"));
224    }
225
226    #[test]
227    fn base_instructions_name_lazy_discovery_tools() {
228        let instructions = default_instructions();
229        let system = instructions.system.expect("system instructions");
230        assert!(system.contains("discovery.list"));
231        assert!(system.contains("discovery.search"));
232        assert!(system.contains("discovery.read"));
233        assert!(system.contains("promotes its detailed schema"));
234    }
235
236    #[test]
237    fn windows_instructions_match_host_platform() {
238        let instructions = default_instructions();
239        let system = instructions.system.expect("system instructions");
240        assert_eq!(
241            system.contains("You are running on Windows."),
242            cfg!(target_os = "windows")
243        );
244        assert_eq!(
245            system.contains("Prefer PowerShell commands"),
246            cfg!(target_os = "windows")
247        );
248        assert_eq!(
249            system.contains(
250                "does not supersede the instruction to prefer dedicated file-editing tools"
251            ),
252            cfg!(target_os = "windows")
253        );
254    }
255
256    #[test]
257    fn base_instructions_prefer_edit_tools_for_source_changes() {
258        let instructions = default_instructions();
259        let system = instructions.system.expect("system instructions");
260        assert!(system.contains("Prefer dedicated file-editing tools"));
261        assert!(system.contains("apply_patch"));
262    }
263
264    #[test]
265    fn plan_mode_instructions_tell_model_to_request_approval() {
266        let instructions = apply_plan_mode(InstructionBundle::default());
267        let developer = instructions.developer.expect("developer instructions");
268        assert!(developer.contains("You are in plan mode"));
269        assert!(developer.contains("exit_plan_mode"));
270        assert!(developer.contains("After the user approves"));
271    }
272
273    #[test]
274    fn thread_developer_instructions_prepend_to_developer_slot() {
275        let instructions = apply_runtime_profile(
276            apply_thread_developer_instructions(
277                default_instructions(),
278                "You are embedded in a host app.",
279            ),
280            RuntimeProfile::NonInteractive,
281        );
282        let developer = instructions.developer.expect("developer instructions");
283        assert!(developer.starts_with("You are embedded in a host app."));
284        assert!(developer.contains("non-interactive profile"));
285        assert!(
286            instructions
287                .system
288                .expect("system")
289                .starts_with("You are Roder")
290        );
291
292        let unchanged = apply_thread_developer_instructions(default_instructions(), "   ");
293        assert_eq!(unchanged.developer, None);
294    }
295
296    #[test]
297    fn turn_developer_context_fills_dedicated_slot_after_thread_instructions() {
298        let instructions = apply_turn_developer_context(
299            apply_runtime_profile(
300                apply_thread_developer_instructions(
301                    default_instructions(),
302                    "You are embedded in a host app.",
303                ),
304                RuntimeProfile::NonInteractive,
305            ),
306            "Connected accounts: example-service.",
307        );
308        let developer = instructions.developer.expect("developer instructions");
309        assert!(developer.starts_with("You are embedded in a host app."));
310        assert!(!developer.contains("Connected accounts"));
311        assert_eq!(
312            instructions.developer_context.as_deref(),
313            Some("Connected accounts: example-service.")
314        );
315
316        let unchanged = apply_turn_developer_context(default_instructions(), "   ");
317        assert_eq!(unchanged.developer_context, None);
318    }
319
320    #[test]
321    fn base_instructions_include_intermediary_message_guidance() {
322        let instructions = default_instructions();
323        let system = instructions.system.expect("system instructions");
324        assert!(system.starts_with("You are Roder"));
325        assert!(system.contains("Before making tool calls, send a brief preamble message"));
326        assert!(system.contains("Group related tool calls under one concise preamble"));
327        assert!(system.contains("Build on prior context in later preambles"));
328    }
329}