feat: universal tool support - inject instructions by default, add plan mode TODO, improve file handling

1. Tool instructions now ALWAYS injected by default:
   - Removed condition that only injected on first request
   - Any client (Continue, hollama) can now use tools without client-side setup
   - Added check to avoid duplicating instructions if already present

2. Updated tool instructions with file verification guidance:
   - Added 'FILE OPERATIONS - ALWAYS VERIFY FIRST' section
   - Instructs to use 'ls' and 'grep' to verify files exist before reading
   - Prevents blind file reads on non-existent paths

3. Added TODO to README:
   - Plan mode feature (disable tool execution for planning-only conversations)
   - Current status section showing what's implemented

4. Working directory extraction from prompts:
   - New _extract_working_dir_from_prompt() function
   - Extracts paths from patterns like 'in /path/to/dir', 'under /path/to/dir'
   - Validates paths exist before using
   - Falls back to auto-detection if not found in prompt
   - All 41 tests passing
This commit is contained in:
2026-02-25 20:37:23 +01:00
parent c46684f03e
commit a09d23156b
4 changed files with 90 additions and 9 deletions
+16
View File
@@ -282,6 +282,22 @@ Major refactoring completed to improve modularity:
See `docs/ARCHITECTURE.md` for detailed architecture documentation.
## TODO / Roadmap
### Planned Features
- **Plan Mode**: Add a "plan mode" that disables tool execution for planning-only conversations. This would allow the model to discuss file changes without actually modifying them until explicitly confirmed.
- Usage: `--plan-mode` flag or API parameter
- When enabled: Model can see what tools would do but doesn't execute them
- Use case: Review changes before applying them
### Current Status
- ✅ Tool instructions now injected by default for all clients
- ✅ Improved file operation safety (verify with ls/grep before reading)
- ✅ Working directory support (extracted from client context)
- 🔄 Plan mode - coming soon
## Contributing
Contributions are welcome! Please ensure:
+8 -1
View File
@@ -15,6 +15,13 @@ CRITICAL RULES:
6. NO explanations beyond necessary. Be concise.
7. NO markdown formatting. Use plain text only.
FILE OPERATIONS - ALWAYS VERIFY FIRST:
Before reading a file, ALWAYS verify it exists using 'ls' or 'grep':
- Use 'ls' to list directory contents and confirm the file exists
- Use 'grep' to search for files containing specific content
- Only use 'read' AFTER confirming the file exists
- If the user mentions a working directory, use paths relative to that directory
TOOL USAGE FORMAT:
For read operations:
@@ -25,7 +32,7 @@ For write operations:
TOOL: write
ARGUMENTS: {"filePath": "path/to/file", "content": "content to write"}
For bash commands:
For bash commands (including ls, grep):
TOOL: bash
ARGUMENTS: {"command": "your command here"}
+53 -1
View File
@@ -21,12 +21,53 @@ from api.tool_parser import parse_tool_calls
from utils.token_counter import count_tokens
from tools.executor import get_tool_executor
from chatlog import get_chat_logger
from chatlog import get_chat_logger
logger = logging.getLogger(__name__)
def _extract_working_dir_from_prompt(prompt: str) -> Optional[str]:
"""Extract working directory from user prompt.
Looks for patterns like:
- "in the /path/to/dir directory"
- "in directory /path/to/dir"
- "in /path/to/dir"
- "under /path/to/dir"
- "from /path/to/dir"
Args:
prompt: User prompt text
Returns:
Extracted directory path or None
"""
import re
import os
# Common patterns for directory mentions
patterns = [
r'in the\s+([/~]?[\w\-/.]+)\s+(?:directory|folder|dir)',
r'in\s+(?:directory|folder|dir)\s+([/~]?[\w\-/.]+)',
r'(?:in|under|from|at)\s+([/~]?[\w\-/.]{3,})', # At least 3 chars to avoid "in a"
]
for pattern in patterns:
match = re.search(pattern, prompt, re.IGNORECASE)
if match:
path = match.group(1)
# Validate it looks like a path
if path.startswith('/') or path.startswith('~') or '/' in path:
# Expand home directory
if path.startswith('~'):
path = os.path.expanduser(path)
# Check if it's a valid directory or parent exists
if os.path.isdir(path) or os.path.isdir(os.path.dirname(path)):
return os.path.abspath(path)
return None
def _sanitize_tools(tools: Optional[list]) -> Optional[list]:
"""Sanitize tool definitions to fix invalid schemas.
@@ -290,6 +331,17 @@ async def handle_chat_completion(
# Initialize chat logger (if enabled via LOCAL_SWARM_CHATLOG=1)
chat_logger = get_chat_logger()
# Extract working directory from prompt if not provided by client
if client_working_dir is None:
# Try to extract from user messages
for msg in reversed(request.messages):
if msg.role == 'user':
extracted_dir = _extract_working_dir_from_prompt(msg.content)
if extracted_dir:
client_working_dir = extracted_dir
logger.info(f"📁 Extracted working directory from prompt: {client_working_dir}")
break
# Log initial conversation history to chatlog
for msg in request.messages:
if msg.role == 'user':
+13 -7
View File
@@ -153,7 +153,13 @@ def _filter_messages(messages: List[ChatMessage]) -> List[ChatMessage]:
def _add_tool_instructions(messages: List[ChatMessage]) -> List[ChatMessage]:
"""Add tool instructions to messages if needed.
"""Add tool instructions to the beginning of messages.
Tool instructions are now ALWAYS injected by default so any client
(Continue, hollama, etc.) can use tools without requiring client-side
tool instruction injection.
TODO: Add a "plan mode" that disables tool use for planning-only conversations.
Args:
messages: List of chat messages
@@ -161,13 +167,13 @@ def _add_tool_instructions(messages: List[ChatMessage]) -> List[ChatMessage]:
Returns:
Messages with tool instructions added
"""
has_assistant = any(msg.role == "assistant" for msg in messages)
if has_assistant:
return messages
tool_instructions = _load_tool_instructions()
logger.debug(f"Using {'opencode' if _USE_OPENCODE_TOOLS else 'local'} tool mode: {len(tool_instructions)} chars")
logger.debug(f"Injecting tool instructions: {len(tool_instructions)} chars")
# Check if instructions already present (avoid duplication)
if messages and messages[0].role == "system" and "AVAILABLE TOOLS" in messages[0].content:
logger.debug("Tool instructions already present, skipping injection")
return messages
return [ChatMessage(role="system", content=tool_instructions)] + messages