diff --git a/src/.cargo/config.toml b/src/.cargo/config.toml new file mode 100644 index 00000000..ddbe09f3 --- /dev/null +++ b/src/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustdocflags = ["-D", "warnings"] diff --git a/src/.cargo/verify.ps1 b/src/.cargo/verify.ps1 new file mode 100644 index 00000000..4bd97d78 --- /dev/null +++ b/src/.cargo/verify.ps1 @@ -0,0 +1,39 @@ +# Post-change verification script +# All steps must pass without warnings +# Keep in sync with verify.sh +# +# Note: llm-coding-tools-rig and llm-coding-tools-serdesai are async-only (implement async Tool traits). +# The blocking feature only applies to llm-coding-tools-core. + +$ErrorActionPreference = "Stop" + +Write-Host "Building..." +cargo build -p llm-coding-tools-core +cargo build -p llm-coding-tools-rig --quiet +cargo build -p llm-coding-tools-serdesai --quiet + +Write-Host "Testing..." +cargo test -p llm-coding-tools-core +cargo test -p llm-coding-tools-rig --quiet +cargo test -p llm-coding-tools-serdesai --quiet + +Write-Host "Clippy..." +cargo clippy -p llm-coding-tools-core -- -D warnings +cargo clippy -p llm-coding-tools-rig --quiet -- -D warnings +cargo clippy -p llm-coding-tools-serdesai --quiet -- -D warnings + +Write-Host "Testing blocking feature..." +cargo test -p llm-coding-tools-core --no-default-features --features blocking --quiet + +Write-Host "Docs..." +cargo doc --workspace --no-deps --quiet + +Write-Host "Formatting..." +cargo fmt --all + +Write-Host "Publish dry-run..." +cargo publish --dry-run -p llm-coding-tools-core --quiet +cargo publish --dry-run -p llm-coding-tools-rig --quiet +cargo publish --dry-run -p llm-coding-tools-serdesai --quiet + +Write-Host "All checks passed!" diff --git a/src/.cargo/verify.sh b/src/.cargo/verify.sh new file mode 100755 index 00000000..d6c53e75 --- /dev/null +++ b/src/.cargo/verify.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Post-change verification script +# All steps must pass without warnings +# Keep in sync with verify.ps1 +# +# Note: llm-coding-tools-rig and llm-coding-tools-serdesai are async-only (implement async Tool traits). +# The blocking feature only applies to llm-coding-tools-core. + +set -e + +echo "Building..." +cargo build -p llm-coding-tools-core +cargo build -p llm-coding-tools-rig --quiet +cargo build -p llm-coding-tools-serdesai --quiet + +echo "Testing..." +cargo test -p llm-coding-tools-core +cargo test -p llm-coding-tools-rig --quiet +cargo test -p llm-coding-tools-serdesai --quiet + +echo "Clippy..." +cargo clippy -p llm-coding-tools-core -- -D warnings +cargo clippy -p llm-coding-tools-rig --quiet -- -D warnings +cargo clippy -p llm-coding-tools-serdesai --quiet -- -D warnings + +echo "Testing blocking feature..." +cargo test -p llm-coding-tools-core --no-default-features --features blocking --quiet + +echo "Docs..." +cargo doc --workspace --no-deps --quiet + +echo "Formatting..." +cargo fmt --all + +echo "Publish dry-run..." +cargo publish --dry-run -p llm-coding-tools-core --quiet +cargo publish --dry-run -p llm-coding-tools-rig --quiet +cargo publish --dry-run -p llm-coding-tools-serdesai --quiet + +echo "All checks passed!" diff --git a/src/AGENTS.md b/src/AGENTS.md index 0ee8505b..ba6e03a6 100644 --- a/src/AGENTS.md +++ b/src/AGENTS.md @@ -72,16 +72,4 @@ This is a high-performance library. Optimize aggressively. # Post-Change Verification -All must pass without warnings: - -```bash -cargo build -p llm-coding-tools-core && cargo build -p llm-coding-tools-rig --quiet && cargo build -p llm-coding-tools-serdesai --quiet && cargo test -p llm-coding-tools-core && cargo test -p llm-coding-tools-rig --quiet && cargo test -p llm-coding-tools-serdesai --quiet && cargo clippy -p llm-coding-tools-core -- -D warnings && cargo clippy -p llm-coding-tools-rig --quiet -- -D warnings && cargo clippy -p llm-coding-tools-serdesai --quiet -- -D warnings && cargo test -p llm-coding-tools-core --no-default-features --features blocking --quiet && cargo doc --workspace --no-deps --quiet && cargo fmt --all -``` - -Note: `llm-coding-tools-rig` and `llm-coding-tools-serdesai` are async-only (implement async `Tool` traits). -The `blocking` feature only applies to `llm-coding-tools-core`. - -For individual crates: -```bash -cargo publish --dry-run -p llm-coding-tools-core --quiet && cargo publish --dry-run -p llm-coding-tools-rig --quiet && cargo publish --dry-run -p llm-coding-tools-serdesai --quiet -``` +All must pass without warnings. Run: `.cargo/verify.sh` (or `.cargo/verify.ps1` on Windows) diff --git a/src/llm-coding-tools-core/examples/preamble_preview.rs b/src/llm-coding-tools-core/examples/preamble_preview.rs new file mode 100644 index 00000000..3d4b4b42 --- /dev/null +++ b/src/llm-coding-tools-core/examples/preamble_preview.rs @@ -0,0 +1,130 @@ +//! Preamble preview - demonstrates full preamble generation. +//! +//! Shows how PreambleBuilder combines: +//! - Custom system prompts +//! - Environment section (working directory, allowed paths) +//! - Tool usage guidelines (from tracked tools) +//! - Supplemental context (git workflow, GitHub CLI) +//! +//! Run: cargo run --example preamble_preview -p llm-coding-tools-core + +use llm_coding_tools_core::context::ToolContext; +use llm_coding_tools_core::{context, AllowedPathResolver, PreambleBuilder}; + +fn main() { + // Use from_canonical to avoid filesystem requirements for the example. + // In real usage, AllowedPathResolver::new() canonicalizes and validates paths. + let resolver = AllowedPathResolver::from_canonical(["/home/user/project", "/tmp"]); + + // Build preamble with all features demonstrated + let mut pb = PreambleBuilder::new() + .system_prompt( + "# System Instructions\n\n\ + You are a helpful coding assistant. Follow best practices and \ + write clean, maintainable code.", + ) + .working_directory("/home/user/project") + .allowed_paths(&resolver) + .add_context("Git Workflow", context::GIT_WORKFLOW) + .add_context("GitHub CLI", context::GITHUB_CLI); + + // Track tools - in real usage this would be: + // .tool(pb.track(ReadTool::new())) + // For the preview, we just register them without using the returned tool. + let _ = pb.track(MockReadTool); + let _ = pb.track(MockWriteTool); + let _ = pb.track(MockEditTool); + let _ = pb.track(MockBashTool); + let _ = pb.track(MockGlobTool); + let _ = pb.track(MockGrepTool); + let _ = pb.track(MockWebFetchTool); + let _ = pb.track(MockTodoWriteTool); + let _ = pb.track(MockTodoReadTool); + + let preamble = pb.build(); + + // Output the preamble + println!("{preamble}"); + + // Show statistics for token estimation + println!("\n{}", "=".repeat(60)); + println!("Statistics:"); + println!(" Characters: {}", preamble.len()); + println!(" Lines: {}", preamble.lines().count()); + println!(" Estimated tokens: ~{} (chars/4)", preamble.len() / 4); +} + +// Mock tools implementing ToolContext for demonstration. +// In real usage, these would be actual tool structs from llm-coding-tools-rig. + +struct MockReadTool; +impl ToolContext for MockReadTool { + const NAME: &'static str = "read"; + fn context(&self) -> &'static str { + context::READ_ALLOWED + } +} + +struct MockWriteTool; +impl ToolContext for MockWriteTool { + const NAME: &'static str = "write"; + fn context(&self) -> &'static str { + context::WRITE_ALLOWED + } +} + +struct MockEditTool; +impl ToolContext for MockEditTool { + const NAME: &'static str = "edit"; + fn context(&self) -> &'static str { + context::EDIT_ALLOWED + } +} + +struct MockBashTool; +impl ToolContext for MockBashTool { + const NAME: &'static str = "bash"; + fn context(&self) -> &'static str { + context::BASH + } +} + +struct MockGlobTool; +impl ToolContext for MockGlobTool { + const NAME: &'static str = "glob"; + fn context(&self) -> &'static str { + context::GLOB_ALLOWED + } +} + +struct MockGrepTool; +impl ToolContext for MockGrepTool { + const NAME: &'static str = "grep"; + fn context(&self) -> &'static str { + context::GREP_ALLOWED + } +} + +struct MockWebFetchTool; +impl ToolContext for MockWebFetchTool { + const NAME: &'static str = "webfetch"; + fn context(&self) -> &'static str { + context::WEBFETCH + } +} + +struct MockTodoWriteTool; +impl ToolContext for MockTodoWriteTool { + const NAME: &'static str = "todowrite"; + fn context(&self) -> &'static str { + context::TODO_WRITE + } +} + +struct MockTodoReadTool; +impl ToolContext for MockTodoReadTool { + const NAME: &'static str = "todoread"; + fn context(&self) -> &'static str { + context::TODO_READ + } +} diff --git a/src/llm-coding-tools-core/src/context/bash.txt b/src/llm-coding-tools-core/src/context/bash.txt index 4102ccbd..5c40dac0 100644 --- a/src/llm-coding-tools-core/src/context/bash.txt +++ b/src/llm-coding-tools-core/src/context/bash.txt @@ -6,115 +6,41 @@ IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO N Before executing the command, please follow these steps: -1. Directory Verification: - - If the command will create new directories or files, first use `ls` to verify the parent directory exists and is the correct location - - For example, before running "mkdir foo/bar", first use `ls foo` to check that "foo" exists and is the intended parent directory - -2. Command Execution: - - Always quote file paths that contain spaces with double quotes (e.g., rm "path with spaces/file.txt") - - Examples of proper quoting: - - mkdir "/Users/name/My Documents" (correct) - - mkdir /Users/name/My Documents (incorrect - will fail) - - python "/path/with spaces/script.py" (correct) - - python /path/with spaces/script.py (incorrect - will fail) - - After ensuring proper quoting, execute the command. - - Capture the output of the command. - -Usage notes: - - The `command` argument is required. - - You can specify an optional `timeout_ms` in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes). - - It is very helpful if you write a clear, concise description of what this command does in 5-10 words. - - If the output exceeds 30000 characters, output will be truncated before being returned to you. - - - Avoid using Bash with the `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or `echo` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands: - - File search: Use Glob (NOT find or ls) - - Content search: Use Grep (NOT grep) - - Read files: Use Read (NOT cat/head/tail) - - Edit files: Use Edit (NOT sed/awk) - - Write files: Use Write (NOT echo >/cat < && `. Use the `workdir` parameter to change directories instead. - - Use workdir="/foo/bar" with command: pytest tests - - - cd /foo/bar && pytest tests - - -# Committing changes with git - -Only create commits when requested by the user. If unclear, ask first. When the user asks you to create a new git commit, follow these steps carefully: - -Git Safety Protocol: -- NEVER update the git config -- NEVER run destructive/irreversible git commands (like push --force, hard reset, etc) unless the user explicitly requests them -- NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it -- NEVER run force push to main/master, warn the user if they request it -- Avoid git commit --amend. ONLY use --amend when ALL conditions are met: - (1) User explicitly requested amend, OR commit SUCCEEDED but pre-commit hook auto-modified files that need including - (2) HEAD commit was created by you in this conversation (verify: git log -1 --format='%an %ae') - (3) Commit has NOT been pushed to remote (verify: git status shows "Your branch is ahead") -- CRITICAL: If commit FAILED or was REJECTED by hook, NEVER amend - fix the issue and create a NEW commit -- CRITICAL: If you already pushed to remote, NEVER amend unless user explicitly requests it (requires force push) -- NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive. - -1. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following bash commands in parallel, each using the Bash tool: - - Run a git status command to see all untracked files. - - Run a git diff command to see both staged and unstaged changes that will be committed. - - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style. -2. Analyze all staged changes (both previously staged and newly added) and draft a commit message: - - Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.). Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.). - - Do not commit files that likely contain secrets (.env, credentials.json, etc.). Warn the user if they specifically request to commit those files - - Draft a concise (1-2 sentences) commit message that focuses on the "why" rather than the "what" - - Ensure it accurately reflects the changes and their purpose -3. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following commands: - - Add relevant untracked files to the staging area. - - Create the commit with a message - - Run git status after the commit completes to verify success. - Note: git status depends on the commit completing, so run it sequentially after the commit. -4. If the commit fails due to pre-commit hook, fix the issue and create a NEW commit (see amend rules above) - -Important notes: -- NEVER run additional commands to read or explore code, besides git bash commands -- NEVER use the TodoWrite or Task tools -- DO NOT push to the remote repository unless the user explicitly asks you to do so -- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported. -- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit - -# Creating pull requests -Use the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed. - -IMPORTANT: When the user asks you to create a pull request, follow these steps carefully: - -1. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following bash commands in parallel using the Bash tool, in order to understand the current state of the branch since it diverged from the main branch: - - Run a git status command to see all untracked files - - Run a git diff command to see both staged and unstaged changes that will be committed - - Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote - - Run a git log command and `git diff [base-branch]...HEAD` to understand the full commit history for the current branch (from the time it diverged from the base branch) -2. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (NOT just the latest commit, but ALL commits that will be included in the pull request!!!), and draft a pull request summary -3. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following commands in parallel: - - Create new branch if needed - - Push to remote with -u flag if needed - - Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting. - -gh pr create --title "the pr title" --body "$(cat <<'EOF' -## Summary -<1-3 bullet points> - -## Test plan -[Bulleted markdown checklist of TODOs for testing the pull request...] -EOF -)" - - -Important: -- DO NOT use the TodoWrite or Task tools -- Return the PR URL when you're done, so the user can see it - -# Other common operations -- View comments on a Github PR: gh api repos/foo/bar/pulls/123/comments +#### Directory Verification +- If the command will create new directories or files, first use `ls` to verify the parent directory exists and is the correct location +- For example, before running "mkdir foo/bar", first use `ls foo` to check that "foo" exists and is the intended parent directory + +#### Command Execution +- Always quote file paths that contain spaces with double quotes (e.g., rm "path with spaces/file.txt") +- Examples of proper quoting: + - mkdir "/Users/name/My Documents" (correct) + - mkdir /Users/name/My Documents (incorrect - will fail) + - python "/path/with spaces/script.py" (correct) + - python /path/with spaces/script.py (incorrect - will fail) +- After ensuring proper quoting, execute the command. +- Capture the output of the command. + +##### Usage notes +- The `command` argument is required. +- You can specify an optional `timeout_ms` in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes). +- It is very helpful if you write a clear, concise description of what this command does in 5-10 words. +- If the output exceeds 30000 characters, output will be truncated before being returned to you. +- Avoid using Bash with the `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or `echo` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands: + - File search: Use Glob (NOT find or ls) + - Content search: Use Grep (NOT grep) + - Read files: Use Read (NOT cat/head/tail) + - Edit files: Use Edit (NOT sed/awk) + - Write files: Use Write (NOT echo >/cat < && `. Use the `workdir` parameter to change directories instead. + + Use workdir="/foo/bar" with command: pytest tests + + + cd /foo/bar && pytest tests + diff --git a/src/llm-coding-tools-core/src/context/edit_absolute.txt b/src/llm-coding-tools-core/src/context/edit_absolute.txt index 9b54d7d2..2840b792 100644 --- a/src/llm-coding-tools-core/src/context/edit_absolute.txt +++ b/src/llm-coding-tools-core/src/context/edit_absolute.txt @@ -6,19 +6,19 @@ Usage: - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required. - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked. -## Parameters +### Parameters - `file_path`: Absolute path to the file to modify (required) - `old_string`: Exact text to find and replace (required) - `new_string`: Replacement text (required) - `replace_all`: Replace all occurrences when true, default false (optional) -## Error Behavior +### Error Behavior - The edit will FAIL if `old_string` is not found in the file with an error "oldString not found in content". - The edit will FAIL if `old_string` is found multiple times in the file with an error "oldString found multiple times and requires more code context to uniquely identify the intended match". Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`. -## When to Use This Tool +### When to Use This Tool - Making targeted changes to existing files - Fixing bugs in specific code sections @@ -26,13 +26,13 @@ Usage: - Renaming variables across a file (with `replace_all: true`) - Adding new code to existing files -## When NOT to Use This Tool +### When NOT to Use This Tool - Creating new files - use Write tool instead - When most of a file needs to change - use Write tool instead - When you haven't read the file yet - read it first! -## Examples +### Examples Replacing a single occurrence: ``` @@ -56,7 +56,7 @@ old_string: "use std::io;" new_string: "use std::io;\nuse std::fs;" ``` -## Best Practices +### Best Practices 1. Always read the file first using the Read tool 2. Copy the exact text from the Read output, preserving whitespace and indentation @@ -65,7 +65,7 @@ new_string: "use std::io;\nuse std::fs;" 5. Use `replace_all: true` when renaming variables or making consistent changes 6. Don't include line number prefixes (like "L42: ") in your old_string or new_string -## Common Mistakes to Avoid +### Common Mistakes to Avoid - Forgetting to read the file first - Including line number prefixes in old_string diff --git a/src/llm-coding-tools-core/src/context/edit_allowed.txt b/src/llm-coding-tools-core/src/context/edit_allowed.txt index 615c42a0..24663175 100644 --- a/src/llm-coding-tools-core/src/context/edit_allowed.txt +++ b/src/llm-coding-tools-core/src/context/edit_allowed.txt @@ -8,19 +8,19 @@ Usage: - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required. - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked. -## Parameters +### Parameters - `file_path`: Path to the file to modify - can be relative or absolute within allowed directories (required) - `old_string`: Exact text to find and replace (required) - `new_string`: Replacement text (required) - `replace_all`: Replace all occurrences when true, default false (optional) -## Error Behavior +### Error Behavior - The edit will FAIL if `old_string` is not found in the file with an error "oldString not found in content". - The edit will FAIL if `old_string` is found multiple times in the file with an error "oldString found multiple times and requires more code context to uniquely identify the intended match". Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`. -## When to Use This Tool +### When to Use This Tool - Making targeted changes to existing files - Fixing bugs in specific code sections @@ -28,13 +28,13 @@ Usage: - Renaming variables across a file (with `replace_all: true`) - Adding new code to existing files -## When NOT to Use This Tool +### When NOT to Use This Tool - Creating new files - use Write tool instead - When most of a file needs to change - use Write tool instead - When you haven't read the file yet - read it first! -## Examples +### Examples Replacing a single occurrence: ``` @@ -58,7 +58,7 @@ old_string: "use std::io;" new_string: "use std::io;\nuse std::fs;" ``` -## Best Practices +### Best Practices 1. Always read the file first using the Read tool 2. Copy the exact text from the Read output, preserving whitespace and indentation @@ -68,7 +68,7 @@ new_string: "use std::io;\nuse std::fs;" 6. Don't include line number prefixes (like "L42: ") in your old_string or new_string 7. Relative paths are resolved against allowed directories -## Common Mistakes to Avoid +### Common Mistakes to Avoid - Forgetting to read the file first - Including line number prefixes in old_string diff --git a/src/llm-coding-tools-core/src/context/git_workflow.txt b/src/llm-coding-tools-core/src/context/git_workflow.txt new file mode 100644 index 00000000..2a378ec1 --- /dev/null +++ b/src/llm-coding-tools-core/src/context/git_workflow.txt @@ -0,0 +1,45 @@ +Only create commits when requested by the user. If unclear, ask first. When the user asks you to create a new git commit, follow these steps carefully: + +Git Safety Protocol: +- NEVER update the git config +- NEVER run destructive/irreversible git commands (like push --force, hard reset, etc.) unless the user explicitly requests them +- NEVER skip hooks (--no-verify, --no-gpg-sign, etc.) unless the user explicitly requests it +- NEVER run force push to main/master, warn the user if they request it +- Avoid git commit --amend. ONLY use --amend when ALL conditions are met: + (1) User explicitly requested amend, OR commit SUCCEEDED but pre-commit hook auto-modified files that need including + (2) HEAD commit was created by you in this conversation (verify: git log -1 --format='%an %ae') + (3) Commit has NOT been pushed to remote (verify: git status shows "Your branch is ahead") +- CRITICAL: If commit FAILED or was REJECTED by hook, NEVER amend - fix the issue and create a NEW commit +- CRITICAL: If you already pushed to remote, NEVER amend unless user explicitly requests it (requires force push) +- NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive. + +You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. + +#### Step 1: Gather Information +Run the following bash commands in parallel, each using the Bash tool: +- Run git status to see all untracked files +- Run git diff to see both staged and unstaged changes +- Run git log to see recent commit messages for style reference + +#### Step 2: Draft Commit Message +Analyze all staged changes (both previously staged and newly added): +- Summarize the nature of changes (new feature, enhancement, bug fix, refactoring, test, docs, etc.) +- Ensure the message accurately reflects changes and purpose ("add" = new feature, "update" = enhancement, "fix" = bug fix) +- Do not commit files that likely contain secrets (.env, credentials.json, etc.). Warn the user if requested. +- Draft a concise (1-2 sentences) commit message focusing on "why" rather than "what" + +#### Step 3: Create Commit +Run the following commands using the Bash tool: +- Add relevant untracked files to staging +- Create the commit with your message +- Run git status after commit to verify success (sequentially, since it depends on commit completing) + +#### Step 4: Handle Failures +If the commit fails due to pre-commit hook, fix the issue and create a NEW commit (see amend rules above). + +Important notes: +- NEVER run additional commands to read or explore code, besides git bash commands +- NEVER use the TodoWrite or Task tools +- DO NOT push to the remote repository unless the user explicitly asks you to do so +- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported. +- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit diff --git a/src/llm-coding-tools-core/src/context/github_cli.txt b/src/llm-coding-tools-core/src/context/github_cli.txt new file mode 100644 index 00000000..dc6bf90e --- /dev/null +++ b/src/llm-coding-tools-core/src/context/github_cli.txt @@ -0,0 +1,41 @@ +Use the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a GitHub URL use the gh command to get the information needed. + +### Creating Pull Requests + +IMPORTANT: When the user asks you to create a pull request, follow these steps carefully: + +You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. + +#### Step 1: Gather Branch Information +Run the following bash commands in parallel using the Bash tool, in order to understand the current state of the branch since it diverged from the main branch: +- Run git status to see all untracked files +- Run git diff to see both staged and unstaged changes +- Check if current branch tracks a remote and is up to date (to know if push is needed) +- Run git log and `git diff [base-branch]...HEAD` to understand full commit history since diverging + +#### Step 2: Draft PR Summary +Analyze ALL changes that will be included in the pull request (NOT just the latest commit, but ALL commits). Draft a pull request summary. + +#### Step 3: Create PR +Run the following commands in parallel using the Bash tool: +- Create new branch if needed +- Push to remote with -u flag if needed +- Create PR using gh pr create with the format below (use HEREDOC for body): + +gh pr create --title "the pr title" --body "$(cat <<'EOF' +## Summary +<1-3 bullet points> + +## Test plan +[Bulleted markdown checklist of TODOs for testing the pull request...] +EOF +)" + + +Important: +- DO NOT use the TodoWrite or Task tools +- Return the PR URL when you're done, so the user can see it + +### Other Common Operations + +- View comments on a GitHub PR: gh api repos/foo/bar/pulls/123/comments diff --git a/src/llm-coding-tools-core/src/context/glob_absolute.txt b/src/llm-coding-tools-core/src/context/glob_absolute.txt index b8f9f3bc..38d6b6d3 100644 --- a/src/llm-coding-tools-core/src/context/glob_absolute.txt +++ b/src/llm-coding-tools-core/src/context/glob_absolute.txt @@ -6,7 +6,7 @@ Fast file pattern matching tool that works with any codebase size. - Use this tool when you need to find files by name patterns - When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead -## Parameters +### Parameters - `pattern`: Glob pattern to match files against (required) - `*` matches any characters except path separators @@ -16,20 +16,20 @@ Fast file pattern matching tool that works with any codebase size. - `{a,b}` matches either pattern - `path`: Absolute directory path to search in (required) -## When to Use This Tool +### When to Use This Tool - Finding files by extension: `**/*.rs`, `**/*.tsx` - Finding files by name pattern: `**/test_*.py`, `**/*_spec.js` - Locating configuration files: `**/Cargo.toml`, `**/package.json` - Finding files in specific directories: `src/**/*.rs` -## When NOT to Use This Tool +### When NOT to Use This Tool - Searching for content inside files - use Grep instead - Reading file contents - use Read instead - Complex multi-step searches - use Task tool instead -## Examples +### Examples Find all Rust files: ``` @@ -55,7 +55,7 @@ pattern: "**/Cargo.toml" path: "/home/user/project" ``` -## Best Practices +### Best Practices 1. You can call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful. 2. Start with broader patterns and narrow down if needed diff --git a/src/llm-coding-tools-core/src/context/glob_allowed.txt b/src/llm-coding-tools-core/src/context/glob_allowed.txt index 5fef3589..2a4fac82 100644 --- a/src/llm-coding-tools-core/src/context/glob_allowed.txt +++ b/src/llm-coding-tools-core/src/context/glob_allowed.txt @@ -8,7 +8,7 @@ Fast file pattern matching tool that works with any codebase size. - Use this tool when you need to find files by name patterns - When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead -## Parameters +### Parameters - `pattern`: Glob pattern to match files against (required) - `*` matches any characters except path separators @@ -18,20 +18,20 @@ Fast file pattern matching tool that works with any codebase size. - `{a,b}` matches either pattern - `path`: Directory path to search in - can be relative or absolute within allowed directories (required) -## When to Use This Tool +### When to Use This Tool - Finding files by extension: `**/*.rs`, `**/*.tsx` - Finding files by name pattern: `**/test_*.py`, `**/*_spec.js` - Locating configuration files: `**/Cargo.toml`, `**/package.json` - Finding files in specific directories: `src/**/*.rs` -## When NOT to Use This Tool +### When NOT to Use This Tool - Searching for content inside files - use Grep instead - Reading file contents - use Read instead - Complex multi-step searches - use Task tool instead -## Examples +### Examples Find all Rust files: ``` @@ -57,7 +57,7 @@ pattern: "**/Cargo.toml" path: "." ``` -## Best Practices +### Best Practices 1. You can call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful. 2. Start with broader patterns and narrow down if needed diff --git a/src/llm-coding-tools-core/src/context/grep_absolute.txt b/src/llm-coding-tools-core/src/context/grep_absolute.txt index c9684db8..f0db44f5 100644 --- a/src/llm-coding-tools-core/src/context/grep_absolute.txt +++ b/src/llm-coding-tools-core/src/context/grep_absolute.txt @@ -9,14 +9,14 @@ Fast content search tool built on ripgrep. Works with any codebase size. IMPORTANT: ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command. The Grep tool has been optimized for correct permissions and access. -## Parameters +### Parameters - `pattern`: Regex pattern to search for in file contents (required) - `path`: Absolute directory path to search in (required) - `include`: Optional file glob filter (e.g., "*.rs", "*.{ts,tsx}") - `limit`: Maximum number of matches to return (default: 100, max: 2000) -## Pattern Syntax Notes (ripgrep-based) +### Pattern Syntax Notes (ripgrep-based) - Literal braces need escaping: use `interface\\{\\}` to find `interface{}` in Go code - Use `\\b` for word boundaries: `\\bfoo\\b` matches "foo" but not "foobar" @@ -25,7 +25,7 @@ IMPORTANT: ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Ba - Use `|` for alternation: `TODO|FIXME` matches either - Patterns match within single lines only; multiline patterns are not supported -## When to Use This Tool +### When to Use This Tool - Finding function definitions: `fn\\s+process_` - Finding usages of a variable or function: `\\bmy_function\\(` @@ -33,13 +33,13 @@ IMPORTANT: ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Ba - Finding error messages: `error.*failed` - Finding imports: `^use\\s+` -## When NOT to Use This Tool +### When NOT to Use This Tool - Finding files by name - use Glob instead - Reading entire file contents - use Read instead - Complex multi-step research - use Task tool instead -## Examples +### Examples Find all function definitions: ``` @@ -68,7 +68,7 @@ include: "*.rs" limit: 50 ``` -## Best Practices +### Best Practices 1. You can call multiple tools in a single response. It is always better to speculatively perform multiple searches in parallel if they are potentially useful. 2. Use the `include` parameter to narrow searches to relevant file types diff --git a/src/llm-coding-tools-core/src/context/grep_allowed.txt b/src/llm-coding-tools-core/src/context/grep_allowed.txt index 9fe4edc4..7919cd3e 100644 --- a/src/llm-coding-tools-core/src/context/grep_allowed.txt +++ b/src/llm-coding-tools-core/src/context/grep_allowed.txt @@ -11,14 +11,14 @@ Fast content search tool built on ripgrep. Works with any codebase size. IMPORTANT: ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command. The Grep tool has been optimized for correct permissions and access. -## Parameters +### Parameters - `pattern`: Regex pattern to search for in file contents (required) - `path`: Directory path to search in - can be relative or absolute within allowed directories (required) - `include`: Optional file glob filter (e.g., "*.rs", "*.{ts,tsx}") - `limit`: Maximum number of matches to return (default: 100, max: 2000) -## Pattern Syntax Notes (ripgrep-based) +### Pattern Syntax Notes (ripgrep-based) - Literal braces need escaping: use `interface\\{\\}` to find `interface{}` in Go code - Use `\\b` for word boundaries: `\\bfoo\\b` matches "foo" but not "foobar" @@ -27,7 +27,7 @@ IMPORTANT: ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Ba - Use `|` for alternation: `TODO|FIXME` matches either - Patterns match within single lines only; multiline patterns are not supported -## When to Use This Tool +### When to Use This Tool - Finding function definitions: `fn\\s+process_` - Finding usages of a variable or function: `\\bmy_function\\(` @@ -35,13 +35,13 @@ IMPORTANT: ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Ba - Finding error messages: `error.*failed` - Finding imports: `^use\\s+` -## When NOT to Use This Tool +### When NOT to Use This Tool - Finding files by name - use Glob instead - Reading entire file contents - use Read instead - Complex multi-step research - use Task tool instead -## Examples +### Examples Find all function definitions: ``` @@ -70,7 +70,7 @@ include: "*.rs" limit: 50 ``` -## Best Practices +### Best Practices 1. You can call multiple tools in a single response. It is always better to speculatively perform multiple searches in parallel if they are potentially useful. 2. Use the `include` parameter to narrow searches to relevant file types diff --git a/src/llm-coding-tools-core/src/context/mod.rs b/src/llm-coding-tools-core/src/context/mod.rs index bd129316..91a6a5b0 100644 --- a/src/llm-coding-tools-core/src/context/mod.rs +++ b/src/llm-coding-tools-core/src/context/mod.rs @@ -26,6 +26,18 @@ /// Bash tool context - shell command execution guidance. pub const BASH: &str = include_str!("bash.txt"); +/// Git workflow context - commit creation guidance. +/// +/// Supplemental context for agents using git via the Bash tool. +/// Include via [`PreambleBuilder::add_context`](crate::PreambleBuilder::add_context). +pub const GIT_WORKFLOW: &str = include_str!("git_workflow.txt"); + +/// GitHub CLI context - gh command usage guidance. +/// +/// Supplemental context for agents using the GitHub CLI via the Bash tool. +/// Include via [`PreambleBuilder::add_context`](crate::PreambleBuilder::add_context). +pub const GITHUB_CLI: &str = include_str!("github_cli.txt"); + /// Todo read tool context - reading task lists. pub const TODO_READ: &str = include_str!("todoread.txt"); @@ -106,6 +118,14 @@ mod tests { fn context_strings_are_not_empty() { // Non-path tools assert!(!BASH.is_empty(), "BASH context should not be empty"); + assert!( + !GIT_WORKFLOW.is_empty(), + "GIT_WORKFLOW context should not be empty" + ); + assert!( + !GITHUB_CLI.is_empty(), + "GITHUB_CLI context should not be empty" + ); assert!( !TODO_READ.is_empty(), "TODO_READ context should not be empty" @@ -192,4 +212,44 @@ mod tests { "GREP_ALLOWED should mention allowed directories" ); } + + #[test] + fn git_workflow_contains_expected_content() { + assert!( + GIT_WORKFLOW.contains("git commit"), + "GIT_WORKFLOW should mention git commit" + ); + assert!( + GIT_WORKFLOW.contains("NEVER"), + "GIT_WORKFLOW should contain safety rules" + ); + } + + #[test] + fn github_cli_contains_expected_content() { + assert!( + GITHUB_CLI.contains("gh "), + "GITHUB_CLI should mention gh command" + ); + assert!( + GITHUB_CLI.contains("pull request"), + "GITHUB_CLI should mention pull requests" + ); + } + + #[test] + fn bash_does_not_contain_git_workflow() { + assert!( + !BASH.contains("# Committing changes with git"), + "BASH should not contain git workflow section" + ); + } + + #[test] + fn bash_does_not_contain_github_cli() { + assert!( + !BASH.contains("# Creating pull requests"), + "BASH should not contain GitHub CLI section" + ); + } } diff --git a/src/llm-coding-tools-core/src/context/read_absolute.txt b/src/llm-coding-tools-core/src/context/read_absolute.txt index b6fe01da..8d9bcb9e 100644 --- a/src/llm-coding-tools-core/src/context/read_absolute.txt +++ b/src/llm-coding-tools-core/src/context/read_absolute.txt @@ -13,7 +13,7 @@ Usage: You can call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful. -## When to Use This Tool +### When to Use This Tool - Reading source code files to understand implementation - Viewing configuration files @@ -21,13 +21,13 @@ You can call multiple tools in a single response. It is always better to specula - Reading log files for debugging - Viewing images and screenshots provided by the user -## When NOT to Use This Tool +### When NOT to Use This Tool - To list directory contents - use bash with `ls` instead - To search for patterns across files - use Grep instead - To find files by name - use Glob instead -## Examples +### Examples Reading a full file: ``` @@ -41,7 +41,7 @@ offset: 100 limit: 100 ``` -## Best Practices +### Best Practices 1. Read files before editing them - the Edit tool requires you to have read the file first 2. When exploring a codebase, read multiple related files in parallel to save time diff --git a/src/llm-coding-tools-core/src/context/read_allowed.txt b/src/llm-coding-tools-core/src/context/read_allowed.txt index ba55cba5..071e9cff 100644 --- a/src/llm-coding-tools-core/src/context/read_allowed.txt +++ b/src/llm-coding-tools-core/src/context/read_allowed.txt @@ -14,7 +14,7 @@ Usage: You can call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful. -## When to Use This Tool +### When to Use This Tool - Reading source code files to understand implementation - Viewing configuration files @@ -22,13 +22,13 @@ You can call multiple tools in a single response. It is always better to specula - Reading log files for debugging - Viewing images and screenshots provided by the user -## When NOT to Use This Tool +### When NOT to Use This Tool - To list directory contents - use bash with `ls` instead - To search for patterns across files - use Grep instead - To find files by name - use Glob instead -## Examples +### Examples Reading a full file: ``` @@ -42,7 +42,7 @@ offset: 100 limit: 100 ``` -## Best Practices +### Best Practices 1. Read files before editing them - the Edit tool requires you to have read the file first 2. When exploring a codebase, read multiple related files in parallel to save time diff --git a/src/llm-coding-tools-core/src/context/todowrite.txt b/src/llm-coding-tools-core/src/context/todowrite.txt index 9204ec48..5b4d8f73 100644 --- a/src/llm-coding-tools-core/src/context/todowrite.txt +++ b/src/llm-coding-tools-core/src/context/todowrite.txt @@ -1,7 +1,7 @@ Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user. It also helps the user understand the progress of the task and overall progress of their requests. -## When to Use This Tool +### When to Use This Tool Use this tool proactively in these scenarios: @@ -13,7 +13,7 @@ Use this tool proactively in these scenarios: 6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time 7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation -## When NOT to Use This Tool +### When NOT to Use This Tool Skip using this tool when: 1. There is only a single, straightforward task @@ -23,7 +23,7 @@ Skip using this tool when: NOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly. -## Examples of When to Use the Todo List +### Examples of When to Use the Todo List User: I want to add a dark mode toggle to the application settings. Make sure you run the tests and build when you're done! @@ -60,7 +60,7 @@ The assistant used the todo list because: -## Examples of When NOT to Use the Todo List +### Examples of When NOT to Use the Todo List User: How do I print 'Hello World' in Python? @@ -87,38 +87,39 @@ The assistant did not use the todo list because this is a single, straightforwar -## Task States and Management - -1. **Task States**: Use these states to track progress: - - pending: Task not yet started - - in_progress: Currently working on (limit to ONE task at a time) - - completed: Task finished successfully - - **IMPORTANT**: Task descriptions should use imperative form describing what needs to be done: - - "Run tests" (not "Running tests") - - "Build the project" (not "Building the project") - - "Fix authentication bug" (not "Fixing authentication bug") - -2. **Task Management**: - - Update task status in real-time as you work - - Mark tasks complete IMMEDIATELY after finishing (don't batch completions) - - Exactly ONE task must be in_progress at any time (not less, not more) - - Complete current tasks before starting new ones - - Remove tasks that are no longer relevant from the list entirely - -3. **Task Completion Requirements**: - - ONLY mark a task as completed when you have FULLY accomplished it - - If you encounter errors, blockers, or cannot finish, keep the task as in_progress - - When blocked, create a new task describing what needs to be resolved - - Never mark a task as completed if: - - Tests are failing - - Implementation is partial - - You encountered unresolved errors - - You couldn't find necessary files or dependencies - -4. **Task Breakdown**: - - Create specific, actionable items - - Break complex tasks into smaller, manageable steps - - Use clear, descriptive task names +### Task States and Management + +#### Task States +Use these states to track progress: +- pending: Task not yet started +- in_progress: Currently working on (limit to ONE task at a time) +- completed: Task finished successfully + +**IMPORTANT**: Task descriptions should use imperative form: +- "Run tests" (not "Running tests") +- "Build the project" (not "Building the project") +- "Fix authentication bug" (not "Fixing authentication bug") + +#### Task Management +- Update task status in real-time as you work +- Mark tasks complete IMMEDIATELY after finishing (don't batch completions) +- Exactly ONE task must be in_progress at any time (not less, not more) +- Complete current tasks before starting new ones +- Remove tasks that are no longer relevant from the list entirely + +#### Task Completion Requirements +- ONLY mark a task as completed when you have FULLY accomplished it +- If you encounter errors, blockers, or cannot finish, keep the task as in_progress +- When blocked, create a new task describing what needs to be resolved +- Never mark a task as completed if: + - Tests are failing + - Implementation is partial + - You encountered unresolved errors + - You couldn't find necessary files or dependencies + +#### Task Breakdown +- Create specific, actionable items +- Break complex tasks into smaller, manageable steps +- Use clear, descriptive task names When in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully. diff --git a/src/llm-coding-tools-core/src/context/webfetch.txt b/src/llm-coding-tools-core/src/context/webfetch.txt index 3375178f..c9ac1b8b 100644 --- a/src/llm-coding-tools-core/src/context/webfetch.txt +++ b/src/llm-coding-tools-core/src/context/webfetch.txt @@ -6,14 +6,14 @@ Fetches content from a specified URL and processes it for analysis. - Other content types are returned as-is - Use this tool when you need to retrieve and analyze web content -## Parameters +### Parameters - `url`: The URL to fetch content from (required) - Must be a fully-formed valid URL - HTTP URLs will be automatically upgraded to HTTPS - `timeout_ms`: Optional timeout in milliseconds (default varies by implementation) -## Usage Notes +### Usage Notes - IMPORTANT: If another tool is present that offers better web fetching capabilities, is more targeted to the task, or has fewer restrictions, prefer using that tool instead of this one. - The URL must be a fully-formed valid URL (e.g., "https://example.com/page") @@ -22,20 +22,20 @@ Fetches content from a specified URL and processes it for analysis. - This tool is read-only and does not modify any files - Results may be summarized if the content is very large -## When to Use This Tool +### When to Use This Tool - Fetching documentation from the web - Reading API references or library documentation - Retrieving content from URLs provided by the user - Checking website content for analysis -## When NOT to Use This Tool +### When NOT to Use This Tool - For local file operations - use Read tool instead - For searching the web - this only fetches specific URLs - When another MCP tool offers better web capabilities -## Examples +### Examples Fetching a documentation page: ``` @@ -48,7 +48,7 @@ url: "https://api.example.com/large-response" timeout_ms: 30000 ``` -## Best Practices +### Best Practices 1. Provide complete URLs including the protocol (https://) 2. Use this tool for specific URLs, not for web searching diff --git a/src/llm-coding-tools-core/src/context/write_absolute.txt b/src/llm-coding-tools-core/src/context/write_absolute.txt index 547d1e16..4dde1fc7 100644 --- a/src/llm-coding-tools-core/src/context/write_absolute.txt +++ b/src/llm-coding-tools-core/src/context/write_absolute.txt @@ -7,25 +7,25 @@ Usage: - NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User. - Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked. -## Parameters +### Parameters - `file_path`: Absolute path for the file to write (required) - `content`: Content to write to the file (required) -## When to Use This Tool +### When to Use This Tool - Creating new files that don't exist yet - Completely rewriting a file when most content changes - Writing generated output (build artifacts, reports, etc.) - Creating new source files when explicitly requested -## When NOT to Use This Tool +### When NOT to Use This Tool - Modifying existing files - use Edit tool instead (more precise, less error-prone) - Creating documentation unless explicitly requested - Writing files you haven't read first (if they exist) -## Examples +### Examples Creating a new file: ``` @@ -33,7 +33,7 @@ file_path: "/home/user/project/src/new_module.rs" content: "//! New module\n\npub fn hello() {\n println!(\"Hello!\");\n}\n" ``` -## Best Practices +### Best Practices 1. ALWAYS read existing files with Read tool before overwriting them 2. Prefer Edit tool for making changes to existing files - it's safer and more precise @@ -41,7 +41,7 @@ content: "//! New module\n\npub fn hello() {\n println!(\"Hello!\");\n}\n" 4. Don't create files proactively - wait for explicit user requests 5. Use absolute paths only - relative paths will be rejected -## Error Handling +### Error Handling - If you try to overwrite a file you haven't read, the operation will fail - Permission errors will be returned if you can't write to the location diff --git a/src/llm-coding-tools-core/src/context/write_allowed.txt b/src/llm-coding-tools-core/src/context/write_allowed.txt index 41d9b5c3..a9fccefe 100644 --- a/src/llm-coding-tools-core/src/context/write_allowed.txt +++ b/src/llm-coding-tools-core/src/context/write_allowed.txt @@ -9,25 +9,25 @@ Usage: - NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User. - Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked. -## Parameters +### Parameters - `file_path`: Path for the file to write - can be relative or absolute within allowed directories (required) - `content`: Content to write to the file (required) -## When to Use This Tool +### When to Use This Tool - Creating new files that don't exist yet - Completely rewriting a file when most content changes - Writing generated output (build artifacts, reports, etc.) - Creating new source files when explicitly requested -## When NOT to Use This Tool +### When NOT to Use This Tool - Modifying existing files - use Edit tool instead (more precise, less error-prone) - Creating documentation unless explicitly requested - Writing files you haven't read first (if they exist) -## Examples +### Examples Creating a new file: ``` @@ -35,7 +35,7 @@ file_path: "src/new_module.rs" content: "//! New module\n\npub fn hello() {\n println!(\"Hello!\");\n}\n" ``` -## Best Practices +### Best Practices 1. ALWAYS read existing files with Read tool before overwriting them 2. Prefer Edit tool for making changes to existing files - it's safer and more precise @@ -43,7 +43,7 @@ content: "//! New module\n\npub fn hello() {\n println!(\"Hello!\");\n}\n" 4. Don't create files proactively - wait for explicit user requests 5. Relative paths are resolved against allowed directories -## Error Handling +### Error Handling - If you try to overwrite a file you haven't read, the operation will fail - Paths outside allowed directories will be rejected diff --git a/src/llm-coding-tools-core/src/preamble.rs b/src/llm-coding-tools-core/src/preamble.rs index 37a5573d..8698b596 100644 --- a/src/llm-coding-tools-core/src/preamble.rs +++ b/src/llm-coding-tools-core/src/preamble.rs @@ -4,6 +4,7 @@ //! preambles containing tool usage context. use crate::context::ToolContext; +use crate::path::AllowedPathResolver; /// Entry storing tool name and context string. struct ContextEntry { @@ -13,10 +14,7 @@ struct ContextEntry { /// Builder that tracks tools and generates formatted preambles. /// -/// # Generic Parameters -/// -/// - `ENV`: When `true`, includes an environment section with working directory -/// before tool listings. Defaults to `false` for backwards compatibility. +/// The environment section is always included and appears before tool listings. /// /// # Example /// @@ -34,12 +32,7 @@ struct ContextEntry { /// } /// } /// -/// // Without environment section (default) -/// let mut pb = PreambleBuilder::::new(); -/// let _preamble = pb.build(); -/// -/// // With environment section -/// let mut pb = PreambleBuilder::::new() +/// let mut pb = PreambleBuilder::new() /// .working_directory(std::env::current_dir().unwrap().display().to_string()); /// /// pb.track(ReadTool); @@ -52,37 +45,27 @@ struct ContextEntry { /// The generated preamble is Markdown. For example, with two tools: /// /// ```text -/// # Tool Usage Guidelines -/// ## Read Tool -/// Reads files from disk. -/// ## Bash Tool -/// Executes shell commands. -/// ``` -/// -/// When the environment section is enabled and a working directory is provided: -/// -/// ```text /// # Environment +/// /// Working directory: /home/user/project +/// /// # Tool Usage Guidelines -/// ## Read Tool +/// +/// ## `Read` Tool /// Reads files from disk. +/// ## `Bash` Tool +/// Executes shell commands. /// ``` -pub struct PreambleBuilder { +#[derive(Default)] +pub struct PreambleBuilder { entries: Vec, working_directory: Option, + allowed_paths: Option>, + supplemental: Vec<(&'static str, &'static str)>, + system_prompt: Option, } -impl Default for PreambleBuilder { - fn default() -> Self { - Self { - entries: Vec::new(), - working_directory: None, - } - } -} - -impl PreambleBuilder { +impl PreambleBuilder { /// Creates a new preamble builder. #[inline] pub fn new() -> Self { @@ -106,7 +89,7 @@ impl PreambleBuilder { /// } /// } /// - /// let mut pb = PreambleBuilder::::new(); + /// let mut pb = PreambleBuilder::new(); /// let _my_tool = pb.track(MyTool); /// // register _my_tool with your tool collection /// ``` @@ -127,9 +110,78 @@ impl PreambleBuilder { }); tool } + + /// Adds supplemental context to the preamble. + /// + /// Supplemental context appears in a separate "Supplemental Context" section + /// after tool usage guidelines. Use this for guidance that isn't inherent + /// to a specific tool, such as git workflows or GitHub CLI patterns. + /// + /// # Arguments + /// + /// * `name` - Section header (e.g., "Git Workflow", "GitHub CLI") + /// * `context` - Context string content (e.g., [`GIT_WORKFLOW`](crate::context::GIT_WORKFLOW)) + /// + /// # Examples + /// + /// Adding both git and GitHub CLI context: + /// + /// ```rust + /// use llm_coding_tools_core::{PreambleBuilder, context}; + /// + /// let pb = PreambleBuilder::new() + /// .add_context("Git Workflow", context::GIT_WORKFLOW) + /// .add_context("GitHub CLI", context::GITHUB_CLI); + /// + /// let preamble = pb.build(); + /// assert!(preamble.contains("# Supplemental Context")); + /// assert!(preamble.contains("## Git Workflow")); + /// ``` + /// + /// Selective inclusion - adding only Git Workflow when not using GitHub features: + /// + /// ```rust + /// use llm_coding_tools_core::{PreambleBuilder, context}; + /// + /// // Only include git workflow for agents that use git but not GitHub + /// let pb = PreambleBuilder::new() + /// .add_context("Git Workflow", context::GIT_WORKFLOW); + /// + /// let preamble = pb.build(); + /// assert!(preamble.contains("## Git Workflow")); + /// assert!(!preamble.contains("## GitHub CLI")); + /// ``` + #[inline] + pub fn add_context(mut self, name: &'static str, context: &'static str) -> Self { + self.supplemental.push((name, context)); + self + } + + /// Sets a custom system prompt that appears first in the generated preamble. + /// + /// The provided prompt is prepended before all other sections (environment, + /// tools, supplemental context). User provides exactly what they want, + /// including any markdown headers - no auto-modification is applied. + /// + /// # Example + /// + /// ```rust + /// use llm_coding_tools_core::PreambleBuilder; + /// + /// let pb = PreambleBuilder::new() + /// .system_prompt("# System Instructions\n\nYou are a helpful assistant."); + /// + /// let preamble = pb.build(); + /// assert!(preamble.starts_with("# System Instructions")); + /// ``` + #[inline] + pub fn system_prompt(mut self, prompt: impl Into) -> Self { + self.system_prompt = Some(prompt.into()); + self + } } -impl PreambleBuilder { +impl PreambleBuilder { /// Sets the working directory to display in the environment section. /// /// Accepts any type that can be converted to String, including: @@ -137,18 +189,16 @@ impl PreambleBuilder { /// - `String` /// - `PathBuf` or `&Path` (via `.display().to_string()`) /// - /// Only available when environment section is enabled (`PreambleBuilder`). - /// /// # Example /// /// ```no_run /// use llm_coding_tools_core::PreambleBuilder; /// - /// let _pb = PreambleBuilder::::new() + /// let _pb = PreambleBuilder::new() /// .working_directory("/home/user/project"); /// /// // With runtime-computed path - /// let _pb = PreambleBuilder::::new() + /// let _pb = PreambleBuilder::new() /// .working_directory(std::env::current_dir().unwrap().display().to_string()); /// ``` #[inline] @@ -156,53 +206,79 @@ impl PreambleBuilder { self.working_directory = Some(path.into()); self } -} -impl PreambleBuilder { - /// Generates the preamble string without environment section. - pub fn build(self) -> String { - if self.entries.is_empty() { - return String::new(); - } - - let tools_size: usize = self - .entries - .iter() - .map(|e| e.context.len() + e.name.len() + 20) - .sum(); - - let mut output = String::with_capacity(tools_size + 30); - - output.push_str("# Tool Usage Guidelines\n"); - - for entry in self.entries { - output.push_str("## "); - let mut chars = entry.name.chars(); - if let Some(first) = chars.next() { - output.push(first.to_ascii_uppercase()); - output.push_str(chars.as_str()); - } - output.push_str(" Tool\n"); - output.push_str(entry.context); - output.push('\n'); - } + /// Sets the allowed directories to display in the environment section. + /// + /// Takes an [`AllowedPathResolver`] reference and extracts its allowed paths + /// for display. Paths are already canonicalized (absolute, symlinks resolved) + /// by the resolver during construction. + /// + /// # Example + /// + /// ```no_run + /// use llm_coding_tools_core::{AllowedPathResolver, PreambleBuilder}; + /// + /// let resolver = AllowedPathResolver::new(vec!["/home/user/project", "/tmp"]).unwrap(); + /// let _pb = PreambleBuilder::new() + /// .working_directory("/home/user/project") + /// .allowed_paths(&resolver); + /// ``` + #[inline] + pub fn allowed_paths(mut self, resolver: &AllowedPathResolver) -> Self { + // AllowedPathResolver::allowed_paths() returns &[PathBuf] where paths + // are already canonicalized (absolute, symlinks resolved) during + // AllowedPathResolver::new() construction. + self.allowed_paths = Some( + resolver + .allowed_paths() + .iter() + .map(|p| p.display().to_string()) + .collect(), + ); + self + } +} - output.truncate(output.trim_end().len()); - output +/// Returns the separator needed to ensure exactly `\n\n` between content and next section. +/// +/// Given a string, determines how many newlines to append so that the result +/// ends with exactly `\n\n` (one blank line). Does not modify the user's content. +#[inline] +fn section_separator(s: &str) -> &'static str { + if s.ends_with("\n\n") { + "" + } else if s.ends_with('\n') { + "\n" + } else { + "\n\n" } } -impl PreambleBuilder { +impl PreambleBuilder { /// Generates the preamble string with environment section. pub fn build(self) -> String { // Environment section size: ~50 bytes header + path length // "# Environment\n\nWorking directory: \n\n" = ~38 bytes const ENV_HEADER_SIZE: usize = 50; - - let env_size = self - .working_directory - .as_ref() - .map_or(0, |d| d.len() + ENV_HEADER_SIZE); + // "Allowed directories:\n- " per path + path length + const ALLOWED_DIR_PER_ITEM: usize = 25; + + let system_prompt_size = self.system_prompt.as_ref().map_or(0, |p| p.len() + 2); + + let env_size = if self.working_directory.is_some() || self.allowed_paths.is_some() { + ENV_HEADER_SIZE + self.working_directory.as_ref().map_or(0, |d| d.len()) + } else if self.system_prompt.is_some() + || !self.entries.is_empty() + || !self.supplemental.is_empty() + { + ENV_HEADER_SIZE + } else { + 0 + }; + + let allowed_size = self.allowed_paths.as_ref().map_or(0, |paths| { + paths.iter().map(|p| p.len() + ALLOWED_DIR_PER_ITEM).sum() + }); let tools_size: usize = self .entries @@ -210,39 +286,93 @@ impl PreambleBuilder { .map(|e| e.context.len() + e.name.len() + 20) .sum(); + let supplemental_size: usize = self + .supplemental + .iter() + .map(|(n, c)| c.len() + n.len() + 20) + .sum(); + let has_tools = !self.entries.is_empty(); - let has_env = self.working_directory.is_some(); + let has_supplemental = !self.supplemental.is_empty(); + let has_system_prompt = self.system_prompt.is_some(); + let has_env_content = self.working_directory.is_some() || self.allowed_paths.is_some(); + + let total_size = + system_prompt_size + env_size + allowed_size + tools_size + supplemental_size + 90; + let mut output = String::with_capacity(total_size); // Return empty if nothing to output - if !has_tools && !has_env { + if !has_tools && !has_supplemental && !has_system_prompt && !has_env_content { return String::new(); } - let total_size = env_size + tools_size + if has_tools { 30 } else { 0 }; - let mut output = String::with_capacity(total_size); + // System prompt (first) + if let Some(ref prompt) = self.system_prompt { + output.push_str(prompt); + // Smart separator: ensure exactly one blank line before next section + output.push_str(section_separator(prompt)); + } // Environment section - if let Some(ref dir) = self.working_directory { - output.push_str("# Environment\n"); - output.push_str("Working directory: "); - output.push_str(dir); - output.push('\n'); + if has_env_content || has_system_prompt || has_tools || has_supplemental { + output.push_str("# Environment\n\n"); + + if let Some(ref dir) = self.working_directory { + output.push_str("Working directory: "); + output.push_str(dir); + output.push('\n'); + } + + if let Some(ref paths) = self.allowed_paths { + output.push_str("Allowed directories:\n"); + for path in paths { + output.push_str("- "); + output.push_str(path); + output.push('\n'); + } + } + + if (has_tools || has_supplemental) && has_env_content { + if !output.ends_with('\n') { + output.push('\n'); + } + output.push('\n'); + } } // Tool section if has_tools { - output.push_str("# Tool Usage Guidelines\n"); + output.push_str("# Tool Usage Guidelines\n\n"); for entry in self.entries { - output.push_str("## "); + output.push_str("## `"); let mut chars = entry.name.chars(); if let Some(first) = chars.next() { output.push(first.to_ascii_uppercase()); output.push_str(chars.as_str()); + } else { + output.push_str(entry.name); } - output.push_str(" Tool\n"); + output.push_str("` Tool\n"); output.push_str(entry.context); + if !entry.context.ends_with('\n') { + output.push('\n'); + } + } + } + + // Supplemental context section + if has_supplemental { + output.push_str("\n# Supplemental Context\n"); + + for (name, context) in self.supplemental { + output.push_str("## "); + output.push_str(name); output.push('\n'); + output.push_str(context); + if !context.ends_with('\n') { + output.push('\n'); + } } } @@ -329,13 +459,13 @@ mod tests { #[test] fn empty_builder_returns_empty_string() { - let preamble = PreambleBuilder::::new().build(); + let preamble = PreambleBuilder::new().build(); assert!(preamble.is_empty()); } #[test] fn track_returns_tool_unchanged() { - let mut pb = PreambleBuilder::::new(); + let mut pb = PreambleBuilder::new(); let tool = MockTool { id: 42 }; let returned = pb.track(tool); assert_eq!(returned.id, 42); @@ -343,24 +473,26 @@ mod tests { #[test] fn single_tool_formats_correctly() { - let mut pb = PreambleBuilder::::new(); + let mut pb = PreambleBuilder::new().working_directory("/home/user"); let _ = pb.track(MockTool { id: 1 }); let preamble = pb.build(); + assert!(preamble.contains("# Environment")); + assert!(preamble.contains("Working directory: /home/user")); assert!(preamble.contains("# Tool Usage Guidelines")); - assert!(preamble.contains("## Mock Tool")); + assert!(preamble.contains("## `Mock` Tool")); assert!(preamble.contains("Mock tool context.")); } #[test] fn multiple_tools_preserve_order() { - let mut pb = PreambleBuilder::::new(); + let mut pb = PreambleBuilder::new().working_directory("/home/user"); let _ = pb.track(MockTool { id: 1 }); let _ = pb.track(OtherTool); let preamble = pb.build(); - let mock_pos = preamble.find("## Mock Tool").unwrap(); - let other_pos = preamble.find("## Other Tool").unwrap(); + let mock_pos = preamble.find("## `Mock` Tool").unwrap(); + let other_pos = preamble.find("## `Other` Tool").unwrap(); assert!( mock_pos < other_pos, "Tools should appear in insertion order" @@ -369,28 +501,27 @@ mod tests { #[test] fn multiple_tools_have_single_newline_between() { - let mut pb = PreambleBuilder::::new(); + let mut pb = PreambleBuilder::new().working_directory("/home/user"); let _ = pb.track(MockTool { id: 1 }); let _ = pb.track(OtherTool); let preamble = pb.build(); - // Verify exact transition: context ends, separator adds \n, then next tool header - // Pattern: "Mock tool context.\n## Other Tool" + // Verify exact transition: context ends, then next tool header assert!( - preamble.contains("Mock tool context.\n## Other Tool"), + preamble.contains("Mock tool context.\n## `Other` Tool"), "Expected single newline between tool sections.\nGot:\n{preamble}" ); - // Verify single newline after section header + // Verify single newline after tool header assert!( - preamble.contains("## Mock Tool\nMock tool context."), + preamble.contains("## `Mock` Tool\nMock tool context."), "Expected single newline after tool header.\nGot:\n{preamble}" ); - // Verify no double newlines anywhere + // Verify blank line after section header assert!( - !preamble.contains("\n\n"), - "Found double newline in preamble.\nGot:\n{preamble}" + preamble.contains("# Tool Usage Guidelines\n\n## `Mock` Tool"), + "Expected blank line after section header.\nGot:\n{preamble}" ); // Verify no trailing whitespace at end of preamble @@ -402,29 +533,34 @@ mod tests { } #[test] - fn multiple_tools_with_env_have_single_newline_between() { - let mut pb = PreambleBuilder::::new().working_directory("/test"); + fn multiple_tools_with_working_dir_have_single_newline_between() { + let mut pb = PreambleBuilder::new().working_directory("/test"); let _ = pb.track(MockTool { id: 1 }); let _ = pb.track(OtherTool); let preamble = pb.build(); - // Verify exact transition: context ends, separator adds \n, then next tool header - // Pattern: "Mock tool context.\n## Other Tool" + // Verify exact transition: context ends, then next tool header assert!( - preamble.contains("Mock tool context.\n## Other Tool"), + preamble.contains("Mock tool context.\n## `Other` Tool"), "Expected single newline between tool sections.\nGot:\n{preamble}" ); - // Verify single newline after section header + // Verify single newline after tool header assert!( - preamble.contains("## Mock Tool\nMock tool context."), + preamble.contains("## `Mock` Tool\nMock tool context."), "Expected single newline after tool header.\nGot:\n{preamble}" ); - // Verify no double newlines anywhere + // Verify blank line after Environment header assert!( - !preamble.contains("\n\n"), - "Found double newline in preamble.\nGot:\n{preamble}" + preamble.contains("# Environment\n\nWorking directory:"), + "Expected blank line after Environment header.\nGot:\n{preamble}" + ); + + // Verify blank line after Tool Usage Guidelines header + assert!( + preamble.contains("# Tool Usage Guidelines\n\n## `Mock` Tool"), + "Expected blank line after section header.\nGot:\n{preamble}" ); // Verify no trailing whitespace at end of preamble @@ -436,19 +572,8 @@ mod tests { } #[test] - fn builder_without_env_omits_environment_section() { - let mut pb = PreambleBuilder::::new(); - let _ = pb.track(MockTool { id: 1 }); - let preamble = pb.build(); - - assert!(!preamble.contains("# Environment")); - assert!(!preamble.contains("Working directory")); - assert!(preamble.contains("# Tool Usage Guidelines")); - } - - #[test] - fn builder_with_env_includes_environment_section() { - let mut pb = PreambleBuilder::::new().working_directory("/home/user/project"); + fn builder_includes_environment_section() { + let mut pb = PreambleBuilder::new().working_directory("/home/user/project"); let _ = pb.track(MockTool { id: 1 }); let preamble = pb.build(); @@ -461,16 +586,16 @@ mod tests { } #[test] - fn builder_with_env_no_working_dir_no_tools_returns_empty() { - let pb = PreambleBuilder::::new(); + fn builder_without_env_data_and_tools_returns_empty() { + let pb = PreambleBuilder::new(); let preamble = pb.build(); assert!(preamble.is_empty()); } #[test] - fn builder_with_env_and_working_dir_but_no_tools() { + fn builder_with_working_dir_but_no_tools() { // Environment section should render even without tools tracked - let pb = PreambleBuilder::::new().working_directory("/home/user/project"); + let pb = PreambleBuilder::new().working_directory("/home/user/project"); let preamble = pb.build(); assert!(preamble.contains("# Environment")); @@ -482,7 +607,7 @@ mod tests { fn working_directory_accepts_runtime_string() { // Simulates std::env::current_dir().unwrap().display().to_string() let runtime_path = String::from("/runtime/computed/path"); - let pb = PreambleBuilder::::new().working_directory(runtime_path); + let pb = PreambleBuilder::new().working_directory(runtime_path); let preamble = pb.build(); assert!(preamble.contains("Working directory: /runtime/computed/path")); @@ -490,7 +615,7 @@ mod tests { #[test] fn working_directory_accepts_str() { - let pb = PreambleBuilder::::new().working_directory("/static/path"); + let pb = PreambleBuilder::new().working_directory("/static/path"); let preamble = pb.build(); assert!(preamble.contains("Working directory: /static/path")); @@ -542,24 +667,606 @@ mod tests { } #[test] - fn generic_flag_is_compile_time() { - // This test verifies the generic works at compile time - // If it compiles, the generic system works - let _pb_no_env: PreambleBuilder = PreambleBuilder::new(); - let _pb_with_env: PreambleBuilder = PreambleBuilder::new(); - - // Type inference defaults to false + fn default_builder_compiles() { let _pb_default: PreambleBuilder = PreambleBuilder::new(); } #[test] fn backwards_compatibility_existing_api() { // Existing code should work unchanged - let mut pb = PreambleBuilder::::new(); + let mut pb = PreambleBuilder::new(); let _ = pb.track(MockTool { id: 1 }); let preamble = pb.build(); assert!(preamble.contains("# Tool Usage Guidelines")); - assert!(preamble.contains("## Mock Tool")); + assert!(preamble.contains("## `Mock` Tool")); + } + + #[test] + fn builder_with_allowed_paths_shows_paths() { + use tempfile::TempDir; + + let dir = TempDir::new().unwrap(); + let resolver = AllowedPathResolver::new(vec![dir.path()]).unwrap(); + + let pb = PreambleBuilder::new() + .working_directory("/home/user") + .allowed_paths(&resolver); + let preamble = pb.build(); + + assert!(preamble.contains("# Environment")); + assert!(preamble.contains("Working directory: /home/user")); + assert!(preamble.contains("Allowed directories:")); + // Check that the temp dir path appears (canonicalized) + assert!(preamble.contains(&dir.path().canonicalize().unwrap().display().to_string())); + } + + #[test] + fn builder_with_only_allowed_paths_no_working_dir() { + use tempfile::TempDir; + + let dir = TempDir::new().unwrap(); + let resolver = AllowedPathResolver::new(vec![dir.path()]).unwrap(); + + let pb = PreambleBuilder::new().allowed_paths(&resolver); + let preamble = pb.build(); + + assert!(preamble.contains("# Environment")); + assert!(!preamble.contains("Working directory:")); + assert!(preamble.contains("Allowed directories:")); + } + + #[test] + fn allowed_paths_format_is_bulleted_absolute_paths() { + use std::path::Path; + use tempfile::TempDir; + + let dir1 = TempDir::new().unwrap(); + let dir2 = TempDir::new().unwrap(); + let resolver = AllowedPathResolver::new(vec![dir1.path(), dir2.path()]).unwrap(); + + let pb = PreambleBuilder::new().allowed_paths(&resolver); + let preamble = pb.build(); + + // Check format: "- " (cross-platform) + let lines: Vec<&str> = preamble.lines().collect(); + let allowed_idx = lines + .iter() + .position(|l| l.contains("Allowed directories")) + .unwrap(); + + for i in 1..=2 { + let line = lines[allowed_idx + i]; + assert!( + line.starts_with("- "), + "Line should start with '- ': {}", + line + ); + let path_str = line.strip_prefix("- ").unwrap(); + assert!( + Path::new(path_str).is_absolute(), + "Path should be absolute: {}", + path_str + ); + } + } + + #[test] + fn allowed_paths_appears_after_working_directory() { + use tempfile::TempDir; + + let dir = TempDir::new().unwrap(); + let resolver = AllowedPathResolver::new(vec![dir.path()]).unwrap(); + + let pb = PreambleBuilder::new() + .working_directory("/home/user") + .allowed_paths(&resolver); + let preamble = pb.build(); + + let working_dir_pos = preamble.find("Working directory:").unwrap(); + let allowed_pos = preamble.find("Allowed directories:").unwrap(); + assert!( + working_dir_pos < allowed_pos, + "Working directory should appear before allowed paths" + ); + } + + #[test] + fn builder_with_only_working_dir_no_allowed_paths() { + // Only working_directory() should not render "Allowed directories:" section + let pb = PreambleBuilder::new().working_directory("/home/user/project"); + let preamble = pb.build(); + + assert!(preamble.contains("# Environment")); + assert!(preamble.contains("Working directory: /home/user/project")); + assert!( + !preamble.contains("Allowed directories:"), + "Should not render Allowed directories when not explicitly set" + ); + } + + #[test] + fn add_context_includes_supplemental_section() { + let pb = PreambleBuilder::new() + .working_directory("/home/user") + .add_context("Git Workflow", "Git guidance content."); + + let preamble = pb.build(); + + assert!(preamble.contains("# Supplemental Context")); + assert!(preamble.contains("## Git Workflow")); + assert!(preamble.contains("Git guidance content.")); + } + + #[test] + fn add_context_appears_after_tools() { + let mut pb = PreambleBuilder::new().add_context("Git Workflow", "Git guidance."); + let _ = pb.track(MockTool { id: 1 }); + + let preamble = pb.build(); + + let tools_pos = preamble.find("# Tool Usage Guidelines").unwrap(); + let supplemental_pos = preamble.find("# Supplemental Context").unwrap(); + assert!( + tools_pos < supplemental_pos, + "Tools should appear before supplemental context" + ); + } + + #[test] + fn add_context_multiple_sections_preserve_order() { + let pb = PreambleBuilder::new() + .working_directory("/home/user") + .add_context("Git Workflow", "Git content.") + .add_context("GitHub CLI", "GitHub content."); + + let preamble = pb.build(); + + let git_pos = preamble.find("## Git Workflow").unwrap(); + let github_pos = preamble.find("## GitHub CLI").unwrap(); + assert!( + git_pos < github_pos, + "Contexts should appear in insertion order" + ); + } + + #[test] + fn add_context_only_no_tools() { + let pb = PreambleBuilder::new() + .working_directory("/home/user") + .add_context("Git Workflow", "Git guidance."); + + let preamble = pb.build(); + + assert!(!preamble.contains("# Tool Usage Guidelines")); + assert!(preamble.contains("# Supplemental Context")); + assert!(preamble.contains("## Git Workflow")); + } + + #[test] + fn add_context_with_env_section() { + let pb = PreambleBuilder::new() + .working_directory("/home/user") + .add_context("Git Workflow", "Git guidance."); + + let preamble = pb.build(); + + let env_pos = preamble.find("# Environment").unwrap(); + let supplemental_pos = preamble.find("# Supplemental Context").unwrap(); + assert!(env_pos < supplemental_pos); + } + + #[test] + fn add_context_with_env_and_tools() { + let mut pb = PreambleBuilder::new() + .working_directory("/home/user") + .add_context("Git Workflow", "Git guidance."); + let _ = pb.track(MockTool { id: 1 }); + + let preamble = pb.build(); + + let env_pos = preamble.find("# Environment").unwrap(); + let tools_pos = preamble.find("# Tool Usage Guidelines").unwrap(); + let supplemental_pos = preamble.find("# Supplemental Context").unwrap(); + + assert!(env_pos < tools_pos); + assert!(tools_pos < supplemental_pos); + } + + #[test] + fn add_context_no_triple_newlines() { + let mut pb = PreambleBuilder::new() + .working_directory("/home/user") + .add_context("Git Workflow", "Git guidance.\n"); + let _ = pb.track(MockTool { id: 1 }); + + let preamble = pb.build(); + + assert!( + !preamble.contains("\n\n\n"), + "Found triple newline in preamble.\nGot:\n{preamble}" + ); + } + + #[test] + fn add_context_chains_fluently() { + // Verify fluent chaining works + let pb = PreambleBuilder::new() + .add_context("A", "a") + .add_context("B", "b") + .add_context("C", "c"); + + let preamble = pb.build(); + + assert!(preamble.contains("## A")); + assert!(preamble.contains("## B")); + assert!(preamble.contains("## C")); + } + + #[test] + fn add_context_with_actual_git_workflow_constant() { + use crate::context::GIT_WORKFLOW; + + let pb = PreambleBuilder::new() + .working_directory("/home/user") + .add_context("Git Workflow", GIT_WORKFLOW); + + let preamble = pb.build(); + + assert!(preamble.contains("# Supplemental Context")); + assert!(preamble.contains("## Git Workflow")); + // Verify actual content from git_workflow.txt is included + assert!( + preamble.contains("Only create commits when requested"), + "Should contain git commit workflow content" + ); + assert!( + preamble.contains("Git Safety Protocol"), + "Should contain safety protocol section" + ); + } + + #[test] + fn add_context_with_actual_github_cli_constant() { + use crate::context::GITHUB_CLI; + + let pb = PreambleBuilder::new() + .working_directory("/home/user") + .add_context("GitHub CLI", GITHUB_CLI); + + let preamble = pb.build(); + + assert!(preamble.contains("# Supplemental Context")); + assert!(preamble.contains("## GitHub CLI")); + // Verify actual content from github_cli.txt is included + assert!( + preamble.contains("gh pr create"), + "Should contain gh pr create example" + ); + } + + #[test] + fn add_context_selective_inclusion_git_only() { + use crate::context::{GITHUB_CLI, GIT_WORKFLOW}; + + // Only include git workflow (not GitHub CLI) + let pb = PreambleBuilder::new() + .working_directory("/home/user") + .add_context("Git Workflow", GIT_WORKFLOW); + + let preamble = pb.build(); + + assert!(preamble.contains("## Git Workflow")); + assert!(!preamble.contains("## GitHub CLI")); + assert!(!preamble.contains(GITHUB_CLI)); + } + + #[test] + fn add_context_both_git_and_github() { + use crate::context::{GITHUB_CLI, GIT_WORKFLOW}; + + let pb = PreambleBuilder::new() + .working_directory("/home/user") + .add_context("Git Workflow", GIT_WORKFLOW) + .add_context("GitHub CLI", GITHUB_CLI); + + let preamble = pb.build(); + + assert!(preamble.contains("## Git Workflow")); + assert!(preamble.contains("## GitHub CLI")); + // Verify order preserved + let git_pos = preamble.find("## Git Workflow").unwrap(); + let github_pos = preamble.find("## GitHub CLI").unwrap(); + assert!( + git_pos < github_pos, + "Git Workflow should appear before GitHub CLI" + ); + } + + #[test] + fn system_prompt_appears_first() { + let pb = PreambleBuilder::new() + .system_prompt("# System Instructions\n\nYou are a helpful assistant.") + .working_directory("/home/user"); + + let preamble = pb.build(); + + assert!( + preamble.starts_with("# System Instructions"), + "System prompt should appear first.\nGot:\n{preamble}" + ); + + let system_pos = preamble.find("# System Instructions").unwrap(); + let env_pos = preamble.find("# Environment").unwrap(); + assert!( + system_pos < env_pos, + "System prompt should appear before environment section" + ); + } + + #[test] + fn system_prompt_appears_before_tools() { + let mut pb = + PreambleBuilder::new().system_prompt("# Custom Header\n\nMy custom instructions."); + let _ = pb.track(MockTool { id: 1 }); + + let preamble = pb.build(); + + let system_pos = preamble.find("# Custom Header").unwrap(); + let tools_pos = preamble.find("# Tool Usage Guidelines").unwrap(); + assert!( + system_pos < tools_pos, + "System prompt should appear before tools section" + ); + } + + #[test] + fn system_prompt_no_modification() { + // User provides exact content, no auto-header added + let custom = "My custom content without header"; + let pb = PreambleBuilder::new().system_prompt(custom); + + let preamble = pb.build(); + + assert!( + preamble.starts_with("My custom content without header"), + "System prompt should not be modified.\nGot:\n{preamble}" + ); + } + + #[test] + fn system_prompt_optional_default_behavior() { + // Without system_prompt, existing behavior preserved + let mut pb = PreambleBuilder::new(); + let _ = pb.track(MockTool { id: 1 }); + + let preamble = pb.build(); + + assert!( + preamble.starts_with("# Environment"), + "Without system prompt, should start with Environment.\nGot:\n{preamble}" + ); + } + + #[test] + fn system_prompt_only_produces_output() { + let pb = PreambleBuilder::new() + .system_prompt("# Just Instructions\n\nOnly system prompt, no tools."); + + let preamble = pb.build(); + + assert!(!preamble.is_empty()); + assert!(preamble.contains("# Just Instructions")); + assert!(!preamble.contains("# Tool Usage Guidelines")); + } + + #[test] + fn system_prompt_with_env_and_tools_and_supplemental() { + let mut pb = PreambleBuilder::new() + .system_prompt("# System\n\nInstructions.") + .working_directory("/home/user") + .add_context("Git Workflow", "Git guidance."); + let _ = pb.track(MockTool { id: 1 }); + + let preamble = pb.build(); + + let system_pos = preamble.find("# System").unwrap(); + let env_pos = preamble.find("# Environment").unwrap(); + let tools_pos = preamble.find("# Tool Usage Guidelines").unwrap(); + let supplemental_pos = preamble.find("# Supplemental Context").unwrap(); + + assert!(system_pos < env_pos); + assert!(env_pos < tools_pos); + assert!(tools_pos < supplemental_pos); + } + + #[test] + fn system_prompt_no_trailing_newline_gets_separator() { + // System prompt without trailing newline should get "\n\n" separator + let mut pb = PreambleBuilder::new().system_prompt("# System\n\nNo trailing newline"); + let _ = pb.track(MockTool { id: 1 }); + + let preamble = pb.build(); + + // Should have exactly one blank line between system prompt and environment + assert!( + preamble.contains("No trailing newline\n\n# Environment"), + "Expected one blank line after system prompt.\nGot:\n{preamble}" + ); + assert!( + !preamble.contains("\n\n\n"), + "Found triple newline in preamble.\nGot:\n{preamble}" + ); + } + + #[test] + fn system_prompt_single_trailing_newline_gets_one_more() { + // System prompt ending with \n should get "\n" to make "\n\n" + let mut pb = PreambleBuilder::new().system_prompt("# System\n\nEnds with single newline\n"); + let _ = pb.track(MockTool { id: 1 }); + + let preamble = pb.build(); + + // Should have exactly one blank line between system prompt and environment + assert!( + preamble.contains("Ends with single newline\n\n# Environment"), + "Expected one blank line after system prompt.\nGot:\n{preamble}" + ); + assert!( + !preamble.contains("\n\n\n"), + "Found triple newline in preamble.\nGot:\n{preamble}" + ); + } + + #[test] + fn system_prompt_double_trailing_newline_no_extra() { + // System prompt ending with \n\n should get no extra separator + let mut pb = + PreambleBuilder::new().system_prompt("# System\n\nEnds with double newline\n\n"); + let _ = pb.track(MockTool { id: 1 }); + + let preamble = pb.build(); + + // Should have exactly one blank line between system prompt and environment + assert!( + preamble.contains("Ends with double newline\n\n# Environment"), + "Expected one blank line after system prompt.\nGot:\n{preamble}" + ); + assert!( + !preamble.contains("\n\n\n"), + "Found triple newline in preamble.\nGot:\n{preamble}" + ); + } + + #[test] + fn system_prompt_trailing_newlines_with_environment() { + let pb = PreambleBuilder::new() + .system_prompt("# System\n\nEnds with single newline\n") + .working_directory("/home/user"); + + let preamble = pb.build(); + + assert!( + preamble.contains("Ends with single newline\n\n# Environment"), + "Expected one blank line after system prompt.\nGot:\n{preamble}" + ); + assert!( + !preamble.contains("\n\n\n"), + "Found triple newline in preamble.\nGot:\n{preamble}" + ); + } + + #[test] + fn system_prompt_chains_fluently() { + // Verify fluent chaining with other methods + let pb = PreambleBuilder::new() + .system_prompt("# System\n\nContent.") + .working_directory("/home/user") + .add_context("A", "a"); + + let preamble = pb.build(); + + assert!(preamble.contains("# System")); + assert!(preamble.contains("# Environment")); + assert!(preamble.contains("# Supplemental Context")); + } + + #[test] + fn section_separator_returns_correct_suffix() { + // Direct unit test for section_separator helper + assert_eq!(section_separator("no newline"), "\n\n"); + assert_eq!(section_separator("single newline\n"), "\n"); + assert_eq!(section_separator("double newline\n\n"), ""); + assert_eq!(section_separator("triple newline\n\n\n"), ""); + assert_eq!(section_separator(""), "\n\n"); + } + + #[test] + fn preamble_preview_structure_has_correct_section_order() { + // Mirrors the example binary to verify structure + let resolver = AllowedPathResolver::from_canonical(["/home/user/project", "/tmp"]); + + let mut pb = PreambleBuilder::new() + .system_prompt("# System Instructions\n\nYou are helpful.") + .working_directory("/home/user/project") + .allowed_paths(&resolver) + .add_context("Git Workflow", "Git guidance content.") + .add_context("GitHub CLI", "GitHub guidance content."); + + let _ = pb.track(MockTool { id: 1 }); + let _ = pb.track(OtherTool); + + let preamble = pb.build(); + + // Verify all sections present + assert!( + preamble.contains("# System Instructions"), + "Missing system prompt" + ); + assert!( + preamble.contains("# Environment"), + "Missing environment section" + ); + assert!( + preamble.contains("Working directory:"), + "Missing working directory" + ); + assert!( + preamble.contains("Allowed directories:"), + "Missing allowed directories" + ); + assert!( + preamble.contains("# Tool Usage Guidelines"), + "Missing tools section" + ); + assert!( + preamble.contains("# Supplemental Context"), + "Missing supplemental section" + ); + + // Verify section order: system -> env -> tools -> supplemental + let system_pos = preamble.find("# System Instructions").unwrap(); + let env_pos = preamble.find("# Environment").unwrap(); + let tools_pos = preamble.find("# Tool Usage Guidelines").unwrap(); + let supplemental_pos = preamble.find("# Supplemental Context").unwrap(); + + assert!( + system_pos < env_pos, + "System prompt should come before environment" + ); + assert!(env_pos < tools_pos, "Environment should come before tools"); + assert!( + tools_pos < supplemental_pos, + "Tools should come before supplemental" + ); + + // Verify no formatting issues + assert!( + !preamble.contains("\n\n\n"), + "Found triple newline (double blank line)" + ); + assert_eq!( + preamble, + preamble.trim_end(), + "Preamble has trailing whitespace" + ); + } + + #[test] + fn preamble_preview_allowed_paths_rendered_correctly() { + let resolver = AllowedPathResolver::from_canonical(["/home/user/project", "/tmp"]); + + let pb = PreambleBuilder::new() + .working_directory("/home/user/project") + .allowed_paths(&resolver); + + let preamble = pb.build(); + + // Verify both paths appear as bullet points + assert!( + preamble.contains("- /home/user/project"), + "Missing project path" + ); + assert!(preamble.contains("- /tmp"), "Missing tmp path"); } } diff --git a/src/llm-coding-tools-rig/README.md b/src/llm-coding-tools-rig/README.md index 917efe3a..1e89c447 100644 --- a/src/llm-coding-tools-rig/README.md +++ b/src/llm-coding-tools-rig/README.md @@ -39,7 +39,7 @@ use rig::completion::Prompt; #[tokio::main] async fn main() -> Result<(), Box> { let todos = TodoTools::new(); - let mut pb = PreambleBuilder::::new(); + let mut pb = PreambleBuilder::new(); // Build agent with preamble tracking let client = openai::Client::from_env(); @@ -66,14 +66,15 @@ async fn main() -> Result<(), Box> { Example preamble output (truncated): ```text -# Tool Usage Guidelines - -## Read Tool +# Environment -Reads files from disk. +Working directory: /home/user/project -## Bash Tool +# Tool Usage Guidelines +## `Read` Tool +Reads files from disk. +## `Bash` Tool Executes shell commands. ``` @@ -89,12 +90,12 @@ use std::path::PathBuf; let read = ReadTool::::new(); let resolver = AllowedPathResolver::new([PathBuf::from("/home/user/project")]).unwrap(); -let sandboxed_read: AllowedReadTool = AllowedReadTool::with_resolver(resolver.clone()); -let sandboxed_write = AllowedWriteTool::with_resolver(resolver); +let sandboxed_read: AllowedReadTool = AllowedReadTool::new(resolver.clone()); +let sandboxed_write = AllowedWriteTool::new(resolver); ``` Other tools: `BashTool`, `WebFetchTool`, `TaskTool`, `TodoTools`. -Use `PreambleBuilder` to register tools and pass `pb.build()` to `.preamble()`. +Use `PreambleBuilder` to register tools and pass `pb.build()` to `.preamble()`. Set `working_directory()` so the environment section is populated. Context strings are re-exported in `llm_coding_tools_rig::context` (e.g., `BASH`, `READ_ABSOLUTE`). ## Examples diff --git a/src/llm-coding-tools-rig/examples/rig-basic.rs b/src/llm-coding-tools-rig/examples/rig-basic.rs index f553768d..bcedd7b1 100644 --- a/src/llm-coding-tools-rig/examples/rig-basic.rs +++ b/src/llm-coding-tools-rig/examples/rig-basic.rs @@ -20,7 +20,7 @@ async fn main() -> Result<(), Box> { let todos = TodoTools::new(); // === Create preamble builder to track tools === - let mut pb = PreambleBuilder::::new(); + let mut pb = PreambleBuilder::new().working_directory(std::env::current_dir()?.to_string()); // === Build agent with chained .tool() calls === let client = openai::Client::from_env(); diff --git a/src/llm-coding-tools-rig/examples/rig-sandboxed.rs b/src/llm-coding-tools-rig/examples/rig-sandboxed.rs index 7ac370a0..03d20615 100644 --- a/src/llm-coding-tools-rig/examples/rig-sandboxed.rs +++ b/src/llm-coding-tools-rig/examples/rig-sandboxed.rs @@ -36,14 +36,21 @@ async fn main() -> Result<(), Box> { // More efficient and ensures consistency. let resolver = AllowedPathResolver::new(allowed_paths)?; - let read: ReadTool = ReadTool::with_resolver(resolver.clone()); - let write = WriteTool::with_resolver(resolver.clone()); - let edit = EditTool::with_resolver(resolver.clone()); - let glob = GlobTool::with_resolver(resolver.clone()); - let grep: GrepTool = GrepTool::with_resolver(resolver); + let read: ReadTool = ReadTool::new(resolver.clone()); + let write = WriteTool::new(resolver.clone()); + let edit = EditTool::new(resolver.clone()); + let glob = GlobTool::new(resolver.clone()); + let grep: GrepTool = GrepTool::new(resolver.clone()); // === Build agent with sandboxed tools === - let mut pb = PreambleBuilder::::new(); + // + // Use PreambleBuilder with fluent chaining: + // - working_directory() and allowed_paths() consume self (chaining) + // - track() takes &mut self (passthrough for agent builder) + let mut pb = PreambleBuilder::new() + .working_directory(std::env::current_dir()?.to_string()) + .allowed_paths(&resolver); + let client = openai::Client::from_env(); let agent = client .agent("gpt-4o") diff --git a/src/llm-coding-tools-rig/src/allowed/edit.rs b/src/llm-coding-tools-rig/src/allowed/edit.rs index b06b9ff5..ccf2f647 100644 --- a/src/llm-coding-tools-rig/src/allowed/edit.rs +++ b/src/llm-coding-tools-rig/src/allowed/edit.rs @@ -4,12 +4,11 @@ use llm_coding_tools_core::operations::edit_file; use llm_coding_tools_core::path::AllowedPathResolver; use llm_coding_tools_core::tool_names; pub use llm_coding_tools_core::EditError; -use llm_coding_tools_core::{ToolContext, ToolResult}; +use llm_coding_tools_core::ToolContext; use rig::completion::ToolDefinition; use rig::tool::Tool; use schemars::{schema_for, JsonSchema}; use serde::Deserialize; -use std::path::Path; /// Arguments for file editing. #[derive(Debug, Clone, Deserialize, JsonSchema)] @@ -32,15 +31,10 @@ pub struct EditTool { } impl EditTool { - /// Creates a new edit tool restricted to the given directories. - pub fn new(allowed_paths: impl IntoIterator>) -> ToolResult { - Ok(Self { - resolver: AllowedPathResolver::new(allowed_paths)?, - }) - } - - /// Creates a new edit tool with an existing resolver. - pub fn with_resolver(resolver: AllowedPathResolver) -> Self { + /// Creates a new edit tool with a shared resolver. + /// + /// See [`ReadTool::new`](crate::allowed::read::ReadTool::new) for usage example. + pub fn new(resolver: AllowedPathResolver) -> Self { Self { resolver } } } @@ -94,7 +88,8 @@ mod tests { let dir = TempDir::new().unwrap(); std::fs::write(dir.path().join("test.txt"), "hello world").unwrap(); - let tool = EditTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool = EditTool::new(resolver); let result = tool .call(EditArgs { file_path: "test.txt".to_string(), @@ -110,7 +105,8 @@ mod tests { #[tokio::test] async fn rejects_path_traversal() { let dir = TempDir::new().unwrap(); - let tool = EditTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool = EditTool::new(resolver); let result = tool .call(EditArgs { file_path: "../../../etc/passwd".to_string(), diff --git a/src/llm-coding-tools-rig/src/allowed/glob.rs b/src/llm-coding-tools-rig/src/allowed/glob.rs index 2e477bd7..f91d0acc 100644 --- a/src/llm-coding-tools-rig/src/allowed/glob.rs +++ b/src/llm-coding-tools-rig/src/allowed/glob.rs @@ -3,12 +3,11 @@ use llm_coding_tools_core::operations::glob_files; use llm_coding_tools_core::path::AllowedPathResolver; use llm_coding_tools_core::tool_names; -use llm_coding_tools_core::{GlobOutput, ToolContext, ToolError, ToolResult}; +use llm_coding_tools_core::{GlobOutput, ToolContext, ToolError}; use rig::completion::ToolDefinition; use rig::tool::Tool; use schemars::{schema_for, JsonSchema}; use serde::Deserialize; -use std::path::Path; /// Arguments for the glob tool. #[derive(Debug, Deserialize, JsonSchema)] @@ -26,15 +25,10 @@ pub struct GlobTool { } impl GlobTool { - /// Creates a new glob tool restricted to the given directories. - pub fn new(allowed_paths: impl IntoIterator>) -> ToolResult { - Ok(Self { - resolver: AllowedPathResolver::new(allowed_paths)?, - }) - } - - /// Creates a new glob tool with an existing resolver. - pub fn with_resolver(resolver: AllowedPathResolver) -> Self { + /// Creates a new glob tool with a shared resolver. + /// + /// See [`ReadTool::new`](crate::allowed::read::ReadTool::new) for usage example. + pub fn new(resolver: AllowedPathResolver) -> Self { Self { resolver } } } @@ -82,7 +76,8 @@ mod tests { fs::create_dir_all(dir.path().join("src")).unwrap(); File::create(dir.path().join("src/lib.rs")).unwrap(); - let tool = GlobTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool = GlobTool::new(resolver); let result = tool .call(GlobArgs { pattern: "**/*.rs".to_string(), @@ -96,7 +91,8 @@ mod tests { #[tokio::test] async fn rejects_path_traversal() { let dir = TempDir::new().unwrap(); - let tool = GlobTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool = GlobTool::new(resolver); let result = tool .call(GlobArgs { pattern: "*.rs".to_string(), diff --git a/src/llm-coding-tools-rig/src/allowed/grep.rs b/src/llm-coding-tools-rig/src/allowed/grep.rs index 74f73a2a..dc146290 100644 --- a/src/llm-coding-tools-rig/src/allowed/grep.rs +++ b/src/llm-coding-tools-rig/src/allowed/grep.rs @@ -3,12 +3,11 @@ use llm_coding_tools_core::operations::{grep_search, DEFAULT_MAX_LINE_LENGTH}; use llm_coding_tools_core::path::AllowedPathResolver; use llm_coding_tools_core::tool_names; -use llm_coding_tools_core::{ToolContext, ToolError, ToolOutput, ToolResult}; +use llm_coding_tools_core::{ToolContext, ToolError, ToolOutput}; use rig::completion::ToolDefinition; use rig::tool::Tool; use schemars::{schema_for, JsonSchema}; use serde::Deserialize; -use std::path::Path; const DEFAULT_LIMIT: usize = 100; const MAX_LIMIT: usize = 2000; @@ -39,15 +38,12 @@ pub struct GrepTool { } impl GrepTool { - /// Creates a new grep tool restricted to the given directories. - pub fn new(allowed_paths: impl IntoIterator>) -> ToolResult { - Ok(Self { - resolver: AllowedPathResolver::new(allowed_paths)?, - }) - } - - /// Creates a new grep tool with an existing resolver. - pub fn with_resolver(resolver: AllowedPathResolver) -> Self { + /// Creates a new grep tool with a shared resolver. + /// + /// See [`ReadTool::new`] for usage example. + /// + /// [`ReadTool::new`]: super::ReadTool::new + pub fn new(resolver: AllowedPathResolver) -> Self { Self { resolver } } } @@ -128,7 +124,8 @@ mod tests { let dir = TempDir::new().unwrap(); std::fs::write(dir.path().join("test.txt"), "hello world").unwrap(); - let tool: GrepTool = GrepTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool: GrepTool = GrepTool::new(resolver); let result = tool .call(GrepArgs { pattern: "hello".to_string(), @@ -145,7 +142,8 @@ mod tests { #[tokio::test] async fn rejects_path_traversal() { let dir = TempDir::new().unwrap(); - let tool: GrepTool = GrepTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool: GrepTool = GrepTool::new(resolver); let result = tool .call(GrepArgs { pattern: "test".to_string(), @@ -160,7 +158,8 @@ mod tests { #[tokio::test] async fn rejects_empty_pattern() { let dir = TempDir::new().unwrap(); - let tool: GrepTool = GrepTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool: GrepTool = GrepTool::new(resolver); let result = tool .call(GrepArgs { pattern: " ".to_string(), @@ -187,7 +186,8 @@ mod tests { std::fs::write(dir.path().join("utf8_test.txt"), &long_line).unwrap(); - let tool: GrepTool = GrepTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool: GrepTool = GrepTool::new(resolver); let result = tool .call(GrepArgs { pattern: "match_me".to_string(), diff --git a/src/llm-coding-tools-rig/src/allowed/mod.rs b/src/llm-coding-tools-rig/src/allowed/mod.rs index 76b56336..4bab361d 100644 --- a/src/llm-coding-tools-rig/src/allowed/mod.rs +++ b/src/llm-coding-tools-rig/src/allowed/mod.rs @@ -2,7 +2,6 @@ //! //! These tools restrict file access to configured allowed directories. //! Use for sandboxed file system access. -//! //! # Available Tools //! //! - [`ReadTool`] - Read file contents within allowed paths @@ -10,6 +9,8 @@ //! - [`EditTool`] - Edit file with search/replace within allowed paths //! - [`GlobTool`] - Find files by pattern within allowed paths //! - [`GrepTool`] - Search file contents within allowed paths +//! +//! [`AllowedPathResolver`]: llm_coding_tools_core::path::AllowedPathResolver mod edit; mod glob; diff --git a/src/llm-coding-tools-rig/src/allowed/read.rs b/src/llm-coding-tools-rig/src/allowed/read.rs index e4dbf91c..ab165f08 100644 --- a/src/llm-coding-tools-rig/src/allowed/read.rs +++ b/src/llm-coding-tools-rig/src/allowed/read.rs @@ -3,12 +3,11 @@ use llm_coding_tools_core::operations::read_file; use llm_coding_tools_core::path::AllowedPathResolver; use llm_coding_tools_core::tool_names; -use llm_coding_tools_core::{ToolContext, ToolError, ToolOutput, ToolResult}; +use llm_coding_tools_core::{ToolContext, ToolError, ToolOutput}; use rig::completion::ToolDefinition; use rig::tool::Tool; use schemars::{schema_for, JsonSchema}; use serde::Deserialize; -use std::path::Path; const DEFAULT_OFFSET: usize = 1; const DEFAULT_LIMIT: usize = 2000; @@ -43,15 +42,26 @@ pub struct ReadTool { } impl ReadTool { - /// Creates a new read tool restricted to the given directories. - pub fn new(allowed_paths: impl IntoIterator>) -> ToolResult { - Ok(Self { - resolver: AllowedPathResolver::new(allowed_paths)?, - }) - } - - /// Creates a new read tool with an existing resolver. - pub fn with_resolver(resolver: AllowedPathResolver) -> Self { + /// Creates a new read tool with a shared resolver. + /// + /// Use a single [`AllowedPathResolver`] across all allowed tools to ensure + /// consistent path access: + /// + /// ```no_run + /// use llm_coding_tools_core::path::AllowedPathResolver; + /// use llm_coding_tools_rig::allowed::{ReadTool, WriteTool, EditTool}; + /// use std::path::PathBuf; + /// + /// let resolver = AllowedPathResolver::new(vec![ + /// std::env::current_dir().unwrap(), + /// PathBuf::from("/tmp"), + /// ]).unwrap(); + /// + /// let read: ReadTool = ReadTool::new(resolver.clone()); + /// let write = WriteTool::new(resolver.clone()); + /// let edit = EditTool::new(resolver); + /// ``` + pub fn new(resolver: AllowedPathResolver) -> Self { Self { resolver } } } @@ -103,7 +113,8 @@ mod tests { let file_path = dir.path().join("test.txt"); std::fs::write(&file_path, "hello\nworld\n").unwrap(); - let tool: ReadTool = ReadTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool: ReadTool = ReadTool::new(resolver); let args = ReadArgs { file_path: "test.txt".to_string(), offset: 1, @@ -116,7 +127,8 @@ mod tests { #[tokio::test] async fn rejects_path_traversal() { let dir = TempDir::new().unwrap(); - let tool: ReadTool = ReadTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool: ReadTool = ReadTool::new(resolver); let args = ReadArgs { file_path: "../../../etc/passwd".to_string(), offset: 1, diff --git a/src/llm-coding-tools-rig/src/allowed/write.rs b/src/llm-coding-tools-rig/src/allowed/write.rs index d0a72a94..979daba6 100644 --- a/src/llm-coding-tools-rig/src/allowed/write.rs +++ b/src/llm-coding-tools-rig/src/allowed/write.rs @@ -3,12 +3,11 @@ use llm_coding_tools_core::operations::write_file; use llm_coding_tools_core::path::AllowedPathResolver; use llm_coding_tools_core::tool_names; -use llm_coding_tools_core::{ToolContext, ToolError, ToolResult}; +use llm_coding_tools_core::{ToolContext, ToolError}; use rig::completion::ToolDefinition; use rig::tool::Tool; use schemars::{schema_for, JsonSchema}; use serde::Deserialize; -use std::path::Path; /// Arguments for the write tool. #[derive(Debug, Clone, Deserialize, JsonSchema)] @@ -26,15 +25,12 @@ pub struct WriteTool { } impl WriteTool { - /// Creates a new write tool restricted to the given directories. - pub fn new(allowed_paths: impl IntoIterator>) -> ToolResult { - Ok(Self { - resolver: AllowedPathResolver::new(allowed_paths)?, - }) - } - - /// Creates a new write tool with an existing resolver. - pub fn with_resolver(resolver: AllowedPathResolver) -> Self { + /// Creates a new write tool with a shared resolver. + /// + /// See [`ReadTool::new`] for usage example. + /// + /// [`ReadTool::new`]: super::ReadTool::new + pub fn new(resolver: AllowedPathResolver) -> Self { Self { resolver } } } @@ -78,7 +74,8 @@ mod tests { #[tokio::test] async fn writes_new_file() { let dir = TempDir::new().unwrap(); - let tool = WriteTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool = WriteTool::new(resolver); let result = tool .call(WriteToolArgs { file_path: "new.txt".to_string(), @@ -93,7 +90,8 @@ mod tests { #[tokio::test] async fn rejects_path_traversal() { let dir = TempDir::new().unwrap(); - let tool = WriteTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool = WriteTool::new(resolver); let result = tool .call(WriteToolArgs { file_path: "../../../tmp/escape.txt".to_string(), diff --git a/src/llm-coding-tools-rig/src/lib.rs b/src/llm-coding-tools-rig/src/lib.rs index a7c4c3d2..02627f62 100644 --- a/src/llm-coding-tools-rig/src/lib.rs +++ b/src/llm-coding-tools-rig/src/lib.rs @@ -52,14 +52,14 @@ mod tests { #[test] fn preamble_builder_with_real_tools() { - let mut pb = PreambleBuilder::::new(); + let mut pb = PreambleBuilder::new(); let read: absolute::ReadTool = pb.track(absolute::ReadTool::new()); let bash = pb.track(BashTool::new()); let preamble = pb.build(); - assert!(preamble.contains("## Read Tool")); - assert!(preamble.contains("## Bash Tool")); + assert!(preamble.contains("## `Read` Tool")); + assert!(preamble.contains("## `Bash` Tool")); assert!(preamble.contains("absolute path")); // From READ_ABSOLUTE // Tools are returned unchanged diff --git a/src/llm-coding-tools-serdesai/README.md b/src/llm-coding-tools-serdesai/README.md index 68a212fb..e95df580 100644 --- a/src/llm-coding-tools-serdesai/README.md +++ b/src/llm-coding-tools-serdesai/README.md @@ -39,7 +39,7 @@ use serdes_ai::prelude::*; #[tokio::main] async fn main() -> std::result::Result<(), Box> { let (todo_read, todo_write, _state) = create_todo_tools(); - let mut pb = PreambleBuilder::::new(); + let mut pb = PreambleBuilder::new(); // Build agent with tools - call .system_prompt() last let agent = AgentBuilder::<(), String>::from_model("openai:gpt-4o")? @@ -71,6 +71,7 @@ File tools come in `absolute::*` (unrestricted) and `allowed::*` (sandboxed) var ```rust,no_run use llm_coding_tools_serdesai::absolute::{ReadTool, WriteTool}; use llm_coding_tools_serdesai::allowed::{ReadTool as AllowedReadTool, WriteTool as AllowedWriteTool}; +use llm_coding_tools_serdesai::AllowedPathResolver; use std::path::PathBuf; // Unrestricted access (absolute paths) @@ -78,12 +79,13 @@ let read = ReadTool::::new(); // Sandboxed access (paths relative to allowed directories) let allowed_paths = vec![PathBuf::from("/home/user/project"), PathBuf::from("/tmp")]; -let sandboxed_read: AllowedReadTool = AllowedReadTool::new(allowed_paths.clone()).unwrap(); -let sandboxed_write = AllowedWriteTool::new(allowed_paths).unwrap(); +let resolver = AllowedPathResolver::new(allowed_paths).unwrap(); +let sandboxed_read: AllowedReadTool = AllowedReadTool::new(resolver.clone()); +let sandboxed_write = AllowedWriteTool::new(resolver); ``` Other tools: `BashTool`, `WebFetchTool`, `TaskTool`, `TodoReadTool`, `TodoWriteTool`. -Use `PreambleBuilder` to track tools and pass `pb.build()` to `.system_prompt()`. +Use `PreambleBuilder` to track tools and pass `pb.build()` to `.system_prompt()`. Set `working_directory()` so the environment section is populated. Use `AgentBuilderExt::tool()` to add tools that implement `Tool` to the agent. Context strings are re-exported in `llm_coding_tools_serdesai::context` (e.g., `BASH`, `READ_ABSOLUTE`). diff --git a/src/llm-coding-tools-serdesai/examples/serdesai-basic.rs b/src/llm-coding-tools-serdesai/examples/serdesai-basic.rs index 87d23e7a..abb62a35 100644 --- a/src/llm-coding-tools-serdesai/examples/serdesai-basic.rs +++ b/src/llm-coding-tools-serdesai/examples/serdesai-basic.rs @@ -16,7 +16,7 @@ use serdes_ai::prelude::*; #[tokio::main] async fn main() -> std::result::Result<(), Box> { // === Create preamble builder to track tools === - let mut pb = PreambleBuilder::::new(); + let mut pb = PreambleBuilder::new().working_directory(std::env::current_dir()?.to_string()); // === Create todo tools with shared state === let (todo_read, todo_write, _state) = create_todo_tools(); diff --git a/src/llm-coding-tools-serdesai/examples/serdesai-sandboxed.rs b/src/llm-coding-tools-serdesai/examples/serdesai-sandboxed.rs index d7e690a7..4f025c3b 100644 --- a/src/llm-coding-tools-serdesai/examples/serdesai-sandboxed.rs +++ b/src/llm-coding-tools-serdesai/examples/serdesai-sandboxed.rs @@ -9,6 +9,7 @@ //! //! Run: OPENAI_API_KEY=... cargo run --example serdesai-sandboxed -p llm-coding-tools-serdesai +use llm_coding_tools_serdesai::AllowedPathResolver; use llm_coding_tools_serdesai::PreambleBuilder; use llm_coding_tools_serdesai::agent_ext::AgentBuilderExt; use llm_coding_tools_serdesai::allowed::{EditTool, GlobTool, GrepTool, ReadTool, WriteTool}; @@ -25,15 +26,27 @@ async fn main() -> std::result::Result<(), Box> { std::env::temp_dir(), // Temp directory (cross-platform) ]; - // === Create tools with allowed paths === - let read: ReadTool = ReadTool::new(allowed_paths.clone())?; - let write = WriteTool::new(allowed_paths.clone())?; - let edit = EditTool::new(allowed_paths.clone())?; - let glob = GlobTool::new(allowed_paths.clone())?; - let grep: GrepTool = GrepTool::new(allowed_paths)?; + // === Create resolver and tools === + // + // Create one resolver and share it across tools. + // More efficient and ensures consistency. + let resolver = AllowedPathResolver::new(allowed_paths)?; + + let read: ReadTool = ReadTool::new(resolver.clone()); + let write = WriteTool::new(resolver.clone()); + let edit = EditTool::new(resolver.clone()); + let glob = GlobTool::new(resolver.clone()); + let grep: GrepTool = GrepTool::new(resolver.clone()); + + // === Build agent with sandboxed tools === + // + // Use PreambleBuilder with fluent chaining: + // - working_directory() and allowed_paths() consume self (chaining) + // - track() takes &mut self (passthrough for agent builder) + let mut pb = PreambleBuilder::new() + .working_directory(std::env::current_dir()?.to_string()) + .allowed_paths(&resolver); - // === Build agent with sandboxed tools - call .system_prompt() last === - let mut pb = PreambleBuilder::::new(); let agent = AgentBuilder::<(), String>::from_model("openai:gpt-4o")? .tool(pb.track(read)) .tool(pb.track(write)) diff --git a/src/llm-coding-tools-serdesai/src/allowed/edit.rs b/src/llm-coding-tools-serdesai/src/allowed/edit.rs index 232e4574..13da3d2f 100644 --- a/src/llm-coding-tools-serdesai/src/allowed/edit.rs +++ b/src/llm-coding-tools-serdesai/src/allowed/edit.rs @@ -9,7 +9,6 @@ use serde::Deserialize; use serdes_ai::tools::{ RunContext, SchemaBuilder, Tool, ToolDefinition, ToolError, ToolResult, ToolReturn, }; -use std::path::Path; use crate::common::edit::error_to_serdes; @@ -34,15 +33,13 @@ pub struct EditTool { } impl EditTool { - /// Creates a new edit tool restricted to the given directories. + /// Creates a new edit tool with a shared resolver. /// - /// Returns an error if any directory doesn't exist or can't be canonicalized. - pub fn new( - allowed_paths: impl IntoIterator>, - ) -> llm_coding_tools_core::ToolResult { - Ok(Self { - resolver: AllowedPathResolver::new(allowed_paths)?, - }) + /// See [`ReadTool::new`] for usage example. + /// + /// [`ReadTool::new`]: super::ReadTool::new + pub fn new(resolver: AllowedPathResolver) -> Self { + Self { resolver } } } @@ -114,7 +111,8 @@ mod tests { let dir = TempDir::new().unwrap(); std::fs::write(dir.path().join("test.txt"), "hello world").unwrap(); - let tool = EditTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool = EditTool::new(resolver); let result = tool .call( &mock_ctx(), @@ -134,7 +132,8 @@ mod tests { #[tokio::test] async fn rejects_path_traversal() { let dir = TempDir::new().unwrap(); - let tool = EditTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool = EditTool::new(resolver); let result = tool .call( &mock_ctx(), diff --git a/src/llm-coding-tools-serdesai/src/allowed/glob.rs b/src/llm-coding-tools-serdesai/src/allowed/glob.rs index 44b7a580..d4bbfb56 100644 --- a/src/llm-coding-tools-serdesai/src/allowed/glob.rs +++ b/src/llm-coding-tools-serdesai/src/allowed/glob.rs @@ -7,7 +7,6 @@ use llm_coding_tools_core::tool_names; use llm_coding_tools_core::{ToolContext, ToolOutput}; use serde::Deserialize; use serdes_ai::tools::{RunContext, SchemaBuilder, Tool, ToolDefinition, ToolError, ToolResult}; -use std::path::Path; use crate::convert::to_serdes_result; @@ -27,13 +26,13 @@ pub struct GlobTool { } impl GlobTool { - /// Creates a new glob tool restricted to the given directories. - pub fn new( - allowed_paths: impl IntoIterator>, - ) -> llm_coding_tools_core::ToolResult { - Ok(Self { - resolver: AllowedPathResolver::new(allowed_paths)?, - }) + /// Creates a new glob tool with a shared resolver. + /// + /// See [`ReadTool::new`] for usage example. + /// + /// [`ReadTool::new`]: super::ReadTool::new + pub fn new(resolver: AllowedPathResolver) -> Self { + Self { resolver } } } @@ -111,7 +110,8 @@ mod tests { fs::create_dir_all(dir.path().join("src")).unwrap(); File::create(dir.path().join("src/lib.rs")).unwrap(); - let tool = GlobTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool = GlobTool::new(resolver); let result = tool .call( &mock_ctx(), @@ -130,7 +130,8 @@ mod tests { #[tokio::test] async fn rejects_path_traversal() { let dir = TempDir::new().unwrap(); - let tool = GlobTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool = GlobTool::new(resolver); let result = tool .call( &mock_ctx(), diff --git a/src/llm-coding-tools-serdesai/src/allowed/grep.rs b/src/llm-coding-tools-serdesai/src/allowed/grep.rs index 4cbc8b8a..da3f2e6b 100644 --- a/src/llm-coding-tools-serdesai/src/allowed/grep.rs +++ b/src/llm-coding-tools-serdesai/src/allowed/grep.rs @@ -9,7 +9,6 @@ use serde::Deserialize; use serdes_ai::tools::{ RunContext, SchemaBuilder, Tool, ToolDefinition, ToolError, ToolResult, ToolReturn, }; -use std::path::Path; use crate::convert::to_serdes_result; @@ -38,13 +37,13 @@ pub struct GrepTool { } impl GrepTool { - /// Creates a new grep tool restricted to the given directories. - pub fn new( - allowed_paths: impl IntoIterator>, - ) -> llm_coding_tools_core::ToolResult { - Ok(Self { - resolver: AllowedPathResolver::new(allowed_paths)?, - }) + /// Creates a new grep tool with a shared resolver. + /// + /// See [`ReadTool::new`] for usage example. + /// + /// [`ReadTool::new`]: super::ReadTool::new + pub fn new(resolver: AllowedPathResolver) -> Self { + Self { resolver } } } @@ -158,7 +157,8 @@ mod tests { let dir = TempDir::new().unwrap(); std::fs::write(dir.path().join("test.txt"), "hello world").unwrap(); - let tool: GrepTool = GrepTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool: GrepTool = GrepTool::new(resolver); let result = tool .call( &mock_ctx(), @@ -178,7 +178,8 @@ mod tests { #[tokio::test] async fn rejects_path_traversal() { let dir = TempDir::new().unwrap(); - let tool: GrepTool = GrepTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool: GrepTool = GrepTool::new(resolver); let result = tool .call( &mock_ctx(), @@ -195,7 +196,8 @@ mod tests { #[tokio::test] async fn rejects_empty_pattern() { let dir = TempDir::new().unwrap(); - let tool: GrepTool = GrepTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool: GrepTool = GrepTool::new(resolver); let result = tool .call( &mock_ctx(), diff --git a/src/llm-coding-tools-serdesai/src/allowed/mod.rs b/src/llm-coding-tools-serdesai/src/allowed/mod.rs index f4dd4ee5..910102d7 100644 --- a/src/llm-coding-tools-serdesai/src/allowed/mod.rs +++ b/src/llm-coding-tools-serdesai/src/allowed/mod.rs @@ -2,7 +2,6 @@ //! //! These tools restrict file access to configured allowed directories. //! Use for sandboxed file system access. -//! //! # Available Tools //! //! - [`ReadTool`] - Read file contents within allowed paths @@ -10,6 +9,8 @@ //! - [`EditTool`] - Edit file with search/replace within allowed paths //! - [`GlobTool`] - Find files by pattern within allowed paths //! - [`GrepTool`] - Search file contents within allowed paths +//! +//! [`AllowedPathResolver`]: llm_coding_tools_core::path::AllowedPathResolver mod edit; mod glob; diff --git a/src/llm-coding-tools-serdesai/src/allowed/read.rs b/src/llm-coding-tools-serdesai/src/allowed/read.rs index 956ceac6..ba53ca13 100644 --- a/src/llm-coding-tools-serdesai/src/allowed/read.rs +++ b/src/llm-coding-tools-serdesai/src/allowed/read.rs @@ -1,13 +1,12 @@ //! Read file tool using [`AllowedPathResolver`]. use async_trait::async_trait; +use llm_coding_tools_core::ToolContext; use llm_coding_tools_core::operations::read_file; use llm_coding_tools_core::path::AllowedPathResolver; use llm_coding_tools_core::tool_names; -use llm_coding_tools_core::{ToolContext, ToolResult as CoreToolResult}; use serde::Deserialize; use serdes_ai::tools::{RunContext, SchemaBuilder, Tool, ToolDefinition, ToolError, ToolResult}; -use std::path::Path; use crate::convert::to_serdes_result; @@ -44,13 +43,27 @@ pub struct ReadTool { } impl ReadTool { - /// Creates a new read tool restricted to the given directories. + /// Creates a new read tool with a shared resolver. /// - /// Returns an error if any directory doesn't exist or can't be canonicalized. - pub fn new(allowed_paths: impl IntoIterator>) -> CoreToolResult { - Ok(Self { - resolver: AllowedPathResolver::new(allowed_paths)?, - }) + /// Use a single [`AllowedPathResolver`] across all allowed tools to ensure + /// consistent path access: + /// + /// ```no_run + /// use llm_coding_tools_core::path::AllowedPathResolver; + /// use llm_coding_tools_serdesai::allowed::{ReadTool, WriteTool, EditTool}; + /// use std::path::PathBuf; + /// + /// let resolver = AllowedPathResolver::new(vec![ + /// std::env::current_dir().unwrap(), + /// PathBuf::from("/tmp"), + /// ]).unwrap(); + /// + /// let read: ReadTool = ReadTool::new(resolver.clone()); + /// let write = WriteTool::new(resolver.clone()); + /// let edit = EditTool::new(resolver); + /// ``` + pub fn new(resolver: AllowedPathResolver) -> Self { + Self { resolver } } } @@ -125,7 +138,8 @@ mod tests { let dir = TempDir::new().unwrap(); std::fs::write(dir.path().join("test.txt"), "hello\nworld\n").unwrap(); - let tool: ReadTool = ReadTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool: ReadTool = ReadTool::new(resolver); let result = tool .call( &mock_ctx(), @@ -146,7 +160,8 @@ mod tests { #[tokio::test] async fn rejects_path_traversal() { let dir = TempDir::new().unwrap(); - let tool: ReadTool = ReadTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool: ReadTool = ReadTool::new(resolver); let result = tool .call( &mock_ctx(), diff --git a/src/llm-coding-tools-serdesai/src/allowed/write.rs b/src/llm-coding-tools-serdesai/src/allowed/write.rs index fbed5734..be19a9bf 100644 --- a/src/llm-coding-tools-serdesai/src/allowed/write.rs +++ b/src/llm-coding-tools-serdesai/src/allowed/write.rs @@ -7,7 +7,6 @@ use llm_coding_tools_core::tool_names; use llm_coding_tools_core::{ToolContext, ToolOutput}; use serde::Deserialize; use serdes_ai::tools::{RunContext, SchemaBuilder, Tool, ToolDefinition, ToolError, ToolResult}; -use std::path::Path; use crate::convert::to_serdes_result; @@ -26,13 +25,13 @@ pub struct WriteTool { } impl WriteTool { - /// Creates a new write tool restricted to the given directories. - pub fn new( - allowed_paths: impl IntoIterator>, - ) -> llm_coding_tools_core::ToolResult { - Ok(Self { - resolver: AllowedPathResolver::new(allowed_paths)?, - }) + /// Creates a new write tool with a shared resolver. + /// + /// See [`ReadTool::new`] for usage example. + /// + /// [`ReadTool::new`]: super::ReadTool::new + pub fn new(resolver: AllowedPathResolver) -> Self { + Self { resolver } } } @@ -88,7 +87,8 @@ mod tests { #[tokio::test] async fn writes_new_file() { let dir = TempDir::new().unwrap(); - let tool = WriteTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool = WriteTool::new(resolver); let result = tool .call( &mock_ctx(), @@ -108,7 +108,8 @@ mod tests { #[tokio::test] async fn rejects_path_traversal() { let dir = TempDir::new().unwrap(); - let tool = WriteTool::new([dir.path()]).unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + let tool = WriteTool::new(resolver); let result = tool .call( &mock_ctx(),