Applying additional cache fixes

This commit is contained in:
Ryan Ward
2026-06-01 13:02:22 -05:00
parent 8c0a2d67cb
commit 7f039d2c08
12 changed files with 1326 additions and 187 deletions
+7 -3
View File
@@ -1,9 +1,10 @@
use std::collections::HashMap;
use aws_sdk_bedrockruntime::types::{
CachePointBlock, CachePointType, ContentBlock, ConversationRole, InferenceConfiguration,
Message as BedrockMessage, SystemContentBlock, Tool, ToolConfiguration, ToolInputSchema,
ToolResultBlock, ToolResultContentBlock, ToolResultStatus, ToolSpecification, ToolUseBlock,
CachePointBlock, CachePointType, CacheTtl, ContentBlock, ConversationRole,
InferenceConfiguration, Message as BedrockMessage, SystemContentBlock, Tool,
ToolConfiguration, ToolInputSchema, ToolResultBlock, ToolResultContentBlock, ToolResultStatus,
ToolSpecification, ToolUseBlock,
};
use aws_smithy_types::Document;
use serde_json::Value as JsonValue;
@@ -220,6 +221,7 @@ fn convert_messages(messages: Vec<ConversationMessage>) -> Vec<BedrockMessage> {
content.push(ContentBlock::CachePoint(
CachePointBlock::builder()
.r#type(CachePointType::Default)
.ttl(CacheTtl::OneHour)
.build()
.expect("valid cache point"),
));
@@ -273,6 +275,7 @@ fn convert_system_prompt(system_prompt: Option<String>) -> Vec<SystemContentBloc
SystemContentBlock::CachePoint(
CachePointBlock::builder()
.r#type(CachePointType::Default)
.ttl(CacheTtl::OneHour)
.build()
.expect("valid cache point"),
),
@@ -326,6 +329,7 @@ fn build_tool_config(tools: Vec<ToolDefinition>) -> Option<ToolConfiguration> {
tool_specs.push(Tool::CachePoint(
CachePointBlock::builder()
.r#type(CachePointType::Default)
.ttl(CacheTtl::OneHour)
.build()
.expect("valid cache point"),
));
+1
View File
@@ -17,6 +17,7 @@ pub(crate) mod attachment_utils;
pub mod aws_credentials;
#[cfg(not(target_family = "wasm"))]
pub mod bedrock;
pub mod prompt_builder;
pub(crate) mod block_context;
pub(crate) mod blocklist;
pub mod control_code_parser;
+48
View File
@@ -0,0 +1,48 @@
//! Environment context that gets injected into system prompts.
/// Environment context about the user's machine and project.
#[derive(Debug, Clone, Default)]
pub struct PromptContext {
pub working_dir: String,
pub home_dir: String,
pub os: String,
pub shell: String,
pub git_branch: String,
pub current_time: String,
}
impl PromptContext {
pub fn new() -> Self {
Self::default()
}
pub fn with_working_dir(mut self, dir: impl Into<String>) -> Self {
self.working_dir = dir.into();
self
}
pub fn with_home_dir(mut self, dir: impl Into<String>) -> Self {
self.home_dir = dir.into();
self
}
pub fn with_os(mut self, os: impl Into<String>) -> Self {
self.os = os.into();
self
}
pub fn with_shell(mut self, shell: impl Into<String>) -> Self {
self.shell = shell.into();
self
}
pub fn with_git_branch(mut self, branch: impl Into<String>) -> Self {
self.git_branch = branch.into();
self
}
pub fn with_current_time(mut self, time: impl Into<String>) -> Self {
self.current_time = time.into();
self
}
}
+192
View File
@@ -0,0 +1,192 @@
//! Prompt Builder Module
//!
//! Centralized system for constructing AI model prompts based on the current
//! operational mode (code, plan, review, etc.) and provider (Anthropic, etc.).
//!
//! # Architecture
//!
//! The prompt builder separates concerns into:
//! - **Mode**: What the agent is doing (coding, planning, reviewing, etc.)
//! - **Provider**: Which LLM is being used (Anthropic Claude, future: Gemini, GPT, etc.)
//! - **Context**: Environment info (working directory, OS, shell, git state, etc.)
//! - **Rules**: Project-specific rules from AGENTS.md / GALAXY.md files
//! - **Tools**: Which tools are available (varies by mode)
//!
//! The builder composes these layers to produce the final system prompt and tool
//! definitions for each request.
mod context;
mod mode;
mod prompts;
mod tools;
pub use context::PromptContext;
pub use mode::Mode;
pub use prompts::provider::Provider;
pub use tools::ToolSet;
use crate::ai::bedrock::convert::ToolDefinition;
/// The fully-resolved prompt configuration ready to send to a model.
#[derive(Debug, Clone)]
pub struct ResolvedPrompt {
/// The system prompt text.
pub system_prompt: String,
/// The tool definitions available for this request.
pub tools: Vec<ToolDefinition>,
}
/// The main prompt builder. Composes mode, provider, context, and rules
/// into a final system prompt and tool set.
pub struct PromptBuilder {
mode: Mode,
provider: Provider,
context: PromptContext,
project_rules: Vec<ProjectRule>,
mcp_tools: Vec<ToolDefinition>,
}
/// A project rule loaded from AGENTS.md or GALAXY.md files.
#[derive(Debug, Clone)]
pub struct ProjectRule {
pub root_path: String,
pub content: String,
}
impl PromptBuilder {
/// Create a new prompt builder with the given mode and provider.
pub fn new(mode: Mode, provider: Provider) -> Self {
Self {
mode,
provider,
context: PromptContext::default(),
project_rules: Vec::new(),
mcp_tools: Vec::new(),
}
}
/// Set the environment context.
pub fn with_context(mut self, context: PromptContext) -> Self {
self.context = context;
self
}
/// Add project rules (from AGENTS.md, GALAXY.md, etc.).
pub fn with_project_rules(mut self, rules: Vec<ProjectRule>) -> Self {
self.project_rules = rules;
self
}
/// Add MCP tools from connected servers.
pub fn with_mcp_tools(mut self, tools: Vec<ToolDefinition>) -> Self {
self.mcp_tools = tools;
self
}
/// Build the final resolved prompt.
pub fn build(&self) -> ResolvedPrompt {
let system_prompt = self.build_system_prompt();
let tools = self.build_tools();
ResolvedPrompt {
system_prompt,
tools,
}
}
fn build_system_prompt(&self) -> String {
let mut parts: Vec<String> = Vec::with_capacity(8);
// 1. Identity + mode-specific base prompt
parts.push(self.identity_prompt());
// 2. Environment context
if let Some(env) = self.environment_section() {
parts.push(env);
}
// 3. Project rules
if let Some(rules) = self.rules_section() {
parts.push(rules);
}
// 4. Mode-specific instructions
parts.push(self.mode_instructions());
// 5. Tool usage guidelines
parts.push(self.tool_usage_section());
parts.join("\n\n")
}
fn identity_prompt(&self) -> String {
let base = prompts::base_identity(&self.provider);
let mode_identity = prompts::mode_identity(&self.mode);
format!("{base}\n\n{mode_identity}")
}
fn environment_section(&self) -> Option<String> {
let ctx = &self.context;
if ctx.working_dir.is_empty() && ctx.os.is_empty() {
return None;
}
let mut lines = vec!["## Environment".to_string()];
if !ctx.working_dir.is_empty() {
lines.push(format!("- Working directory: {}", ctx.working_dir));
}
if !ctx.home_dir.is_empty() {
lines.push(format!("- Home directory: {}", ctx.home_dir));
}
if !ctx.os.is_empty() {
lines.push(format!("- OS: {}", ctx.os));
}
if !ctx.shell.is_empty() {
lines.push(format!("- Shell: {}", ctx.shell));
}
if !ctx.git_branch.is_empty() {
lines.push(format!("- Git branch: {}", ctx.git_branch));
}
if !ctx.current_time.is_empty() {
lines.push(format!("- Current time (UTC): {}", ctx.current_time));
}
Some(lines.join("\n"))
}
fn rules_section(&self) -> Option<String> {
if self.project_rules.is_empty() {
return None;
}
let mut section = String::from("## Project Rules\n");
for rule in &self.project_rules {
if !rule.root_path.is_empty() {
section.push_str(&format!("### Rules from {}\n", rule.root_path));
}
section.push_str(&rule.content);
section.push('\n');
}
Some(section)
}
fn mode_instructions(&self) -> String {
prompts::mode_instructions(&self.mode, &self.provider)
}
fn tool_usage_section(&self) -> String {
prompts::tool_usage_guidelines(&self.mode, &self.provider)
}
fn build_tools(&self) -> Vec<ToolDefinition> {
let mut tool_set = tools::tools_for_mode(&self.mode);
// Append MCP tools
tool_set.extend(self.mcp_tools.clone());
tool_set
}
}
#[cfg(test)]
mod tests;
+66
View File
@@ -0,0 +1,66 @@
//! Operational modes that determine prompt behavior and tool availability.
/// The operational mode the agent is currently in.
///
/// Each mode provides different system prompt instructions and makes
/// different tools available to the model.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Mode {
/// General coding mode - the default. Full tool access, focused on
/// implementing changes, debugging, and exploring codebases.
Code,
/// Planning mode - read-only exploration and design. The model should
/// NOT make edits, only analyze, research, and produce a plan.
Plan,
/// Code review mode - focused on reviewing diffs/changes and providing
/// feedback on correctness, style, and potential issues.
Review,
/// Conversation summary mode - condensing conversation history while
/// preserving key decisions and context.
Summarize,
/// Title generation mode - producing a short title for a conversation.
Title,
}
impl Mode {
/// Whether this mode allows file edits.
pub fn allows_edits(&self) -> bool {
matches!(self, Mode::Code)
}
/// Whether this mode allows shell command execution.
pub fn allows_shell(&self) -> bool {
matches!(self, Mode::Code)
}
/// Whether this mode allows read-only exploration tools.
pub fn allows_read(&self) -> bool {
matches!(self, Mode::Code | Mode::Plan | Mode::Review)
}
/// Whether this mode should use sub-agents.
pub fn allows_subagents(&self) -> bool {
matches!(self, Mode::Code | Mode::Plan)
}
/// Human-readable label for logging/diagnostics.
pub fn label(&self) -> &'static str {
match self {
Mode::Code => "code",
Mode::Plan => "plan",
Mode::Review => "review",
Mode::Summarize => "summarize",
Mode::Title => "title",
}
}
}
impl std::fmt::Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.label())
}
}
@@ -0,0 +1,160 @@
//! Anthropic Claude-specific prompt templates.
//!
//! These are tuned for Claude's instruction-following style and capabilities.
use crate::ai::prompt_builder::mode::Mode;
pub const BASE_IDENTITY: &str = r#"You are Galaxy, an AI coding assistant embedded in a terminal application. You help users with software engineering tasks including writing code, debugging, explaining concepts, and navigating codebases.
You are concise, direct, and to the point. Your output is displayed in a terminal use GitHub-flavored markdown for formatting. Output text to communicate with the user; only use tools to complete tasks.
If you cannot or will not help with something, say so briefly (1-2 sentences) and offer alternatives if possible. Do not add unnecessary preamble or postamble unless the user asks for detail."#;
pub fn mode_instructions(mode: &Mode) -> String {
match mode {
Mode::Code => CODE_INSTRUCTIONS.to_string(),
Mode::Plan => PLAN_INSTRUCTIONS.to_string(),
Mode::Review => REVIEW_INSTRUCTIONS.to_string(),
Mode::Summarize => SUMMARIZE_INSTRUCTIONS.to_string(),
Mode::Title => TITLE_INSTRUCTIONS.to_string(),
}
}
pub fn tool_usage_guidelines(mode: &Mode) -> String {
match mode {
Mode::Code => CODE_TOOL_GUIDELINES.to_string(),
Mode::Plan => PLAN_TOOL_GUIDELINES.to_string(),
Mode::Review => REVIEW_TOOL_GUIDELINES.to_string(),
Mode::Summarize => String::new(),
Mode::Title => String::new(),
}
}
const CODE_INSTRUCTIONS: &str = r#"## Instructions
### Doing Tasks
The user will primarily request software engineering tasks: solving bugs, adding features, refactoring, explaining code, and more. For these tasks:
1. Use search tools to understand the codebase and the user's query. Search extensively both in parallel and sequentially.
2. Implement the solution using all tools available to you.
3. Verify the solution if possible with tests. NEVER assume a specific test framework check the project first.
4. After making changes, run lint/typecheck/build commands if you know them, to ensure correctness.
NEVER commit changes unless the user explicitly asks you to.
### Following Conventions
When making changes, first understand the file's code conventions. Mimic code style, use existing libraries, and follow existing patterns.
- NEVER assume a library is available. Check the project's dependency files (package.json, Cargo.toml, requirements.txt, etc.) first.
- When creating new components, look at existing ones to understand conventions.
- When editing code, look at surrounding context (imports, patterns) to ensure idiomatic changes.
- Always follow security best practices. Never introduce code that exposes or logs secrets.
### Proactiveness
Strike a balance between doing the right thing when asked (including follow-up actions) and not surprising the user with unrequested actions. If the user asks HOW to do something, explain first don't immediately take action.
Do not add code explanation summaries after making changes unless asked."#;
const PLAN_INSTRUCTIONS: &str = r#"## Instructions
CRITICAL: You are in READ-ONLY planning mode. You MUST NOT:
- Edit, create, or delete any files
- Run any mutating shell commands
- Make commits or change configuration
You MAY:
- Read files
- Search the codebase (grep, glob)
- Run read-only shell commands (ls, cat, git log, git status)
- Ask the user clarifying questions
### Planning Workflow
1. **Understand**: Explore the codebase to understand the current state and the user's goal.
2. **Research**: Look at relevant files, patterns, and dependencies.
3. **Design**: Produce a clear, concise implementation plan with:
- The approach and rationale
- Key files that need modification
- Potential risks or tradeoffs
- Verification strategy (how to test the changes)
4. **Clarify**: Ask the user questions about ambiguities or tradeoffs before finalizing.
Present your plan in a structured, scannable format. Focus on the recommended approach don't enumerate every alternative."#;
const REVIEW_INSTRUCTIONS: &str = r#"## Instructions
You are reviewing code changes. Your primary focus is identifying:
1. **Bugs and correctness issues** logic errors, off-by-ones, race conditions, null handling
2. **Security risks** exposed secrets, injection vulnerabilities, unsafe operations
3. **Behavioral regressions** changes that break existing functionality
4. **Missing tests** untested edge cases or new code paths
Present findings ordered by severity with file/line references. Keep summaries brief findings are the primary focus.
Also note:
- Style issues (only if they meaningfully impact readability)
- Performance concerns (only if significant)
- Suggestions for better approaches
If no issues are found, state that explicitly and mention any residual risks or testing gaps."#;
const SUMMARIZE_INSTRUCTIONS: &str = r#"Summarize the conversation so far. Preserve:
- Key decisions made
- Code changes (files modified, what was changed and why)
- Important context (file paths, function names, architectural choices)
- Outstanding tasks or next steps
- Any errors encountered and how they were resolved
Be concise but retain all information needed to continue the work without re-reading the full history."#;
const TITLE_INSTRUCTIONS: &str = r#"Rules:
- Output ONLY a title, nothing else
- Under 50 characters
- Use the same language as the user's message
- Focus on what the user wants to accomplish
- Keep technical terms, filenames, and numbers exact
- Never use tools"#;
const CODE_TOOL_GUIDELINES: &str = r#"## Tool Usage
You have been given every tool you need to complete your tasks. Use them to achieve results with as few calls and as little back-and-forth as possible.
**How to choose tools:**
- For reading, writing, searching, and navigating files on the local filesystem, use your filesystem tools (`read_files`, `file_glob`, `grep`, `apply_file_diffs`).
- For running commands, installing packages, building, testing, and any shell operation, use `run_shell_command`.
- For tasks that require interacting with external services, web UIs, or capabilities not covered by your filesystem and shell tools, use your MCP tools.
- For complex multi-step tasks where a single script would replace many tool calls, write code (Python, Node, bash) via `run_shell_command` to reduce round-trips. But never use scripts for simple operations that a single command handles.
**Critical rules:**
- Use ONLY the tools in your tool configuration. Never invent or guess tool names.
- ALWAYS pass `--no-pager` (or equivalent) flags to CLI tools like git, less, man, etc. Tools that lock stdin will freeze the session.
- Output text directly in your response instead of using `echo` echo requires user approval and adds unnecessary friction.
- Use absolute paths based on the working directory shown above.
- Be logical in your tool choices. Read files before making claims about code. List files before assuming project structure.
- Be concise and direct.
When making function calls using tools that accept array or object parameters ensure those are structured using JSON. For example:
```json
{"parameter": [{"color": "orange", "options": {"option_key_1": true}}]}
```
Answer the user's request using the relevant tool(s), if they are available. Check that all the required parameters for each tool call are provided or can reasonably be inferred from context. IF there are no relevant tools or there are missing values for required parameters, ask the user to supply these values; otherwise proceed with the tool calls. If the user provides a specific value for a parameter (for example provided in quotes), make sure to use that value EXACTLY. DO NOT make up values for or ask about optional parameters.
If you intend to call multiple tools and there are no dependencies between the calls, make all of the independent calls in the same block, otherwise you MUST wait for previous calls to finish first to determine the dependent values (do NOT use placeholders or guess missing parameters)."#;
const PLAN_TOOL_GUIDELINES: &str = r#"## Tool Usage
You have access to read-only tools for exploring the codebase:
- `read_files` Read file contents
- `grep` Search for patterns in files
- `file_glob` Find files by glob pattern
- `search_codebase` Semantic code search
- `run_shell_command` ONLY for read-only commands (ls, git log, git status, etc.)
- `ask_user_question` Ask the user for clarification
You MUST NOT use `apply_file_diffs`, `create_documents`, `edit_documents`, or any command that modifies files or state. Any attempt to edit is a critical violation of planning mode."#;
const REVIEW_TOOL_GUIDELINES: &str = r#"## Tool Usage
You have access to tools for examining the code under review:
- `read_files` Read file contents to understand context
- `grep` Search for patterns to find related code
- `file_glob` Find related files
- `search_codebase` Semantic search for related implementations
- `run_shell_command` For read-only commands (git diff, git log, etc.)
Use these tools to gather context needed for a thorough review. You should read the files being changed and their surrounding context before providing feedback."#;
+40
View File
@@ -0,0 +1,40 @@
//! Prompt text templates organized by provider and mode.
pub mod provider;
mod anthropic;
use crate::ai::prompt_builder::mode::Mode;
use provider::Provider;
/// The base identity statement shared across all modes.
pub fn base_identity(provider: &Provider) -> &'static str {
match provider {
Provider::Anthropic => anthropic::BASE_IDENTITY,
}
}
/// Mode-specific identity/role description.
pub fn mode_identity(mode: &Mode) -> &'static str {
match mode {
Mode::Code => "You are in coding mode. Your primary role is to help the user implement changes, debug issues, explore codebases, and complete software engineering tasks.",
Mode::Plan => "You are in planning mode. Your role is to analyze, research, and design an implementation approach. You must NOT make any edits or run any mutating commands — only read, search, and think.",
Mode::Review => "You are in code review mode. Your role is to review code changes and provide detailed feedback on correctness, style, potential bugs, and improvements.",
Mode::Summarize => "You are summarizing a conversation. Preserve key decisions, code changes, file paths, and important context. Be concise but retain all information needed to continue the work.",
Mode::Title => "Generate a brief conversation title. Output ONLY the title — no explanation, no quotes. Keep it under 50 characters.",
}
}
/// Mode-specific behavioral instructions.
pub fn mode_instructions(mode: &Mode, provider: &Provider) -> String {
match provider {
Provider::Anthropic => anthropic::mode_instructions(mode),
}
}
/// Tool usage guidelines tailored to mode and provider.
pub fn tool_usage_guidelines(mode: &Mode, provider: &Provider) -> String {
match provider {
Provider::Anthropic => anthropic::tool_usage_guidelines(mode),
}
}
@@ -0,0 +1,28 @@
//! LLM provider definitions.
//!
//! Each provider may have different prompting strategies, tone preferences,
//! and tool schema requirements. New providers are added here as variants.
/// The LLM provider being targeted.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Provider {
/// Anthropic Claude models (Sonnet, Opus, Haiku, etc.)
Anthropic,
// Future:
// Gemini,
// OpenAI,
}
impl Provider {
pub fn label(&self) -> &'static str {
match self {
Provider::Anthropic => "anthropic",
}
}
}
impl std::fmt::Display for Provider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.label())
}
}
+131
View File
@@ -0,0 +1,131 @@
#[cfg(test)]
mod prompt_builder_tests {
use super::*;
#[test]
fn test_code_mode_builds_successfully() {
let prompt = PromptBuilder::new(Mode::Code, Provider::Anthropic)
.with_context(
PromptContext::new()
.with_working_dir("/home/user/project")
.with_os("Linux")
.with_shell("zsh 5.9"),
)
.build();
assert!(prompt.system_prompt.contains("Galaxy"));
assert!(prompt.system_prompt.contains("coding mode"));
assert!(prompt.system_prompt.contains("/home/user/project"));
assert!(!prompt.tools.is_empty());
// Code mode should have apply_file_diffs
assert!(prompt.tools.iter().any(|t| t.name == "apply_file_diffs"));
}
#[test]
fn test_plan_mode_has_no_edit_tools() {
let prompt = PromptBuilder::new(Mode::Plan, Provider::Anthropic).build();
assert!(prompt.system_prompt.contains("planning mode"));
assert!(prompt.system_prompt.contains("READ-ONLY"));
// Plan mode should NOT have apply_file_diffs
assert!(!prompt.tools.iter().any(|t| t.name == "apply_file_diffs"));
// But should have read_files
assert!(prompt.tools.iter().any(|t| t.name == "read_files"));
}
#[test]
fn test_review_mode_is_read_only() {
let prompt = PromptBuilder::new(Mode::Review, Provider::Anthropic).build();
assert!(prompt.system_prompt.contains("code review mode"));
assert!(!prompt.tools.iter().any(|t| t.name == "apply_file_diffs"));
assert!(!prompt.tools.iter().any(|t| t.name == "create_documents"));
assert!(prompt.tools.iter().any(|t| t.name == "read_files"));
assert!(prompt.tools.iter().any(|t| t.name == "grep"));
}
#[test]
fn test_summarize_mode_has_no_tools() {
let prompt = PromptBuilder::new(Mode::Summarize, Provider::Anthropic).build();
assert!(prompt.system_prompt.contains("summariz"));
assert!(prompt.tools.is_empty());
}
#[test]
fn test_title_mode_has_no_tools() {
let prompt = PromptBuilder::new(Mode::Title, Provider::Anthropic).build();
assert!(prompt.system_prompt.contains("title"));
assert!(prompt.tools.is_empty());
}
#[test]
fn test_project_rules_included() {
let prompt = PromptBuilder::new(Mode::Code, Provider::Anthropic)
.with_project_rules(vec![ProjectRule {
root_path: "/home/user/project".to_string(),
content: "Always use snake_case for function names.".to_string(),
}])
.build();
assert!(prompt.system_prompt.contains("Project Rules"));
assert!(prompt.system_prompt.contains("snake_case"));
}
#[test]
fn test_mcp_tools_appended() {
use crate::ai::bedrock::convert::ToolDefinition;
let mcp_tool = ToolDefinition {
name: "mcp__github__create_pr".to_string(),
description: "Create a pull request".to_string(),
input_schema: serde_json::json!({"type": "object", "properties": {}}),
};
let prompt = PromptBuilder::new(Mode::Code, Provider::Anthropic)
.with_mcp_tools(vec![mcp_tool.clone()])
.build();
assert!(prompt.tools.iter().any(|t| t.name == "mcp__github__create_pr"));
}
#[test]
fn test_mode_allows_edits() {
assert!(Mode::Code.allows_edits());
assert!(!Mode::Plan.allows_edits());
assert!(!Mode::Review.allows_edits());
assert!(!Mode::Summarize.allows_edits());
assert!(!Mode::Title.allows_edits());
}
#[test]
fn test_mode_allows_shell() {
assert!(Mode::Code.allows_shell());
assert!(!Mode::Plan.allows_shell());
assert!(!Mode::Review.allows_shell());
}
#[test]
fn test_empty_context_omits_environment_section() {
let prompt = PromptBuilder::new(Mode::Code, Provider::Anthropic).build();
// With default (empty) context, no Environment section
assert!(!prompt.system_prompt.contains("## Environment"));
}
#[test]
fn test_context_populates_environment_section() {
let prompt = PromptBuilder::new(Mode::Code, Provider::Anthropic)
.with_context(
PromptContext::new()
.with_working_dir("/projects/myapp")
.with_git_branch("feature/new-thing"),
)
.build();
assert!(prompt.system_prompt.contains("## Environment"));
assert!(prompt.system_prompt.contains("/projects/myapp"));
assert!(prompt.system_prompt.contains("feature/new-thing"));
}
}
+331
View File
@@ -0,0 +1,331 @@
//! Tool definitions filtered by mode.
//!
//! Each mode has a different set of tools available. Code mode gets everything,
//! Plan mode gets read-only tools, Review mode gets read + search, etc.
use crate::ai::bedrock::convert::ToolDefinition;
use crate::ai::prompt_builder::mode::Mode;
/// Returns the tool definitions available for the given mode.
pub fn tools_for_mode(mode: &Mode) -> Vec<ToolDefinition> {
match mode {
Mode::Code => code_tools(),
Mode::Plan => plan_tools(),
Mode::Review => review_tools(),
Mode::Summarize => vec![],
Mode::Title => vec![],
}
}
/// Full tool set for coding mode.
fn code_tools() -> Vec<ToolDefinition> {
vec![
run_shell_command(),
read_files(),
apply_file_diffs(),
grep(),
file_glob(),
search_codebase(),
write_to_long_running_shell_command(),
read_shell_command_output(),
read_mcp_resource(),
read_documents(),
create_documents(),
edit_documents(),
start_agent(),
send_message_to_agent(),
ask_user_question(),
read_skill(),
fetch_conversation(),
]
}
/// Read-only tools for planning mode.
fn plan_tools() -> Vec<ToolDefinition> {
vec![
read_files(),
grep(),
file_glob(),
search_codebase(),
run_shell_command_readonly(),
ask_user_question(),
start_agent(),
send_message_to_agent(),
read_skill(),
fetch_conversation(),
]
}
/// Tools for code review mode.
fn review_tools() -> Vec<ToolDefinition> {
vec![
read_files(),
grep(),
file_glob(),
search_codebase(),
run_shell_command_readonly(),
]
}
// ─── Tool Definitions ────────────────────────────────────────────────────────
fn run_shell_command() -> ToolDefinition {
ToolDefinition {
name: "run_shell_command".to_string(),
description: "Execute a shell command in the user's terminal and return its output. Use for running builds, tests, git operations, installing packages, or any shell operation. Commands run in the user's actual shell with their environment. Set is_read_only=true for read-only commands (ls, cat, git status) to enable auto-execution. Always use --no-pager for git commands.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"command": { "type": "string", "description": "The shell command to execute" },
"is_read_only": { "type": "boolean", "description": "True if command only reads data and makes no changes" },
"is_risky": { "type": "boolean", "description": "True if command is destructive or irreversible (rm -rf, git push --force)" }
},
"required": ["command"]
}),
}
}
fn run_shell_command_readonly() -> ToolDefinition {
ToolDefinition {
name: "run_shell_command".to_string(),
description: "Execute a READ-ONLY shell command and return its output. Only use for commands that inspect state (ls, cat, git log, git status, find, etc.). Do NOT use for commands that modify files or state. Always use --no-pager for git commands.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"command": { "type": "string", "description": "The read-only shell command to execute" },
"is_read_only": { "type": "boolean", "description": "Must be true — only read-only commands are allowed in this mode" }
},
"required": ["command"]
}),
}
}
fn read_files() -> ToolDefinition {
ToolDefinition {
name: "read_files".to_string(),
description: "Read the contents of one or more files. Pass ALL file paths you need in a single call for efficiency. Returns file contents with path headers. Binary files are detected and skipped. Use absolute paths.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"files": { "type": "array", "items": { "type": "string" }, "description": "Absolute file paths to read" }
},
"required": ["files"]
}),
}
}
fn apply_file_diffs() -> ToolDefinition {
ToolDefinition {
name: "apply_file_diffs".to_string(),
description: "Apply search/replace edits to files. Creates files if they don't exist (use empty search string). The search string must uniquely match one location in the file. Include enough surrounding context for uniqueness. For new files, use search=\"\" and put full content in replace.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"summary": { "type": "string", "description": "A brief summary of what these edits accomplish (e.g. 'Add error handling to parse_config')" },
"diffs": { "type": "array", "items": { "type": "object", "properties": { "file_path": { "type": "string", "description": "Absolute path to the file" }, "search": { "type": "string", "description": "Exact text to find (must match uniquely). Empty string to create a new file." }, "replace": { "type": "string", "description": "Text to replace with" } }, "required": ["file_path", "search", "replace"] }, "description": "Array of file edits to apply" }
},
"required": ["summary", "diffs"]
}),
}
}
fn grep() -> ToolDefinition {
ToolDefinition {
name: "grep".to_string(),
description: "Search for regex patterns in files. Uses git grep in git repos (respects .gitignore) or ripgrep otherwise. Returns file paths and matching line numbers. Use read_files afterward to see context around matches. Pass ALL patterns you need in one call.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"queries": { "type": "array", "items": { "type": "string" }, "description": "Regex patterns to search for" },
"path": { "type": "string", "description": "Directory to scope the search to" }
},
"required": ["queries"]
}),
}
}
fn file_glob() -> ToolDefinition {
ToolDefinition {
name: "file_glob".to_string(),
description: "Find files matching glob patterns. Uses git ls-files in git repos. Returns absolute file paths of matches. Common patterns: '**/*.rs', 'src/**/*.ts', '**/Cargo.toml'. Pass ALL patterns in one call.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"patterns": { "type": "array", "items": { "type": "string" }, "description": "Glob patterns to match files" },
"path": { "type": "string", "description": "Directory to search from" }
},
"required": ["patterns"]
}),
}
}
fn search_codebase() -> ToolDefinition {
ToolDefinition {
name: "search_codebase".to_string(),
description: "Semantic code search across the indexed codebase. Use for finding relevant code by meaning rather than exact text match. Better than grep for conceptual queries like 'authentication logic' or 'error handling for database connections'.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"query": { "type": "string", "description": "Natural language search query describing what you're looking for" },
"path": { "type": "string", "description": "Optional directory path to narrow search scope" }
},
"required": ["query"]
}),
}
}
fn write_to_long_running_shell_command() -> ToolDefinition {
ToolDefinition {
name: "write_to_long_running_shell_command".to_string(),
description: "Send input (stdin) to a currently running shell command. Use this to interact with commands that are waiting for input, like interactive prompts, REPLs, or commands that accept piped input.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"input": { "type": "string", "description": "Text to send as stdin to the running command" }
},
"required": ["input"]
}),
}
}
fn read_shell_command_output() -> ToolDefinition {
ToolDefinition {
name: "read_shell_command_output".to_string(),
description: "Read the latest output from a previously started long-running shell command. Use to check progress or get results from commands that are still running.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {},
"required": []
}),
}
}
fn read_mcp_resource() -> ToolDefinition {
ToolDefinition {
name: "read_mcp_resource".to_string(),
description: "Read a resource from a connected MCP (Model Context Protocol) server. Resources provide context like database schemas, API docs, or live system state.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"server_id": { "type": "string", "description": "MCP server identifier" },
"uri": { "type": "string", "description": "Resource URI to read" }
},
"required": ["server_id", "uri"]
}),
}
}
fn read_documents() -> ToolDefinition {
ToolDefinition {
name: "read_documents".to_string(),
description: "Read the contents of one or more Galaxy notebook documents by their IDs.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"document_ids": { "type": "array", "items": { "type": "string" }, "description": "Document IDs to read" }
},
"required": ["document_ids"]
}),
}
}
fn create_documents() -> ToolDefinition {
ToolDefinition {
name: "create_documents".to_string(),
description: "Create new Galaxy notebook documents with the specified title and content.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"documents": { "type": "array", "items": { "type": "object", "properties": { "title": { "type": "string" }, "content": { "type": "string" } }, "required": ["title", "content"] }, "description": "Documents to create" }
},
"required": ["documents"]
}),
}
}
fn edit_documents() -> ToolDefinition {
ToolDefinition {
name: "edit_documents".to_string(),
description: "Edit existing Galaxy notebook documents using search/replace diffs.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"diffs": { "type": "array", "items": { "type": "object", "properties": { "document_id": { "type": "string" }, "search": { "type": "string" }, "replace": { "type": "string" } }, "required": ["document_id", "search", "replace"] }, "description": "Edits to apply to documents" }
},
"required": ["diffs"]
}),
}
}
fn start_agent() -> ToolDefinition {
ToolDefinition {
name: "start_agent".to_string(),
description: "Start a sub-agent to handle a specific task autonomously. Use for delegating independent work that can run in parallel. The agent gets its own conversation context and tool access.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"name": { "type": "string", "description": "Name for the sub-agent (used for identification)" },
"prompt": { "type": "string", "description": "The task/instructions for the sub-agent to execute" }
},
"required": ["name", "prompt"]
}),
}
}
fn send_message_to_agent() -> ToolDefinition {
ToolDefinition {
name: "send_message_to_agent".to_string(),
description: "Send a message to a running sub-agent. Use to provide additional context, ask for updates, or redirect the agent's work.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "ID of the target sub-agent" },
"message": { "type": "string", "description": "Message to send to the agent" }
},
"required": ["agent_id", "message"]
}),
}
}
fn ask_user_question() -> ToolDefinition {
ToolDefinition {
name: "ask_user_question".to_string(),
description: "Ask the user a question when you need clarification or a decision. Present clear options when possible. Use sparingly \u{2014} prefer making reasonable assumptions.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"question": { "type": "string", "description": "The question to ask the user" },
"options": { "type": "array", "items": { "type": "string" }, "description": "Optional multiple-choice options to present" }
},
"required": ["question"]
}),
}
}
fn read_skill() -> ToolDefinition {
ToolDefinition {
name: "read_skill".to_string(),
description: "Read a skill definition to understand available capabilities and how to use them.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"skill": { "type": "string", "description": "Skill identifier to read" }
},
"required": ["skill"]
}),
}
}
fn fetch_conversation() -> ToolDefinition {
ToolDefinition {
name: "fetch_conversation".to_string(),
description: "Fetch the contents of a previous conversation for context. Use when the user references prior work or you need history from another session.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"conversation_id": { "type": "string", "description": "ID of the conversation to fetch" }
},
"required": ["conversation_id"]
}),
}
}