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
103pub 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
121pub 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_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 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}