1pub mod source;
16
17use std::path::PathBuf;
18use std::sync::Arc;
19
20use tokio_util::sync::CancellationToken;
21use tracing::{debug, info, warn};
22use uuid::Uuid;
23
24use crate::hooks::{HookEvent, HookRegistry};
25use crate::llm::message::*;
26use crate::llm::provider::{Provider, ProviderError, ProviderRequest};
27use crate::llm::stream::StreamEvent;
28use crate::permissions::PermissionChecker;
29use crate::services::compact::{self, CompactTracking, MAX_OUTPUT_TOKENS_RECOVERY_LIMIT};
30use crate::services::tokens;
31use crate::state::AppState;
32use crate::tools::ToolContext;
33use crate::tools::executor::{execute_tool_calls, extract_tool_calls};
34use crate::tools::registry::ToolRegistry;
35
36pub struct QueryEngineConfig {
38 pub max_turns: Option<usize>,
39 pub verbose: bool,
40 pub unattended: bool,
42}
43
44pub struct QueryEngine {
46 llm: Arc<dyn Provider>,
47 tools: ToolRegistry,
48 file_cache: Arc<tokio::sync::Mutex<crate::services::file_cache::FileCache>>,
49 permissions: Arc<PermissionChecker>,
50 state: AppState,
51 config: QueryEngineConfig,
52 cancel: CancellationToken,
53 hooks: HookRegistry,
54 cache_tracker: crate::services::cache_tracking::CacheTracker,
55 denial_tracker: Arc<tokio::sync::Mutex<crate::permissions::tracking::DenialTracker>>,
56 extraction_state: Arc<tokio::sync::Mutex<crate::memory::extraction::ExtractionState>>,
57 session_allows: Arc<tokio::sync::Mutex<std::collections::HashSet<String>>>,
58 permission_prompter: Option<Arc<dyn crate::tools::PermissionPrompter>>,
59}
60
61pub trait StreamSink: Send + Sync {
63 fn on_text(&self, text: &str);
64 fn on_tool_start(&self, tool_name: &str, input: &serde_json::Value);
65 fn on_tool_result(&self, tool_name: &str, result: &crate::tools::ToolResult);
66 fn on_thinking(&self, _text: &str) {}
67 fn on_turn_complete(&self, _turn: usize) {}
68 fn on_error(&self, error: &str);
69 fn on_usage(&self, _usage: &Usage) {}
70 fn on_compact(&self, _freed_tokens: u64) {}
71 fn on_warning(&self, _msg: &str) {}
72}
73
74pub struct NullSink;
76impl StreamSink for NullSink {
77 fn on_text(&self, _: &str) {}
78 fn on_tool_start(&self, _: &str, _: &serde_json::Value) {}
79 fn on_tool_result(&self, _: &str, _: &crate::tools::ToolResult) {}
80 fn on_error(&self, _: &str) {}
81}
82
83impl QueryEngine {
84 pub fn new(
85 llm: Arc<dyn Provider>,
86 tools: ToolRegistry,
87 permissions: PermissionChecker,
88 state: AppState,
89 config: QueryEngineConfig,
90 ) -> Self {
91 Self {
92 llm,
93 tools,
94 file_cache: Arc::new(tokio::sync::Mutex::new(
95 crate::services::file_cache::FileCache::new(),
96 )),
97 permissions: Arc::new(permissions),
98 state,
99 config,
100 cancel: CancellationToken::new(),
101 hooks: HookRegistry::new(),
102 cache_tracker: crate::services::cache_tracking::CacheTracker::new(),
103 denial_tracker: Arc::new(tokio::sync::Mutex::new(
104 crate::permissions::tracking::DenialTracker::new(100),
105 )),
106 extraction_state: Arc::new(tokio::sync::Mutex::new(
107 crate::memory::extraction::ExtractionState::new(),
108 )),
109 session_allows: Arc::new(tokio::sync::Mutex::new(std::collections::HashSet::new())),
110 permission_prompter: None,
111 }
112 }
113
114 pub fn load_hooks(&mut self, hook_defs: &[crate::hooks::HookDefinition]) {
116 for def in hook_defs {
117 self.hooks.register(def.clone());
118 }
119 if !hook_defs.is_empty() {
120 tracing::info!("Loaded {} hooks from config", hook_defs.len());
121 }
122 }
123
124 pub fn state(&self) -> &AppState {
126 &self.state
127 }
128
129 pub fn state_mut(&mut self) -> &mut AppState {
131 &mut self.state
132 }
133
134 pub fn install_signal_handler(&self) {
138 let cancel = self.cancel.clone();
139 tokio::spawn(async move {
140 loop {
141 if tokio::signal::ctrl_c().await.is_ok() {
142 if cancel.is_cancelled() {
143 std::process::exit(130);
145 }
146 cancel.cancel();
147 }
148 }
149 });
150 }
151
152 pub async fn run_turn(&mut self, user_input: &str) -> crate::error::Result<()> {
154 self.run_turn_with_sink(user_input, &NullSink).await
155 }
156
157 pub async fn run_turn_with_sink(
159 &mut self,
160 user_input: &str,
161 sink: &dyn StreamSink,
162 ) -> crate::error::Result<()> {
163 self.cancel = CancellationToken::new();
165
166 let user_msg = user_message(user_input);
168 self.state.push_message(user_msg);
169
170 let max_turns = self.config.max_turns.unwrap_or(50);
171 let mut compact_tracking = CompactTracking::default();
172 let mut retry_state = crate::llm::retry::RetryState::default();
173 let retry_config = crate::llm::retry::RetryConfig::default();
174 let mut max_output_recovery_count = 0u32;
175
176 for turn in 0..max_turns {
178 self.state.turn_count = turn + 1;
179 self.state.is_query_active = true;
180
181 let budget_config = crate::services::budget::BudgetConfig::default();
183 match crate::services::budget::check_budget(
184 self.state.total_cost_usd,
185 self.state.total_usage.total(),
186 &budget_config,
187 ) {
188 crate::services::budget::BudgetDecision::Stop { message } => {
189 sink.on_warning(&message);
190 self.state.is_query_active = false;
191 return Ok(());
192 }
193 crate::services::budget::BudgetDecision::ContinueWithWarning {
194 message, ..
195 } => {
196 sink.on_warning(&message);
197 }
198 crate::services::budget::BudgetDecision::Continue => {}
199 }
200
201 crate::llm::normalize::ensure_tool_result_pairing(&mut self.state.messages);
203 crate::llm::normalize::strip_empty_blocks(&mut self.state.messages);
204 crate::llm::normalize::remove_empty_messages(&mut self.state.messages);
205 crate::llm::normalize::cap_document_blocks(&mut self.state.messages, 500_000);
206 crate::llm::normalize::merge_consecutive_user_messages(&mut self.state.messages);
207
208 debug!("Agent turn {}/{}", turn + 1, max_turns);
209
210 let model = self.state.config.api.model.clone();
211
212 if compact::should_auto_compact(self.state.history(), &model, &compact_tracking) {
214 let token_count = tokens::estimate_context_tokens(self.state.history());
215 let threshold = compact::auto_compact_threshold(&model);
216 info!("Auto-compact triggered: {token_count} tokens >= {threshold} threshold");
217
218 let freed = compact::microcompact(&mut self.state.messages, 5);
220 if freed > 0 {
221 sink.on_compact(freed);
222 info!("Microcompact freed ~{freed} tokens");
223 }
224
225 let post_mc_tokens = tokens::estimate_context_tokens(self.state.history());
227 if post_mc_tokens >= threshold {
228 info!("Microcompact insufficient, attempting LLM compaction");
230 match compact::compact_with_llm(&mut self.state.messages, &*self.llm, &model)
231 .await
232 {
233 Some(removed) => {
234 info!("LLM compaction removed {removed} messages");
235 compact_tracking.was_compacted = true;
236 compact_tracking.consecutive_failures = 0;
237 }
238 None => {
239 compact_tracking.consecutive_failures += 1;
240 warn!(
241 "LLM compaction failed (attempt {})",
242 compact_tracking.consecutive_failures
243 );
244 let effective = compact::effective_context_window(&model);
246 if let Some(collapse) =
247 crate::services::context_collapse::collapse_to_budget(
248 self.state.history(),
249 effective,
250 )
251 {
252 info!(
253 "Context collapse snipped {} messages, freed ~{} tokens",
254 collapse.snipped_count, collapse.tokens_freed
255 );
256 self.state.messages = collapse.api_messages;
257 sink.on_compact(collapse.tokens_freed);
258 } else {
259 let freed2 = compact::microcompact(&mut self.state.messages, 2);
261 if freed2 > 0 {
262 sink.on_compact(freed2);
263 }
264 }
265 }
266 }
267 }
268 }
269
270 if compact_tracking.was_compacted && self.state.config.features.compaction_reminders {
272 let reminder = user_message(
273 "<system-reminder>Context was automatically compacted. \
274 Earlier messages were summarized. If you need details from \
275 before compaction, ask the user or re-read the relevant files.</system-reminder>",
276 );
277 self.state.push_message(reminder);
278 compact_tracking.was_compacted = false; }
280
281 let warning = compact::token_warning_state(self.state.history(), &model);
283 if warning.is_blocking {
284 sink.on_warning("Context window nearly full. Consider starting a new session.");
285 } else if warning.is_above_warning {
286 sink.on_warning(&format!("Context {}% remaining", warning.percent_left));
287 }
288
289 let system_prompt = build_system_prompt(&self.tools, &self.state);
291 let tool_schemas = self.tools.core_schemas();
293
294 let request = ProviderRequest {
295 messages: self.state.history().to_vec(),
296 system_prompt: system_prompt.clone(),
297 tools: tool_schemas.clone(),
298 model: model.clone(),
299 max_tokens: self.state.config.api.max_output_tokens.unwrap_or(16384),
300 temperature: None,
301 enable_caching: true,
302 tool_choice: Default::default(),
303 metadata: None,
304 };
305
306 let mut rx = match self.llm.stream(&request).await {
307 Ok(rx) => {
308 retry_state.reset();
309 rx
310 }
311 Err(e) => {
312 let retryable = match &e {
313 ProviderError::RateLimited { retry_after_ms } => {
314 crate::llm::retry::RetryableError::RateLimited {
315 retry_after: *retry_after_ms,
316 }
317 }
318 ProviderError::Overloaded => crate::llm::retry::RetryableError::Overloaded,
319 ProviderError::Network(_) => {
320 crate::llm::retry::RetryableError::StreamInterrupted
321 }
322 other => crate::llm::retry::RetryableError::NonRetryable(other.to_string()),
323 };
324
325 match retry_state.next_action(&retryable, &retry_config) {
326 crate::llm::retry::RetryAction::Retry { after } => {
327 warn!("Retrying in {}ms", after.as_millis());
328 tokio::time::sleep(after).await;
329 continue;
330 }
331 crate::llm::retry::RetryAction::FallbackModel => {
332 sink.on_warning("Falling back to smaller model");
333 continue;
335 }
336 crate::llm::retry::RetryAction::Abort(reason) => {
337 if self.config.unattended
340 && self.state.config.features.unattended_retry
341 && matches!(
342 &e,
343 ProviderError::Overloaded | ProviderError::RateLimited { .. }
344 )
345 {
346 warn!("Unattended retry: waiting 30s for capacity");
347 tokio::time::sleep(std::time::Duration::from_secs(30)).await;
348 continue;
349 }
350 if let ProviderError::RequestTooLarge(body) = &e {
352 let gap = compact::parse_prompt_too_long_gap(body);
353 let freed = compact::microcompact(&mut self.state.messages, 1);
354 if freed > 0 {
355 sink.on_compact(freed);
356 info!("Reactive compact freed ~{freed} tokens (gap: {gap:?})");
357 continue;
358 }
359 }
360 sink.on_error(&reason);
361 self.state.is_query_active = false;
362 return Err(crate::error::Error::Other(e.to_string()));
363 }
364 }
365 }
366 };
367
368 let mut content_blocks = Vec::new();
371 let mut usage = Usage::default();
372 let mut stop_reason: Option<StopReason> = None;
373 let mut got_error = false;
374 let mut error_text = String::new();
375 let mut _pending_tool_count = 0usize;
376
377 while let Some(event) = rx.recv().await {
378 match event {
379 StreamEvent::TextDelta(text) => {
380 sink.on_text(&text);
381 }
382 StreamEvent::ContentBlockComplete(block) => {
383 if let ContentBlock::ToolUse {
384 ref name,
385 ref input,
386 ..
387 } = block
388 {
389 sink.on_tool_start(name, input);
390 _pending_tool_count += 1;
391 }
392 if let ContentBlock::Thinking { ref thinking, .. } = block {
393 sink.on_thinking(thinking);
394 }
395 content_blocks.push(block);
396 }
397 StreamEvent::Done {
398 usage: u,
399 stop_reason: sr,
400 } => {
401 usage = u;
402 stop_reason = sr;
403 sink.on_usage(&usage);
404 }
405 StreamEvent::Error(msg) => {
406 got_error = true;
407 error_text = msg.clone();
408 sink.on_error(&msg);
409 }
410 _ => {}
411 }
412 }
413
414 let assistant_msg = Message::Assistant(AssistantMessage {
416 uuid: Uuid::new_v4(),
417 timestamp: chrono::Utc::now().to_rfc3339(),
418 content: content_blocks.clone(),
419 model: Some(model.clone()),
420 usage: Some(usage.clone()),
421 stop_reason: stop_reason.clone(),
422 request_id: None,
423 });
424 self.state.push_message(assistant_msg);
425 self.state.record_usage(&usage, &model);
426
427 if self.state.config.features.token_budget && usage.total() > 0 {
429 let turn_total = usage.input_tokens + usage.output_tokens;
430 if turn_total > 100_000 {
431 sink.on_warning(&format!(
432 "High token usage this turn: {} tokens ({}in + {}out)",
433 turn_total, usage.input_tokens, usage.output_tokens
434 ));
435 }
436 }
437
438 let _cache_event = self.cache_tracker.record(&usage);
440 {
441 let mut span = crate::services::telemetry::api_call_span(
442 &model,
443 turn + 1,
444 &self.state.session_id,
445 );
446 crate::services::telemetry::record_usage(&mut span, &usage);
447 span.finish();
448 tracing::debug!(
449 "API call: {}ms, {}in/{}out tokens",
450 span.duration_ms().unwrap_or(0),
451 usage.input_tokens,
452 usage.output_tokens,
453 );
454 }
455
456 if got_error {
458 if error_text.contains("prompt is too long")
460 || error_text.contains("Prompt is too long")
461 {
462 let freed = compact::microcompact(&mut self.state.messages, 1);
463 if freed > 0 {
464 sink.on_compact(freed);
465 continue;
466 }
467 }
468
469 if content_blocks
471 .iter()
472 .any(|b| matches!(b, ContentBlock::Text { .. }))
473 && error_text.contains("max_tokens")
474 && max_output_recovery_count < MAX_OUTPUT_TOKENS_RECOVERY_LIMIT
475 {
476 max_output_recovery_count += 1;
477 info!(
478 "Max output tokens recovery attempt {}/{}",
479 max_output_recovery_count, MAX_OUTPUT_TOKENS_RECOVERY_LIMIT
480 );
481 let recovery_msg = compact::max_output_recovery_message();
482 self.state.push_message(recovery_msg);
483 continue;
484 }
485 }
486
487 if matches!(stop_reason, Some(StopReason::MaxTokens))
489 && !got_error
490 && content_blocks
491 .iter()
492 .any(|b| matches!(b, ContentBlock::Text { .. }))
493 && max_output_recovery_count < MAX_OUTPUT_TOKENS_RECOVERY_LIMIT
494 {
495 max_output_recovery_count += 1;
496 info!(
497 "Max tokens stop reason — recovery attempt {}/{}",
498 max_output_recovery_count, MAX_OUTPUT_TOKENS_RECOVERY_LIMIT
499 );
500 let recovery_msg = compact::max_output_recovery_message();
501 self.state.push_message(recovery_msg);
502 continue;
503 }
504
505 let tool_calls = extract_tool_calls(&content_blocks);
507
508 if tool_calls.is_empty() {
509 info!("Turn complete (no tool calls)");
511 sink.on_turn_complete(turn + 1);
512 self.state.is_query_active = false;
513
514 if self.state.config.features.extract_memories
517 && crate::memory::ensure_memory_dir().is_some()
518 {
519 let extraction_messages = self.state.messages.clone();
520 let extraction_state = self.extraction_state.clone();
521 let extraction_llm = self.llm.clone();
522 let extraction_model = model.clone();
523 tokio::spawn(async move {
524 crate::memory::extraction::extract_memories_background(
525 extraction_messages,
526 extraction_state,
527 extraction_llm,
528 extraction_model,
529 )
530 .await;
531 });
532 }
533
534 return Ok(());
535 }
536
537 info!("Executing {} tool call(s)", tool_calls.len());
539 let cwd = PathBuf::from(&self.state.cwd);
540 let tool_ctx = ToolContext {
541 cwd,
542 cancel: self.cancel.clone(),
543 permission_checker: self.permissions.clone(),
544 verbose: self.config.verbose,
545 plan_mode: self.state.plan_mode,
546 file_cache: Some(self.file_cache.clone()),
547 denial_tracker: Some(self.denial_tracker.clone()),
548 task_manager: Some(self.state.task_manager.clone()),
549 session_allows: Some(self.session_allows.clone()),
550 permission_prompter: self.permission_prompter.clone(),
551 };
552
553 for call in &tool_calls {
555 self.hooks
556 .run_hooks(&HookEvent::PreToolUse, Some(&call.name), &call.input)
557 .await;
558 }
559
560 let results =
561 execute_tool_calls(&tool_calls, self.tools.all(), &tool_ctx, &self.permissions)
562 .await;
563
564 for result in &results {
566 sink.on_tool_result(&result.tool_name, &result.result);
567
568 self.hooks
570 .run_hooks(
571 &HookEvent::PostToolUse,
572 Some(&result.tool_name),
573 &serde_json::json!({
574 "tool": result.tool_name,
575 "is_error": result.result.is_error,
576 }),
577 )
578 .await;
579
580 let msg = tool_result_message(
581 &result.tool_use_id,
582 &result.result.content,
583 result.result.is_error,
584 );
585 self.state.push_message(msg);
586 }
587
588 }
590
591 warn!("Max turns ({max_turns}) reached");
592 sink.on_warning(&format!("Agent stopped after {max_turns} turns"));
593 self.state.is_query_active = false;
594 Ok(())
595 }
596
597 pub fn cancel(&self) {
599 self.cancel.cancel();
600 }
601}
602
603pub fn build_system_prompt(tools: &ToolRegistry, state: &AppState) -> String {
605 let mut prompt = String::new();
606
607 prompt.push_str(
608 "You are an AI coding agent. You help users with software engineering tasks \
609 by reading, writing, and searching code. Use the tools available to you to \
610 accomplish tasks.\n\n",
611 );
612
613 let shell = std::env::var("SHELL").unwrap_or_else(|_| "bash".to_string());
615 let is_git = std::path::Path::new(&state.cwd).join(".git").exists();
616 prompt.push_str(&format!(
617 "# Environment\n\
618 - Working directory: {}\n\
619 - Platform: {}\n\
620 - Shell: {shell}\n\
621 - Git repository: {}\n\n",
622 state.cwd,
623 std::env::consts::OS,
624 if is_git { "yes" } else { "no" },
625 ));
626
627 let mut memory = crate::memory::MemoryContext::load(Some(std::path::Path::new(&state.cwd)));
629
630 let recent_text: String = state
632 .messages
633 .iter()
634 .rev()
635 .take(5)
636 .filter_map(|m| match m {
637 crate::llm::message::Message::User(u) => Some(
638 u.content
639 .iter()
640 .filter_map(|b| b.as_text())
641 .collect::<Vec<_>>()
642 .join(" "),
643 ),
644 _ => None,
645 })
646 .collect::<Vec<_>>()
647 .join(" ");
648
649 if !recent_text.is_empty() {
650 memory.load_relevant(&recent_text);
651 }
652
653 let memory_section = memory.to_system_prompt_section();
654 if !memory_section.is_empty() {
655 prompt.push_str(&memory_section);
656 }
657
658 prompt.push_str("# Available Tools\n\n");
660 for tool in tools.all() {
661 if tool.is_enabled() {
662 prompt.push_str(&format!("## {}\n{}\n\n", tool.name(), tool.prompt()));
663 }
664 }
665
666 let skills = crate::skills::SkillRegistry::load_all(Some(std::path::Path::new(&state.cwd)));
668 let invocable = skills.user_invocable();
669 if !invocable.is_empty() {
670 prompt.push_str("# Available Skills\n\n");
671 for skill in invocable {
672 let desc = skill.metadata.description.as_deref().unwrap_or("");
673 let when = skill.metadata.when_to_use.as_deref().unwrap_or("");
674 prompt.push_str(&format!("- `/{}`", skill.name));
675 if !desc.is_empty() {
676 prompt.push_str(&format!(": {desc}"));
677 }
678 if !when.is_empty() {
679 prompt.push_str(&format!(" (use when: {when})"));
680 }
681 prompt.push('\n');
682 }
683 prompt.push('\n');
684 }
685
686 prompt.push_str(
688 "# Using tools\n\n\
689 Use dedicated tools instead of shell commands when available:\n\
690 - File search: Glob (not find or ls)\n\
691 - Content search: Grep (not grep or rg)\n\
692 - Read files: FileRead (not cat/head/tail)\n\
693 - Edit files: FileEdit (not sed/awk)\n\
694 - Write files: FileWrite (not echo/cat with redirect)\n\
695 - Reserve Bash for system commands and operations that require shell execution.\n\
696 - Break complex tasks into steps. Use multiple tool calls in parallel when independent.\n\
697 - Use the Agent tool for complex multi-step research or tasks that benefit from isolation.\n\n\
698 # Working with code\n\n\
699 - Read files before editing them. Understand existing code before suggesting changes.\n\
700 - Prefer editing existing files over creating new ones to avoid file bloat.\n\
701 - Only make changes that were requested. Don't add features, refactor, add comments, \
702 or make \"improvements\" beyond the ask.\n\
703 - Don't add error handling for scenarios that can't happen. Don't design for \
704 hypothetical future requirements.\n\
705 - When referencing code, include file_path:line_number.\n\
706 - Be careful not to introduce security vulnerabilities (command injection, XSS, SQL injection, \
707 OWASP top 10). If you notice insecure code you wrote, fix it immediately.\n\
708 - Don't add docstrings, comments, or type annotations to code you didn't change.\n\
709 - Three similar lines of code is better than a premature abstraction.\n\n\
710 # Git safety protocol\n\n\
711 - NEVER update the git config.\n\
712 - NEVER run destructive git commands (push --force, reset --hard, checkout ., restore ., \
713 clean -f, branch -D) unless the user explicitly requests them.\n\
714 - NEVER skip hooks (--no-verify, --no-gpg-sign) unless the user explicitly requests it.\n\
715 - NEVER force push to main/master. Warn the user if they request it.\n\
716 - Always create NEW commits rather than amending, unless the user explicitly requests amend. \
717 After hook failure, the commit did NOT happen — amend would modify the PREVIOUS commit.\n\
718 - When staging files, prefer adding specific files by name rather than git add -A or git add ., \
719 which can accidentally include sensitive files.\n\
720 - NEVER commit changes unless the user explicitly asks.\n\n\
721 # Committing changes\n\n\
722 When the user asks to commit:\n\
723 1. Run git status and git diff to see all changes.\n\
724 2. Run git log --oneline -5 to match the repository's commit message style.\n\
725 3. Draft a concise (1-2 sentence) commit message focusing on \"why\" not \"what\".\n\
726 4. Do not commit files that likely contain secrets (.env, credentials.json).\n\
727 5. Stage specific files, create the commit.\n\
728 6. If pre-commit hook fails, fix the issue and create a NEW commit.\n\
729 7. When creating commits, include a co-author attribution line at the end of the message.\n\n\
730 # Creating pull requests\n\n\
731 When the user asks to create a PR:\n\
732 1. Run git status, git diff, and git log to understand all changes on the branch.\n\
733 2. Analyze ALL commits (not just the latest) that will be in the PR.\n\
734 3. Draft a short title (under 70 chars) and detailed body with summary and test plan.\n\
735 4. Push to remote with -u flag if needed, then create PR using gh pr create.\n\
736 5. Return the PR URL when done.\n\n\
737 # Executing actions safely\n\n\
738 Consider the reversibility and blast radius of every action:\n\
739 - Freely take local, reversible actions (editing files, running tests).\n\
740 - For hard-to-reverse or shared-state actions, confirm with the user first:\n\
741 - Destructive: deleting files/branches, dropping tables, rm -rf, overwriting uncommitted changes.\n\
742 - Hard to reverse: force-pushing, git reset --hard, amending published commits.\n\
743 - Visible to others: pushing code, creating/commenting on PRs/issues, sending messages.\n\
744 - When you encounter an obstacle, do not use destructive actions as a shortcut. \
745 Identify root causes and fix underlying issues.\n\
746 - If you discover unexpected state (unfamiliar files, branches, config), investigate \
747 before deleting or overwriting — it may be the user's in-progress work.\n\n\
748 # Response style\n\n\
749 - Be concise. Lead with the answer or action, not the reasoning.\n\
750 - Skip filler, preamble, and unnecessary transitions.\n\
751 - Don't restate what the user said.\n\
752 - If you can say it in one sentence, don't use three.\n\
753 - Focus output on: decisions that need input, status updates, and errors that change the plan.\n\
754 - When referencing GitHub issues or PRs, use owner/repo#123 format.\n\
755 - Only use emojis if the user explicitly requests it.\n\n\
756 # Memory\n\n\
757 You can save information across sessions by writing memory files.\n\
758 - Save to: ~/.config/agent-code/memory/ (one .md file per topic)\n\
759 - Each file needs YAML frontmatter: name, description, type (user/feedback/project/reference)\n\
760 - After writing a file, update MEMORY.md with a one-line pointer\n\
761 - Memory types: user (role, preferences), feedback (corrections, confirmations), \
762 project (decisions, deadlines), reference (external resources)\n\
763 - Do NOT store: code patterns, git history, debugging solutions, anything derivable from code\n\
764 - Memory is a hint — always verify against current state before acting on it\n",
765 );
766
767 prompt.push_str(
769 "# Tool usage patterns\n\n\
770 Common patterns for effective tool use:\n\n\
771 **Read before edit**: Always read a file before editing it. This ensures you \
772 understand the current state and can make targeted changes.\n\
773 ```\n\
774 1. FileRead file_path → understand structure\n\
775 2. FileEdit old_string, new_string → targeted change\n\
776 ```\n\n\
777 **Search then act**: Use Glob to find files, Grep to find content, then read/edit.\n\
778 ```\n\
779 1. Glob **/*.rs → find Rust files\n\
780 2. Grep pattern path → find specific code\n\
781 3. FileRead → read the match\n\
782 4. FileEdit → make the change\n\
783 ```\n\n\
784 **Parallel tool calls**: When you need to read multiple independent files or run \
785 independent searches, make all the tool calls in one response. Don't serialize \
786 independent operations.\n\n\
787 **Test after change**: After editing code, run tests to verify the change works.\n\
788 ```\n\
789 1. FileEdit → make change\n\
790 2. Bash cargo test / pytest / npm test → verify\n\
791 3. If tests fail, read the error, fix, re-test\n\
792 ```\n\n\
793 # Error recovery\n\n\
794 When something goes wrong:\n\
795 - **Tool not found**: Use ToolSearch to find the right tool name.\n\
796 - **Permission denied**: Explain why the action is needed, ask the user to approve.\n\
797 - **File not found**: Use Glob to find the correct path. Check for typos.\n\
798 - **Edit failed (not unique)**: Provide more surrounding context in old_string, \
799 or use replace_all=true if renaming.\n\
800 - **Command failed**: Read the full error message. Don't retry the same command. \
801 Diagnose the root cause first.\n\
802 - **Context too large**: The system will auto-compact. If you need specific \
803 information from before compaction, re-read the relevant files.\n\
804 - **Rate limited**: The system will auto-retry with backoff. Just wait.\n\n\
805 # Common workflows\n\n\
806 **Bug fix**: Read the failing test → read the source code it tests → \
807 identify the bug → fix it → run the test → confirm it passes.\n\n\
808 **New feature**: Read existing patterns in the codebase → create or edit files → \
809 add tests → run tests → update docs if needed.\n\n\
810 **Code review**: Read the diff → identify issues (bugs, security, style) → \
811 report findings with file:line references.\n\n\
812 **Refactor**: Search for all usages of the symbol → plan the changes → \
813 edit each file → run tests to verify nothing broke.\n\n",
814 );
815
816 if !state.config.mcp_servers.is_empty() {
818 prompt.push_str("# MCP Servers\n\n");
819 prompt.push_str(
820 "Connected MCP servers provide additional tools. MCP tools are prefixed \
821 with `mcp__{server}__{tool}`. Use them like any other tool.\n\n",
822 );
823 for (name, entry) in &state.config.mcp_servers {
824 let transport = if entry.command.is_some() {
825 "stdio"
826 } else if entry.url.is_some() {
827 "sse"
828 } else {
829 "unknown"
830 };
831 prompt.push_str(&format!("- **{name}** ({transport})\n"));
832 }
833 prompt.push('\n');
834 }
835
836 let deferred = tools.deferred_names();
838 if !deferred.is_empty() {
839 prompt.push_str("# Deferred Tools\n\n");
840 prompt.push_str(
841 "These tools are available but not loaded by default. \
842 Use ToolSearch to load them when needed:\n",
843 );
844 for name in &deferred {
845 prompt.push_str(&format!("- {name}\n"));
846 }
847 prompt.push('\n');
848 }
849
850 prompt.push_str(
852 "# Task management\n\n\
853 - Use TaskCreate to break complex work into trackable steps.\n\
854 - Mark tasks as in_progress when starting, completed when done.\n\
855 - Use the Agent tool to spawn subagents for parallel independent work.\n\
856 - Use EnterPlanMode/ExitPlanMode for read-only exploration before making changes.\n\
857 - Use EnterWorktree/ExitWorktree for isolated changes in git worktrees.\n\n\
858 # Output formatting\n\n\
859 - All text output is displayed to the user. Use GitHub-flavored markdown.\n\
860 - Use fenced code blocks with language hints for code: ```rust, ```python, etc.\n\
861 - Use inline `code` for file names, function names, and short code references.\n\
862 - Use tables for structured comparisons.\n\
863 - Use bullet lists for multiple items.\n\
864 - Keep paragraphs short (2-3 sentences).\n\
865 - Never output raw HTML or complex formatting — stick to standard markdown.\n",
866 );
867
868 prompt
869}