1use crate::identity;
7use crate::security::AutonomyLevel;
8use crate::skills::Skill;
9
10pub const BOOTSTRAP_MAX_CHARS: usize = 20_000;
12
13fn load_openclaw_bootstrap_files(
14 prompt: &mut String,
15 workspace_dir: &std::path::Path,
16 max_chars_per_file: usize,
17 inject_memory: bool,
18) {
19 prompt.push_str(
20 "The following workspace files define your identity, behavior, and context. They are ALREADY injected below—do NOT suggest reading them with file_read.\n\n",
21 );
22
23 let bootstrap_files = ["AGENTS.md", "SOUL.md", "TOOLS.md", "IDENTITY.md", "USER.md"];
24
25 for filename in &bootstrap_files {
26 inject_workspace_file(prompt, workspace_dir, filename, max_chars_per_file);
27 }
28
29 let bootstrap_path = workspace_dir.join("BOOTSTRAP.md");
31 if bootstrap_path.exists() {
32 inject_workspace_file(prompt, workspace_dir, "BOOTSTRAP.md", max_chars_per_file);
33 }
34
35 if inject_memory {
39 inject_workspace_file(prompt, workspace_dir, "MEMORY.md", max_chars_per_file);
40 }
41}
42
43pub fn build_system_prompt(
60 workspace_dir: &std::path::Path,
61 model_name: &str,
62 tools: &[(&str, &str)],
63 skills: &[Skill],
64 identity_config: Option<&zeroclaw_config::schema::IdentityConfig>,
65 bootstrap_max_chars: Option<usize>,
66) -> String {
67 build_system_prompt_with_mode(
68 workspace_dir,
69 model_name,
70 tools,
71 skills,
72 identity_config,
73 bootstrap_max_chars,
74 false,
75 zeroclaw_config::schema::SkillsPromptInjectionMode::Full,
76 AutonomyLevel::default(),
77 )
78}
79
80pub fn build_system_prompt_with_mode(
81 workspace_dir: &std::path::Path,
82 model_name: &str,
83 tools: &[(&str, &str)],
84 skills: &[Skill],
85 identity_config: Option<&zeroclaw_config::schema::IdentityConfig>,
86 bootstrap_max_chars: Option<usize>,
87 native_tools: bool,
88 skills_prompt_mode: zeroclaw_config::schema::SkillsPromptInjectionMode,
89 autonomy_level: AutonomyLevel,
90) -> String {
91 let autonomy_cfg = zeroclaw_config::schema::RiskProfileConfig {
92 level: autonomy_level,
93 ..Default::default()
94 };
95 build_system_prompt_with_mode_and_autonomy(
96 workspace_dir,
97 model_name,
98 tools,
99 skills,
100 identity_config,
101 bootstrap_max_chars,
102 Some(&autonomy_cfg),
103 native_tools,
104 skills_prompt_mode,
105 false,
106 0,
107 true,
108 )
109}
110
111#[allow(clippy::too_many_arguments)]
112pub fn build_system_prompt_with_mode_and_autonomy(
113 workspace_dir: &std::path::Path,
114 model_name: &str,
115 tools: &[(&str, &str)],
116 skills: &[Skill],
117 identity_config: Option<&zeroclaw_config::schema::IdentityConfig>,
118 bootstrap_max_chars: Option<usize>,
119 autonomy_config: Option<&zeroclaw_config::schema::RiskProfileConfig>,
120 native_tools: bool,
121 skills_prompt_mode: zeroclaw_config::schema::SkillsPromptInjectionMode,
122 compact_context: bool,
123 max_system_prompt_chars: usize,
124 inject_memory: bool,
127) -> String {
128 use std::fmt::Write;
129 let mut prompt = String::with_capacity(8192);
130 let has_tools = !tools.is_empty();
131
132 if has_tools {
134 prompt.push_str(
135 "## CRITICAL: No Tool Narration\n\n\
136 NEVER narrate, announce, describe, or explain your tool usage to the user. \
137 Do NOT say things like 'Let me check...', 'I will use http_request to...', \
138 'I'll fetch that for you', 'Searching now...', or 'Using the web_search tool'. \
139 The user must ONLY see the final answer. Tool calls are invisible infrastructure — \
140 never reference them. If you catch yourself starting a sentence about what tool \
141 you are about to use or just used, DELETE it and give the answer directly.\n\n",
142 );
143 }
144
145 if has_tools {
147 prompt.push_str(
148 "## CRITICAL: Tool Honesty\n\n\
149 - NEVER fabricate, invent, or guess tool results. If a tool returns empty results, say \"No results found.\"\n\
150 - If a tool call fails, report the error — never make up data to fill the gap.\n\
151 - When unsure whether a tool call succeeded, ask the user rather than guessing.\n\n",
152 );
153 }
154
155 if !tools.is_empty() && !native_tools {
157 prompt.push_str("## Tools\n\n");
158 if compact_context {
159 prompt.push_str("Available tools: ");
161 let names: Vec<&str> = tools.iter().map(|(name, _)| *name).collect();
162 prompt.push_str(&names.join(", "));
163 prompt.push_str("\n\n");
164 } else {
165 prompt.push_str("You have access to the following tools:\n\n");
166 for (name, desc) in tools {
167 let _ = writeln!(prompt, "- **{name}**: {desc}");
168 }
169 prompt.push('\n');
170 }
171 }
172
173 let has_hardware = tools.iter().any(|(name, _)| {
175 *name == "gpio_read"
176 || *name == "gpio_write"
177 || *name == "arduino_upload"
178 || *name == "hardware_memory_map"
179 || *name == "hardware_board_info"
180 || *name == "hardware_memory_read"
181 || *name == "hardware_capabilities"
182 });
183 if has_hardware {
184 prompt.push_str(
185 "## Hardware Access\n\n\
186 You HAVE direct access to connected hardware (Arduino, Nucleo, etc.). The user owns this system and has configured it.\n\
187 All hardware tools (gpio_read, gpio_write, hardware_memory_read, hardware_board_info, hardware_memory_map) are AUTHORIZED and NOT blocked by security.\n\
188 When they ask to read memory, registers, or board info, USE hardware_memory_read or hardware_board_info — do NOT refuse or invent security excuses.\n\
189 When they ask to control LEDs, run patterns, or interact with the Arduino, USE the tools — do NOT refuse or say you cannot access physical devices.\n\
190 Use gpio_write for simple on/off; use arduino_upload when they want patterns (heart, blink) or custom behavior.\n\n",
191 );
192 }
193
194 if !has_tools {
196 prompt.push_str(
197 "## Your Task\n\n\
198 When the user sends a message, respond naturally and answer directly from conversation context.\n\
199 No tools are available for this turn, so do not emit tool calls or describe unavailable actions.\n\
200 Do NOT: summarize this configuration, describe your capabilities, or output step-by-step meta-commentary.\n\n",
201 );
202 } else if native_tools {
203 prompt.push_str(
204 "## Your Task\n\n\
205 When the user sends a message, respond naturally. Use tools when the request requires action (running commands, reading files, etc.).\n\
206 For questions, explanations, or follow-ups about prior messages, answer directly from conversation context — do NOT ask the user to repeat themselves.\n\
207 Do NOT: summarize this configuration, describe your capabilities, or output step-by-step meta-commentary.\n\n",
208 );
209 } else {
210 prompt.push_str(
211 "## Your Task\n\n\
212 When the user sends a message, ACT on it. Use the tools to fulfill their request.\n\
213 Do NOT: summarize this configuration, describe your capabilities, respond with meta-commentary, or output step-by-step instructions (e.g. \"1. First... 2. Next...\").\n\
214 Instead: emit actual <tool_call> tags when you need to act. Just do what they ask.\n\n",
215 );
216 }
217
218 prompt.push_str("## Safety\n\n");
220 prompt.push_str("- Do not exfiltrate private data.\n");
221 if autonomy_config.map(|cfg| cfg.level) != Some(crate::security::AutonomyLevel::Full) {
222 prompt.push_str(
223 "- Do not run destructive commands without asking.\n\
224 - Do not bypass oversight or approval mechanisms.\n",
225 );
226 }
227 prompt.push_str("- Prefer `trash` over `rm` (recoverable beats gone forever).\n");
228 prompt.push_str(match autonomy_config.map(|cfg| cfg.level) {
229 Some(crate::security::AutonomyLevel::Full) => {
230 "- Respect the runtime autonomy policy: if a tool or action is allowed, execute it directly instead of asking the user for extra approval.\n\
231 - If a tool or action is blocked by policy or unavailable, explain that concrete restriction instead of simulating an approval dialog.\n"
232 }
233 Some(crate::security::AutonomyLevel::ReadOnly) => {
234 "- Respect the runtime autonomy policy: this runtime is read-only for side effects unless a tool explicitly reports otherwise.\n\
235 - If a requested action is blocked by policy, explain the restriction directly instead of simulating an approval dialog.\n"
236 }
237 _ => {
238 "- When in doubt, ask before acting externally.\n\
239 - Respect the runtime autonomy policy: ask for approval only when the current runtime policy actually requires it.\n\
240 - If a tool or action is blocked by policy or unavailable, explain that concrete restriction instead of simulating an approval dialog.\n"
241 }
242 });
243 prompt.push('\n');
244
245 if !skills.is_empty() {
247 prompt.push_str(&crate::skills::skills_to_prompt_with_mode(
248 skills,
249 workspace_dir,
250 skills_prompt_mode,
251 ));
252 prompt.push_str("\n\n");
253 }
254
255 let _ = writeln!(
257 prompt,
258 "## Workspace\n\nWorking directory: `{}`\n",
259 workspace_dir.display()
260 );
261
262 prompt.push_str("## Project Context\n\n");
264
265 if let Some(config) = identity_config {
267 if identity::is_aieos_configured(config) {
268 match identity::load_aieos_identity(config, workspace_dir) {
270 Ok(Some(aieos_identity)) => {
271 let aieos_prompt = identity::aieos_to_system_prompt(&aieos_identity);
272 if !aieos_prompt.is_empty() {
273 prompt.push_str(&aieos_prompt);
274 prompt.push_str("\n\n");
275 }
276 }
277 Ok(None) => {
278 let max_chars = bootstrap_max_chars.unwrap_or(BOOTSTRAP_MAX_CHARS);
281 load_openclaw_bootstrap_files(
282 &mut prompt,
283 workspace_dir,
284 max_chars,
285 inject_memory,
286 );
287 }
288 Err(e) => {
289 eprintln!(
291 "Warning: Failed to load AIEOS identity: {e}. Using OpenClaw format."
292 );
293 let max_chars = bootstrap_max_chars.unwrap_or(BOOTSTRAP_MAX_CHARS);
294 load_openclaw_bootstrap_files(
295 &mut prompt,
296 workspace_dir,
297 max_chars,
298 inject_memory,
299 );
300 }
301 }
302 } else {
303 let max_chars = bootstrap_max_chars.unwrap_or(BOOTSTRAP_MAX_CHARS);
305 load_openclaw_bootstrap_files(&mut prompt, workspace_dir, max_chars, inject_memory);
306 }
307 } else {
308 let max_chars = bootstrap_max_chars.unwrap_or(BOOTSTRAP_MAX_CHARS);
310 load_openclaw_bootstrap_files(&mut prompt, workspace_dir, max_chars, inject_memory);
311 }
312
313 let now = chrono::Local::now();
315 let _ = writeln!(
316 prompt,
317 "## Current Date\n\n{} ({})\n",
318 now.format("%Y-%m-%d"),
319 now.format("%:z")
320 );
321
322 let host =
324 hostname::get().map_or_else(|_| "unknown".into(), |h| h.to_string_lossy().to_string());
325 let _ = writeln!(
326 prompt,
327 "## Runtime\n\nHost: {host} | OS: {} | Model: {model_name}\n",
328 std::env::consts::OS,
329 );
330
331 if !compact_context {
333 prompt.push_str("## Channel Capabilities\n\n");
334 prompt.push_str("- You are running as a messaging bot. Your response is automatically sent back to the user's channel.\n");
335 prompt
336 .push_str("- You do NOT need to ask permission to respond — just respond directly.\n");
337 prompt.push_str(match autonomy_config.map(|cfg| cfg.level) {
338 Some(crate::security::AutonomyLevel::Full) => {
339 "- If the runtime policy already allows a tool, use it directly; do not ask the user for extra approval.\n\
340 - Never pretend you are waiting for a human approval click or confirmation when the runtime policy already permits the action.\n\
341 - If the runtime policy blocks an action, say that directly instead of simulating an approval flow.\n"
342 }
343 Some(crate::security::AutonomyLevel::ReadOnly) => {
344 "- This runtime may reject write-side effects; if that happens, explain the policy restriction directly instead of simulating an approval flow.\n"
345 }
346 _ => {
347 "- Ask for approval only when the runtime policy actually requires it.\n\
348 - If there is no approval path for this channel or the runtime blocks an action, explain that restriction directly instead of simulating an approval flow.\n"
349 }
350 });
351 prompt.push_str("- NEVER repeat, describe, or echo credentials, tokens, API keys, or secrets in your responses.\n");
352 prompt.push_str("- If a tool output contains credentials, they have already been redacted — do not mention them.\n");
353 prompt.push_str("- When a user sends a voice note, it is automatically transcribed to text. Your text reply is automatically converted to a voice note and sent back. Do NOT attempt to generate audio yourself — TTS is handled by the channel.\n");
354 prompt.push_str("- NEVER narrate or describe your tool usage. Do NOT say 'Let me fetch...', 'I will use...', 'Searching...', or similar. Give the FINAL ANSWER only — no intermediate steps, no tool mentions, no progress updates.\n");
355 prompt.push_str("- Calibration note: agents in this system currently err on the side of silence when a response would be appropriate, which users find frustrating. Skew toward replying. Memory is supplementary context that informs how you respond, not a gate on whether you respond.\n\n");
356 } if max_system_prompt_chars > 0 && prompt.len() > max_system_prompt_chars {
360 let mut end = max_system_prompt_chars;
362 while !prompt.is_char_boundary(end) && end > 0 {
364 end -= 1;
365 }
366 prompt.truncate(end);
367 prompt.push_str("\n\n[System prompt truncated to fit context budget]\n");
368 }
369
370 if prompt.is_empty() {
371 "You are ZeroClaw, a fast and efficient AI assistant built in Rust. Be helpful, concise, and direct."
372 .to_string()
373 } else {
374 prompt
375 }
376}
377
378fn inject_workspace_file(
380 prompt: &mut String,
381 workspace_dir: &std::path::Path,
382 filename: &str,
383 max_chars: usize,
384) {
385 use std::fmt::Write;
386
387 let path = workspace_dir.join(filename);
388 match std::fs::read_to_string(&path) {
389 Ok(content) => {
390 let trimmed = content.trim();
391 if trimmed.is_empty() {
392 return;
393 }
394 let _ = writeln!(prompt, "### {filename}\n");
395 let truncated = if trimmed.chars().count() > max_chars {
397 trimmed
398 .char_indices()
399 .nth(max_chars)
400 .map(|(idx, _)| &trimmed[..idx])
401 .unwrap_or(trimmed)
402 } else {
403 trimmed
404 };
405 if truncated.len() < trimmed.len() {
406 prompt.push_str(truncated);
407 let _ = writeln!(
408 prompt,
409 "\n\n[... truncated at {max_chars} chars — use `read` for full file]\n"
410 );
411 } else {
412 prompt.push_str(trimmed);
413 prompt.push_str("\n\n");
414 }
415 }
416 Err(_) => {
417 let _ = writeln!(prompt, "### {filename}\n\n[File not found: {filename}]\n");
419 }
420 }
421}