Skip to main content

opi_coding_agent/
prompt.rs

1//! System prompt construction (S8.4).
2//!
3//! Assembles the layered system prompt sent to the provider:
4//! 1. Base coding-agent instructions
5//! 2. Tool descriptions from ToolDef
6//! 3. User system prompt file
7//! 4. Project context files (AGENTS.md / CLAUDE.md)
8
9use opi_ai::message::ToolDef;
10
11const BASE_INSTRUCTIONS: &str = "\
12You are opi, an expert coding agent. You help users with software engineering \
13tasks including reading, writing, and editing code, running commands, and \
14searching codebases. Be concise and precise. Explain your reasoning when \
15making changes.";
16
17/// Builder for assembling the system prompt from layered components.
18pub struct SystemPromptBuilder {
19    tools: Vec<ToolDef>,
20    user_system: Option<String>,
21    context_files: Option<String>,
22}
23
24impl SystemPromptBuilder {
25    pub fn new() -> Self {
26        Self {
27            tools: Vec::new(),
28            user_system: None,
29            context_files: None,
30        }
31    }
32
33    /// Add tool definitions. Their names and descriptions are included in the prompt.
34    pub fn tools(mut self, tools: Vec<ToolDef>) -> Self {
35        self.tools = tools;
36        self
37    }
38
39    /// Add user-provided system prompt content (from --system flag or config).
40    pub fn user_system(mut self, content: impl Into<String>) -> Self {
41        let s = content.into();
42        self.user_system = if s.is_empty() { None } else { Some(s) };
43        self
44    }
45
46    /// Add project context file content (from AGENTS.md / CLAUDE.md discovery).
47    pub fn context_files(mut self, content: impl Into<String>) -> Self {
48        let s = content.into();
49        self.context_files = if s.is_empty() { None } else { Some(s) };
50        self
51    }
52
53    /// Return the collected tool definitions for `Request.tools`.
54    pub fn tool_definitions(&self) -> &[ToolDef] {
55        &self.tools
56    }
57
58    /// Assemble and return the full system prompt string.
59    pub fn build(self) -> String {
60        let mut parts = Vec::new();
61
62        // Layer 1: base instructions
63        parts.push(BASE_INSTRUCTIONS.to_owned());
64
65        // Layer 2: tool descriptions
66        if !self.tools.is_empty() {
67            let mut tool_section = String::from("Available tools:\n");
68            for tool in &self.tools {
69                tool_section.push_str(&format!("- {}: {}\n", tool.name, tool.description));
70            }
71            parts.push(tool_section);
72        }
73
74        // Layer 3: user system prompt
75        if let Some(user) = self.user_system {
76            parts.push(format!("User instructions:\n{}", user));
77        }
78
79        // Layer 4: project context files (AGENTS.md / CLAUDE.md)
80        if let Some(context) = self.context_files {
81            parts.push(format!("Project context:\n{}", context));
82        }
83
84        parts.join("\n\n")
85    }
86}
87
88impl Default for SystemPromptBuilder {
89    fn default() -> Self {
90        Self::new()
91    }
92}