diff --git a/docs/adding_tools.md b/docs/adding_tools.md index 480a41e..9cf8798 100644 --- a/docs/adding_tools.md +++ b/docs/adding_tools.md @@ -51,9 +51,10 @@ from tools.simple.base import SimpleTool class ChatRequest(ToolRequest): prompt: str = Field(..., description="Your question or idea.") - files: list[str] | None = Field(default_factory=list) - working_directory: str = Field( - ..., description="Absolute full directory path where the assistant AI can save generated code for implementation." + absolute_file_paths: list[str] | None = Field(default_factory=list) + working_directory_absolute_path: str = Field( + ..., + description="Absolute path to an existing directory where generated code can be saved.", ) class ChatTool(SimpleTool): @@ -72,15 +73,15 @@ class ChatTool(SimpleTool): def get_tool_fields(self) -> dict[str, dict[str, object]]: return { "prompt": {"type": "string", "description": "Your question."}, - "files": SimpleTool.FILES_FIELD, - "working_directory": { + "absolute_file_paths": SimpleTool.FILES_FIELD, + "working_directory_absolute_path": { "type": "string", - "description": "Absolute full directory path where the assistant AI can save generated code for implementation.", + "description": "Absolute path to an existing directory for generated code artifacts.", }, } def get_required_fields(self) -> list[str]: - return ["prompt", "working_directory"] + return ["prompt", "working_directory_absolute_path"] async def prepare_prompt(self, request: ChatRequest) -> str: return self.prepare_chat_style_prompt(request) diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 58b99d7..8db3cd0 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -395,11 +395,11 @@ User: "Use gemini to review this code: [50,000+ character detailed analysis]" Zen MCP: "The prompt is too large for MCP's token limits (>50,000 characters). Please save the prompt text to a temporary file named 'prompt.txt' and resend the request with an empty prompt string and the absolute file path included -in the files parameter, along with any other files you wish to share as context." +in the absolute_file_paths parameter, along with any other files you wish to share as context." # Claude automatically handles this: - Saves your prompt to /tmp/prompt.txt -- Resends: "Use gemini to review this code" with files=["/tmp/prompt.txt", "/path/to/code.py"] +- Resends: "Use gemini to review this code" with absolute_file_paths=["/tmp/prompt.txt", "/path/to/code.py"] # Server processes the large prompt through Gemini's 1M context # Returns comprehensive analysis within MCP's response limits diff --git a/docs/getting-started.md b/docs/getting-started.md index 2f1db83..fa0af0b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -141,6 +141,7 @@ Edit `~/.codex/config.toml`: [mcp_servers.zen] command = "bash" args = ["-c", "for p in $(which uvx 2>/dev/null) $HOME/.local/bin/uvx /opt/homebrew/bin/uvx /usr/local/bin/uvx uvx; do [ -x \\\"$p\\\" ] && exec \\\"$p\\\" --from git+https://github.com/BeehiveInnovations/zen-mcp-server.git zen-mcp-server; done; echo 'uvx not found' >&2; exit 1"] +tool_timeout_sec = 1200 # 20 minutes; added automatically by the setup script so upstream providers can respond [mcp_servers.zen.env] PATH = "/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:$HOME/.local/bin:$HOME/.cargo/bin:$HOME/bin" @@ -304,7 +305,7 @@ CUSTOM_MODEL_NAME=llama3.2 # Default model name ## Prevent Client Timeouts -Some MCP clients default to short timeouts and can disconnect from Zen during long tool runs. Configure each client to wait at least five minutes (300 000 ms) before giving up. +Some MCP clients default to short timeouts and can disconnect from Zen during long tool runs. Configure each client with a generous ceiling (we recommend at least five minutes); the Zen setup script now writes a 20-minute tool timeout for Codex so upstream providers contacted by the server have time to respond. ### Claude Code & Claude Desktop @@ -330,10 +331,10 @@ Codex exposes per-server timeouts in `~/.codex/config.toml`. Add (or bump) these command = "..." args = ["..."] startup_timeout_sec = 300 # default is 10 seconds -tool_timeout_sec = 300 # default is 60 seconds +tool_timeout_sec = 1200 # default is 60 seconds; setup script pre-populates 20 minutes so upstream providers can respond ``` -`startup_timeout_sec` covers the initial handshake/list tools step, while `tool_timeout_sec` governs each tool call. Increase either beyond 300 if your workflow routinely exceeds five minutes. +`startup_timeout_sec` covers the initial handshake/list tools step, while `tool_timeout_sec` governs each tool call. Raise the latter if the providers your MCP server invokes routinely need more than 20 minutes. ### Gemini CLI diff --git a/docs/tools/chat.md b/docs/tools/chat.md index d063851..b5da441 100644 --- a/docs/tools/chat.md +++ b/docs/tools/chat.md @@ -53,9 +53,9 @@ word verdict in the end. - `prompt`: Your question or discussion topic (required) - `model`: auto|pro|flash|flash-2.0|flashlite|o3|o3-mini|o4-mini|gpt4.1|gpt5|gpt5-mini|gpt5-nano (default: server default) -- `files`: Optional files for context (absolute paths) +- `absolute_file_paths`: Optional absolute file or directory paths for additional context - `images`: Optional images for visual context (absolute paths) -- `working_directory`: **Required** - Absolute directory path where generated code artifacts will be saved +- `working_directory_absolute_path`: **Required** - Absolute path to an existing directory where generated code artifacts will be saved - `temperature`: Response creativity (0-1, default 0.5) - `thinking_mode`: minimal|low|medium|high|max (default: medium, Gemini only) - `continuation_id`: Continue previous conversations diff --git a/run-server.sh b/run-server.sh index 5db5353..bf7deb0 100755 --- a/run-server.sh +++ b/run-server.sh @@ -1726,6 +1726,7 @@ check_codex_cli_integration() { echo "[mcp_servers.zen]" echo "command = \"bash\"" echo "args = [\"-c\", \"for p in \$(which uvx 2>/dev/null) \$HOME/.local/bin/uvx /opt/homebrew/bin/uvx /usr/local/bin/uvx uvx; do [ -x \\\"\$p\\\" ] && exec \\\"\$p\\\" --from git+https://github.com/BeehiveInnovations/zen-mcp-server.git zen-mcp-server; done; echo 'uvx not found' >&2; exit 1\"]" + echo "tool_timeout_sec = 1200" echo "" echo "[mcp_servers.zen.env]" echo "PATH = \"/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:\$HOME/.local/bin:\$HOME/.cargo/bin:\$HOME/bin\"" @@ -1808,10 +1809,11 @@ PY # Generate example with actual environment variables for error case env_vars=$(parse_env_variables) - cat << EOF +cat << EOF [mcp_servers.zen] command = "sh" args = ["-c", "exec \$(which uvx 2>/dev/null || echo uvx) --from git+https://github.com/BeehiveInnovations/zen-mcp-server.git zen-mcp-server"] +tool_timeout_sec = 1200 [mcp_servers.zen.env] PATH = "/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:\$HOME/.local/bin:\$HOME/.cargo/bin:\$HOME/bin" diff --git a/server.py b/server.py index 11a468b..26ed923 100644 --- a/server.py +++ b/server.py @@ -852,9 +852,10 @@ async def handle_call_tool(name: str, arguments: dict[str, Any]) -> list[TextCon # EARLY FILE SIZE VALIDATION AT MCP BOUNDARY # Check file sizes before tool execution using resolved model - if "files" in arguments and arguments["files"]: - logger.debug(f"Checking file sizes for {len(arguments['files'])} files with model {model_name}") - file_size_check = check_total_file_size(arguments["files"], model_name) + argument_files = arguments.get("absolute_file_paths") + if argument_files: + logger.debug(f"Checking file sizes for {len(argument_files)} files with model {model_name}") + file_size_check = check_total_file_size(argument_files, model_name) if file_size_check: logger.warning(f"File size check failed for {name} with model {model_name}") raise ToolExecutionError(ToolOutput(**file_size_check).model_dump_json()) @@ -1074,7 +1075,7 @@ async def reconstruct_thread_context(arguments: dict[str, Any]) -> dict[str, Any user_prompt = arguments.get("prompt", "") if user_prompt: # Capture files referenced in this turn - user_files = arguments.get("files", []) + user_files = arguments.get("absolute_file_paths") or [] logger.debug(f"[CONVERSATION_DEBUG] Adding user turn to thread {continuation_id}") from utils.token_utils import estimate_tokens @@ -1268,9 +1269,10 @@ async def reconstruct_thread_context(arguments: dict[str, Any]) -> dict[str, Any logger.info(f"Reconstructed context for thread {continuation_id} (turn {len(context.turns)})") logger.debug(f"[CONVERSATION_DEBUG] Final enhanced arguments keys: {list(enhanced_arguments.keys())}") - # Debug log files in the enhanced arguments for file tracking - if "files" in enhanced_arguments: - logger.debug(f"[CONVERSATION_DEBUG] Final files in enhanced arguments: {enhanced_arguments['files']}") + if "absolute_file_paths" in enhanced_arguments: + logger.debug( + f"[CONVERSATION_DEBUG] Final files in enhanced arguments: {enhanced_arguments['absolute_file_paths']}" + ) # Log to activity file for monitoring try: diff --git a/simulator_tests/test_basic_conversation.py b/simulator_tests/test_basic_conversation.py index b1e0efc..bb2c52c 100644 --- a/simulator_tests/test_basic_conversation.py +++ b/simulator_tests/test_basic_conversation.py @@ -36,7 +36,7 @@ class BasicConversationTest(BaseSimulatorTest): "chat", { "prompt": "Please use low thinking mode. Analyze this Python code and explain what it does", - "files": [self.test_files["python"]], + "absolute_file_paths": [self.test_files["python"]], "model": "flash", }, ) @@ -53,7 +53,7 @@ class BasicConversationTest(BaseSimulatorTest): "chat", { "prompt": "Please use low thinking mode. Now focus on the Calculator class specifically. Are there any improvements you'd suggest?", - "files": [self.test_files["python"]], # Same file - should be deduplicated + "absolute_file_paths": [self.test_files["python"]], # Same file - should be deduplicated "continuation_id": continuation_id, "model": "flash", }, @@ -69,7 +69,7 @@ class BasicConversationTest(BaseSimulatorTest): "chat", { "prompt": "Please use low thinking mode. Now also analyze this configuration file and see how it might relate to the Python code", - "files": [self.test_files["python"], self.test_files["config"]], + "absolute_file_paths": [self.test_files["python"], self.test_files["config"]], "continuation_id": continuation_id, "model": "flash", }, diff --git a/simulator_tests/test_chat_simple_validation.py b/simulator_tests/test_chat_simple_validation.py index 1c0562b..a452d71 100644 --- a/simulator_tests/test_chat_simple_validation.py +++ b/simulator_tests/test_chat_simple_validation.py @@ -159,7 +159,7 @@ class ChatSimpleValidationTest(ConversationBaseTest): "chat", { "prompt": "Please use low thinking mode. Analyze this Python code and tell me what the Calculator class does", - "files": [self.test_files["python"]], + "absolute_file_paths": [self.test_files["python"]], "model": "flash", "thinking_mode": "low", }, @@ -174,7 +174,7 @@ class ChatSimpleValidationTest(ConversationBaseTest): "chat", { "prompt": "Please use low thinking mode. What methods does the Calculator class have?", - "files": [self.test_files["python"]], # Same file + "absolute_file_paths": [self.test_files["python"]], # Same file "continuation_id": continuation_id, "model": "flash", "thinking_mode": "low", @@ -450,7 +450,7 @@ class ChatSimpleValidationTest(ConversationBaseTest): "chat", { "prompt": "Please use low thinking mode. Here are some files for you to analyze", - "files": [self.test_files["python"], self.test_files["config"]], + "absolute_file_paths": [self.test_files["python"], self.test_files["config"]], "model": "flash", "thinking_mode": "low", }, diff --git a/simulator_tests/test_codereview_validation.py b/simulator_tests/test_codereview_validation.py index 963911a..62bab03 100644 --- a/simulator_tests/test_codereview_validation.py +++ b/simulator_tests/test_codereview_validation.py @@ -253,7 +253,7 @@ class ConfigurationManager: "findings": "Initial examination reveals a payment processing class with potential security and performance concerns.", "files_checked": [self.payment_file], "relevant_files": [self.payment_file], - "files": [self.payment_file], # Required for step 1 + "absolute_file_paths": [self.payment_file], # Required for step 1 "review_type": "full", "severity_filter": "all", }, @@ -353,7 +353,7 @@ class ConfigurationManager: "findings": "Initial analysis shows complex configuration class", "files_checked": [self.config_file], "relevant_files": [self.config_file], - "files": [self.config_file], + "absolute_file_paths": [self.config_file], "review_type": "full", }, ) @@ -445,7 +445,7 @@ class ConfigurationManager: "findings": "Found multiple security and performance issues", "files_checked": [self.payment_file], "relevant_files": [self.payment_file], - "files": [self.payment_file], + "absolute_file_paths": [self.payment_file], "relevant_context": ["PaymentProcessor.process_payment"], }, ) @@ -561,7 +561,7 @@ class ConfigurationManager: "findings": "Complete review identified all critical security issues, performance problems, and code quality concerns. All issues are documented with clear severity levels and specific recommendations.", "files_checked": [self.payment_file], "relevant_files": [self.payment_file], - "files": [self.payment_file], + "absolute_file_paths": [self.payment_file], "relevant_context": ["PaymentProcessor.process_payment"], "issues_found": [ {"severity": "critical", "description": "Hardcoded API key security vulnerability"}, @@ -662,7 +662,7 @@ def validate_credit_card(card_number): "findings": "Initial analysis of utility and validation functions", "files_checked": [utils_file, validator_file], "relevant_files": [utils_file], # This should be referenced, not embedded - "files": [utils_file, validator_file], # Required for step 1 + "absolute_file_paths": [utils_file, validator_file], # Required for step 1 "relevant_context": ["calculate_discount"], "confidence": "low", "model": "flash", @@ -770,7 +770,7 @@ def validate_credit_card(card_number): "findings": "Initial review of payment processor and configuration management modules", "files_checked": files_to_review, "relevant_files": [self.payment_file], - "files": files_to_review, + "absolute_file_paths": files_to_review, "relevant_context": [], "confidence": "low", "review_type": "security", diff --git a/simulator_tests/test_consensus_conversation.py b/simulator_tests/test_consensus_conversation.py index 89563f8..f125b51 100644 --- a/simulator_tests/test_consensus_conversation.py +++ b/simulator_tests/test_consensus_conversation.py @@ -56,7 +56,7 @@ class TestConsensusConversation(ConversationBaseTest): "chat", { "prompt": "Please use low thinking mode. I'm working on a web application and need advice on authentication. Can you look at this code?", - "files": [self.test_files["python"]], + "absolute_file_paths": [self.test_files["python"]], "model": "flash", }, ) diff --git a/simulator_tests/test_content_validation.py b/simulator_tests/test_content_validation.py index 10467a1..e435e7f 100644 --- a/simulator_tests/test_content_validation.py +++ b/simulator_tests/test_content_validation.py @@ -68,7 +68,7 @@ DATABASE_CONFIG = { "chat", { "prompt": "Analyze this configuration file briefly", - "files": [validation_file], + "absolute_file_paths": [validation_file], "model": "flash", }, ) @@ -87,7 +87,7 @@ DATABASE_CONFIG = { "chat", { "prompt": "Continue analyzing this configuration file", - "files": [validation_file], # Same file should be deduplicated + "absolute_file_paths": [validation_file], # Same file should be deduplicated "continuation_id": thread_id, "model": "flash", }, diff --git a/simulator_tests/test_conversation_chain_validation.py b/simulator_tests/test_conversation_chain_validation.py index 03313d3..2d70b86 100644 --- a/simulator_tests/test_conversation_chain_validation.py +++ b/simulator_tests/test_conversation_chain_validation.py @@ -72,7 +72,7 @@ class TestClass: "chat", { "prompt": "Analyze this test file and explain what it does.", - "files": [test_file_path], + "absolute_file_paths": [test_file_path], "model": "flash", "temperature": 0.7, }, diff --git a/simulator_tests/test_cross_tool_comprehensive.py b/simulator_tests/test_cross_tool_comprehensive.py index 2c1e588..8389953 100644 --- a/simulator_tests/test_cross_tool_comprehensive.py +++ b/simulator_tests/test_cross_tool_comprehensive.py @@ -84,7 +84,7 @@ def hash_pwd(pwd): self.logger.info(" Step 1: chat tool - Initial codebase exploration") chat_params = { "prompt": "List security issues in auth.py", - "files": [auth_file], + "absolute_file_paths": [auth_file], "thinking_mode": "low", "model": "flash", } @@ -126,7 +126,7 @@ def hash_pwd(pwd): chat_continue_params = { "continuation_id": current_continuation_id, "prompt": "Check config.json too", - "files": [auth_file, config_file_path], # Old + new file + "absolute_file_paths": [auth_file, config_file_path], # Old + new file "thinking_mode": "low", "model": "flash", } diff --git a/simulator_tests/test_cross_tool_continuation.py b/simulator_tests/test_cross_tool_continuation.py index 7d34a87..7feb06e 100644 --- a/simulator_tests/test_cross_tool_continuation.py +++ b/simulator_tests/test_cross_tool_continuation.py @@ -66,7 +66,7 @@ class CrossToolContinuationTest(ConversationBaseTest): "chat", { "prompt": "Please use low thinking mode. Look at this Python code and tell me what you think about it", - "files": [self.test_files["python"]], + "absolute_file_paths": [self.test_files["python"]], "model": "flash", }, ) @@ -198,7 +198,7 @@ class CrossToolContinuationTest(ConversationBaseTest): "chat", { "prompt": "Please use low thinking mode. Analyze both the Python code and configuration file", - "files": [self.test_files["python"], self.test_files["config"]], + "absolute_file_paths": [self.test_files["python"], self.test_files["config"]], "model": "flash", }, ) diff --git a/simulator_tests/test_line_number_validation.py b/simulator_tests/test_line_number_validation.py index ae3e045..a70db2c 100644 --- a/simulator_tests/test_line_number_validation.py +++ b/simulator_tests/test_line_number_validation.py @@ -64,7 +64,7 @@ def validate_data(data): "chat", { "prompt": "Where is tax_rate defined in this file? Please tell me the exact line number.", - "files": [test_file_path], + "absolute_file_paths": [test_file_path], "model": "flash", }, ) @@ -85,7 +85,7 @@ def validate_data(data): "analyze", { "prompt": "What happens between lines 7-11 in this code? Focus on the loop logic.", - "files": [test_file_path], + "absolute_file_paths": [test_file_path], "model": "flash", }, ) @@ -106,7 +106,7 @@ def validate_data(data): "refactor", { "prompt": "Analyze this code for refactoring opportunities", - "files": [test_file_path], + "absolute_file_paths": [test_file_path], "refactor_type": "codesmells", "model": "flash", }, diff --git a/simulator_tests/test_ollama_custom_url.py b/simulator_tests/test_ollama_custom_url.py index 0f4ab62..f23b6ee 100644 --- a/simulator_tests/test_ollama_custom_url.py +++ b/simulator_tests/test_ollama_custom_url.py @@ -105,7 +105,7 @@ if __name__ == "__main__": response2, _ = self.call_mcp_tool( "analyze", { - "files": [ollama_test_file], + "absolute_file_paths": [ollama_test_file], "prompt": "Analyze this Ollama client code. What does this code do and what are its main functions?", "model": "llama3.2", }, diff --git a/simulator_tests/test_prompt_size_limit_bug.py b/simulator_tests/test_prompt_size_limit_bug.py index a158b25..a92398b 100644 --- a/simulator_tests/test_prompt_size_limit_bug.py +++ b/simulator_tests/test_prompt_size_limit_bug.py @@ -81,7 +81,7 @@ protocol View { "chat", { "prompt": initial_prompt, - "files": [test_file_path], + "absolute_file_paths": [test_file_path], "model": "flash", }, ) diff --git a/simulator_tests/test_token_allocation_validation.py b/simulator_tests/test_token_allocation_validation.py index 64c2208..8549354 100644 --- a/simulator_tests/test_token_allocation_validation.py +++ b/simulator_tests/test_token_allocation_validation.py @@ -178,7 +178,7 @@ if __name__ == "__main__": "chat", { "prompt": "Please analyze this math functions file and explain what it does.", - "files": [file1_path], + "absolute_file_paths": [file1_path], "model": "flash", "temperature": 0.7, }, @@ -248,7 +248,7 @@ if __name__ == "__main__": "chat", { "prompt": "Now compare the math functions with this calculator class. How do they differ in approach?", - "files": [file1_path, file2_path], + "absolute_file_paths": [file1_path, file2_path], "continuation_id": continuation_id2, # Continue the conversation from step 2 "model": "flash", "temperature": 0.7, diff --git a/tests/test_auto_mode.py b/tests/test_auto_mode.py index 98341de..3cd775e 100644 --- a/tests/test_auto_mode.py +++ b/tests/test_auto_mode.py @@ -156,7 +156,7 @@ class TestAutoMode: with patch.object(tool, "get_model_provider"): # Execute without model parameter and expect protocol error with pytest.raises(ToolExecutionError) as exc_info: - await tool.execute({"prompt": "Test prompt", "working_directory": str(tmp_path)}) + await tool.execute({"prompt": "Test prompt", "working_directory_absolute_path": str(tmp_path)}) # Should get error payload mentioning model requirement error_payload = getattr(exc_info.value, "payload", str(exc_info.value)) @@ -208,7 +208,7 @@ class TestAutoMode: try: result = await tool.execute( { - "files": ["/tmp/test.py"], + "absolute_file_paths": ["/tmp/test.py"], "prompt": "Analyze this", "model": "nonexistent-model-xyz", # This model definitely doesn't exist } diff --git a/tests/test_auto_mode_comprehensive.py b/tests/test_auto_mode_comprehensive.py index cb326e4..95248a6 100644 --- a/tests/test_auto_mode_comprehensive.py +++ b/tests/test_auto_mode_comprehensive.py @@ -380,7 +380,7 @@ class TestAutoModeComprehensive: await chat_tool.execute( { "prompt": "test", - "working_directory": str(workdir), + "working_directory_absolute_path": str(workdir), # Note: no model parameter provided in auto mode } ) @@ -538,7 +538,7 @@ class TestAutoModeComprehensive: workdir = tmp_path / "chat_artifacts" workdir.mkdir(parents=True, exist_ok=True) result = await chat_tool.execute( - {"prompt": "test", "model": "flash", "working_directory": str(workdir)} + {"prompt": "test", "model": "flash", "working_directory_absolute_path": str(workdir)} ) # Use alias in auto mode # Should succeed with proper model resolution diff --git a/tests/test_chat_codegen_integration.py b/tests/test_chat_codegen_integration.py index 07bb91c..0e96bc3 100644 --- a/tests/test_chat_codegen_integration.py +++ b/tests/test_chat_codegen_integration.py @@ -79,7 +79,7 @@ async def test_chat_codegen_saves_file(monkeypatch, tmp_path): { "prompt": prompt, "model": "gemini-2.5-pro", - "working_directory": str(working_dir), + "working_directory_absolute_path": str(working_dir), } ) diff --git a/tests/test_chat_cross_model_continuation.py b/tests/test_chat_cross_model_continuation.py index ddfdc9d..abdd612 100644 --- a/tests/test_chat_cross_model_continuation.py +++ b/tests/test_chat_cross_model_continuation.py @@ -121,7 +121,7 @@ async def test_chat_cross_model_continuation(monkeypatch, tmp_path): "prompt": "Pick a number between 1 and 10 and respond with JUST that number.", "model": "gemini-2.5-flash", "temperature": 0.2, - "working_directory": working_directory, + "working_directory_absolute_path": working_directory, } step1_result = await chat_tool.execute(step1_args) @@ -186,7 +186,7 @@ async def test_chat_cross_model_continuation(monkeypatch, tmp_path): "model": "gpt-5", "continuation_id": continuation_id, "temperature": 0.2, - "working_directory": working_directory, + "working_directory_absolute_path": working_directory, } step2_result = await chat_tool.execute(step2_args) diff --git a/tests/test_chat_openai_integration.py b/tests/test_chat_openai_integration.py index 89bc2e3..88a0fb7 100644 --- a/tests/test_chat_openai_integration.py +++ b/tests/test_chat_openai_integration.py @@ -68,7 +68,7 @@ async def test_chat_auto_mode_with_openai(monkeypatch, tmp_path): "prompt": "Use chat with gpt5 and ask how far the moon is from earth.", "model": "gpt-5", "temperature": 1.0, - "working_directory": working_directory, + "working_directory_absolute_path": working_directory, } result = await chat_tool.execute(arguments) @@ -135,7 +135,7 @@ async def test_chat_openai_continuation(monkeypatch, tmp_path): "prompt": "In one word, which sells better: iOS app or macOS app?", "model": "gpt-5", "temperature": 1.0, - "working_directory": working_directory, + "working_directory_absolute_path": working_directory, } first_result = await chat_tool.execute(first_args) @@ -156,7 +156,7 @@ async def test_chat_openai_continuation(monkeypatch, tmp_path): "model": "gpt-5", "continuation_id": continuation_id, "temperature": 1.0, - "working_directory": working_directory, + "working_directory_absolute_path": working_directory, } second_result = await chat_tool.execute(second_args) diff --git a/tests/test_chat_simple.py b/tests/test_chat_simple.py index 3ff1564..04dcdb9 100644 --- a/tests/test_chat_simple.py +++ b/tests/test_chat_simple.py @@ -41,34 +41,34 @@ class TestChatTool: # Required fields assert "prompt" in schema["required"] - assert "working_directory" in schema["required"] + assert "working_directory_absolute_path" in schema["required"] # Properties properties = schema["properties"] assert "prompt" in properties - assert "files" in properties + assert "absolute_file_paths" in properties assert "images" in properties - assert "working_directory" in properties + assert "working_directory_absolute_path" in properties def test_request_model_validation(self): """Test that the request model validates correctly""" # Test valid request request_data = { "prompt": "Test prompt", - "files": ["test.txt"], + "absolute_file_paths": ["test.txt"], "images": ["test.png"], "model": "anthropic/claude-opus-4.1", "temperature": 0.7, - "working_directory": "/tmp", # Dummy absolute path + "working_directory_absolute_path": "/tmp", # Dummy absolute path } request = ChatRequest(**request_data) assert request.prompt == "Test prompt" - assert request.files == ["test.txt"] + assert request.absolute_file_paths == ["test.txt"] assert request.images == ["test.png"] assert request.model == "anthropic/claude-opus-4.1" assert request.temperature == 0.7 - assert request.working_directory == "/tmp" + assert request.working_directory_absolute_path == "/tmp" def test_required_fields(self): """Test that required fields are enforced""" @@ -76,7 +76,7 @@ class TestChatTool: from pydantic import ValidationError with pytest.raises(ValidationError): - ChatRequest(model="anthropic/claude-opus-4.1", working_directory="/tmp") + ChatRequest(model="anthropic/claude-opus-4.1", working_directory_absolute_path="/tmp") def test_model_availability(self): """Test that model availability works""" @@ -103,7 +103,11 @@ class TestChatTool: @pytest.mark.asyncio async def test_prompt_preparation(self): """Test that prompt preparation works correctly""" - request = ChatRequest(prompt="Test prompt", files=[], working_directory="/tmp") + request = ChatRequest( + prompt="Test prompt", + absolute_file_paths=[], + working_directory_absolute_path="/tmp", + ) # Mock the system prompt and file handling with patch.object(self.tool, "get_system_prompt", return_value="System prompt"): @@ -120,7 +124,7 @@ class TestChatTool: def test_response_formatting(self): """Test that response formatting works correctly""" response = "Test response content" - request = ChatRequest(prompt="Test", working_directory="/tmp") + request = ChatRequest(prompt="Test", working_directory_absolute_path="/tmp") formatted = self.tool.format_response(response, request) @@ -140,7 +144,7 @@ class TestChatTool: "print('world')" ) - request = ChatRequest(prompt="Test", working_directory=str(tmp_path)) + request = ChatRequest(prompt="Test", working_directory_absolute_path=str(tmp_path)) formatted = tool.format_response(response, request) @@ -164,7 +168,7 @@ class TestChatTool: "Closing thoughts after code." ) - request = ChatRequest(prompt="Test", working_directory=str(tmp_path)) + request = ChatRequest(prompt="Test", working_directory_absolute_path=str(tmp_path)) formatted = tool.format_response(response, request) @@ -183,7 +187,7 @@ class TestChatTool: response = "Intro text\nprint('oops')\nStill ongoing" - request = ChatRequest(prompt="Test", working_directory=str(tmp_path)) + request = ChatRequest(prompt="Test", working_directory_absolute_path=str(tmp_path)) formatted = tool.format_response(response, request) @@ -198,7 +202,7 @@ class TestChatTool: response = "Intro text\n just text" - request = ChatRequest(prompt="Test", working_directory=str(tmp_path)) + request = ChatRequest(prompt="Test", working_directory_absolute_path=str(tmp_path)) formatted = tool.format_response(response, request) @@ -218,7 +222,7 @@ class TestChatTool: "Further analysis and guidance after the generated snippet.\n" ) - request = ChatRequest(prompt="Test", working_directory=str(tmp_path)) + request = ChatRequest(prompt="Test", working_directory_absolute_path=str(tmp_path)) formatted = tool.format_response(response, request) @@ -247,12 +251,12 @@ class TestChatTool: # Test that the tool fields are defined correctly tool_fields = self.tool.get_tool_fields() assert "prompt" in tool_fields - assert "files" in tool_fields + assert "absolute_file_paths" in tool_fields assert "images" in tool_fields required_fields = self.tool.get_required_fields() assert "prompt" in required_fields - assert "working_directory" in required_fields + assert "working_directory_absolute_path" in required_fields class TestChatRequestModel: @@ -265,20 +269,20 @@ class TestChatRequestModel: # Field descriptions should exist and be descriptive assert len(CHAT_FIELD_DESCRIPTIONS["prompt"]) > 50 assert "context" in CHAT_FIELD_DESCRIPTIONS["prompt"] - files_desc = CHAT_FIELD_DESCRIPTIONS["files"].lower() + files_desc = CHAT_FIELD_DESCRIPTIONS["absolute_file_paths"].lower() assert "absolute" in files_desc assert "visual context" in CHAT_FIELD_DESCRIPTIONS["images"] - assert "directory" in CHAT_FIELD_DESCRIPTIONS["working_directory"].lower() + assert "directory" in CHAT_FIELD_DESCRIPTIONS["working_directory_absolute_path"].lower() - def test_working_directory_description_matches_behavior(self): - """Working directory description should reflect automatic creation.""" + def test_working_directory_absolute_path_description_matches_behavior(self): + """Absolute working directory description should reflect existing-directory requirement.""" from tools.chat import CHAT_FIELD_DESCRIPTIONS - description = CHAT_FIELD_DESCRIPTIONS["working_directory"].lower() - assert "must already exist" in description + description = CHAT_FIELD_DESCRIPTIONS["working_directory_absolute_path"].lower() + assert "existing directory" in description @pytest.mark.asyncio - async def test_working_directory_must_exist(self, tmp_path): + async def test_working_directory_absolute_path_must_exist(self, tmp_path): """Chat tool should reject non-existent working directories.""" tool = ChatTool() missing_dir = tmp_path / "nonexistent_subdir" @@ -287,9 +291,9 @@ class TestChatRequestModel: await tool.execute( { "prompt": "test", - "files": [], + "absolute_file_paths": [], "images": [], - "working_directory": str(missing_dir), + "working_directory_absolute_path": str(missing_dir), } ) @@ -299,17 +303,17 @@ class TestChatRequestModel: def test_default_values(self): """Test that default values work correctly""" - request = ChatRequest(prompt="Test", working_directory="/tmp") + request = ChatRequest(prompt="Test", working_directory_absolute_path="/tmp") assert request.prompt == "Test" - assert request.files == [] # Should default to empty list + assert request.absolute_file_paths == [] # Should default to empty list assert request.images == [] # Should default to empty list def test_inheritance(self): """Test that ChatRequest properly inherits from ToolRequest""" from tools.shared.base_models import ToolRequest - request = ChatRequest(prompt="Test", working_directory="/tmp") + request = ChatRequest(prompt="Test", working_directory_absolute_path="/tmp") assert isinstance(request, ToolRequest) # Should have inherited fields diff --git a/tests/test_clink_integration.py b/tests/test_clink_integration.py index 755f169..b863752 100644 --- a/tests/test_clink_integration.py +++ b/tests/test_clink_integration.py @@ -24,7 +24,7 @@ async def test_clink_gemini_single_digit_sum(): "prompt": prompt, "cli_name": "gemini", "role": "default", - "files": [], + "absolute_file_paths": [], "images": [], } ) @@ -56,7 +56,7 @@ async def test_clink_claude_single_digit_sum(): "prompt": prompt, "cli_name": "claude", "role": "default", - "files": [], + "absolute_file_paths": [], "images": [], } ) diff --git a/tests/test_clink_tool.py b/tests/test_clink_tool.py index 2736e5f..5f50295 100644 --- a/tests/test_clink_tool.py +++ b/tests/test_clink_tool.py @@ -37,7 +37,7 @@ async def test_clink_tool_execute(monkeypatch): "prompt": "Summarize the project", "cli_name": "gemini", "role": "default", - "files": [], + "absolute_file_paths": [], "images": [], } @@ -85,7 +85,7 @@ async def test_clink_tool_defaults_to_first_cli(monkeypatch): arguments = { "prompt": "Hello", - "files": [], + "absolute_file_paths": [], "images": [], } @@ -124,7 +124,7 @@ async def test_clink_tool_truncates_large_output(monkeypatch): arguments = { "prompt": "Summarize", "cli_name": tool._default_cli_name, - "files": [], + "absolute_file_paths": [], "images": [], } @@ -165,7 +165,7 @@ async def test_clink_tool_truncates_without_summary(monkeypatch): arguments = { "prompt": "Summarize", "cli_name": tool._default_cli_name, - "files": [], + "absolute_file_paths": [], "images": [], } diff --git a/tests/test_conversation_continuation_integration.py b/tests/test_conversation_continuation_integration.py index 23a623a..fbdd4c2 100644 --- a/tests/test_conversation_continuation_integration.py +++ b/tests/test_conversation_continuation_integration.py @@ -13,7 +13,11 @@ def test_first_response_persisted_in_conversation_history(tmp_path): storage._store.clear() # type: ignore[attr-defined] tool = ChatTool() - request = ChatRequest(prompt="First question?", model="local-llama", working_directory=str(tmp_path)) + request = ChatRequest( + prompt="First question?", + model="local-llama", + working_directory_absolute_path=str(tmp_path), + ) response_text = "Here is the initial answer." # Mimic the first tool invocation (no continuation_id supplied) diff --git a/tests/test_conversation_field_mapping.py b/tests/test_conversation_field_mapping.py index ce80d3a..225912f 100644 --- a/tests/test_conversation_field_mapping.py +++ b/tests/test_conversation_field_mapping.py @@ -70,7 +70,7 @@ async def test_conversation_history_field_mapping(): arguments = { "continuation_id": "test-thread-123", "prompt": test_case["original_value"], - "files": ["/test/file2.py"], + "absolute_file_paths": ["/test/file2.py"], "model": "flash", # Use test model to avoid provider errors } diff --git a/tests/test_conversation_memory.py b/tests/test_conversation_memory.py index d4de674..c1ab41e 100644 --- a/tests/test_conversation_memory.py +++ b/tests/test_conversation_memory.py @@ -32,7 +32,7 @@ class TestConversationMemory: mock_client = Mock() mock_storage.return_value = mock_client - thread_id = create_thread("chat", {"prompt": "Hello", "files": ["/test.py"]}) + thread_id = create_thread("chat", {"prompt": "Hello", "absolute_file_paths": ["/test.py"]}) assert thread_id is not None assert len(thread_id) == 36 # UUID4 length @@ -509,7 +509,7 @@ class TestConversationFlow: mock_storage.return_value = mock_client # Start conversation with files using a simple tool - thread_id = create_thread("chat", {"prompt": "Analyze this codebase", "files": ["/project/src/"]}) + thread_id = create_thread("chat", {"prompt": "Analyze this codebase", "absolute_file_paths": ["/project/src/"]}) # Turn 1: Claude provides context with multiple files initial_context = ThreadContext( @@ -518,7 +518,10 @@ class TestConversationFlow: last_updated_at="2023-01-01T00:00:00Z", tool_name="chat", turns=[], - initial_context={"prompt": "Analyze this codebase", "files": ["/project/src/"]}, + initial_context={ + "prompt": "Analyze this codebase", + "absolute_file_paths": ["/project/src/"], + }, ) mock_client.get.return_value = initial_context.model_dump_json() diff --git a/tests/test_directory_expansion_tracking.py b/tests/test_directory_expansion_tracking.py index 30cd219..f4e56a0 100644 --- a/tests/test_directory_expansion_tracking.py +++ b/tests/test_directory_expansion_tracking.py @@ -63,7 +63,7 @@ def helper_function(): try: yield { "directory": str(temp_dir), - "files": files, + "absolute_file_paths": files, "swift_files": files[:-1], # All but the Python file "python_file": str(python_file), } @@ -84,14 +84,14 @@ def helper_function(): mock_get_provider.return_value = mock_provider directory = temp_directory_with_files["directory"] - expected_files = temp_directory_with_files["files"] + expected_files = temp_directory_with_files["absolute_file_paths"] # Create a request with the directory (not individual files) request_args = { "prompt": "Analyze this codebase structure", - "files": [directory], # Directory path, not individual files + "absolute_file_paths": [directory], # Directory path, not individual files "model": "flash", - "working_directory": directory, + "working_directory_absolute_path": directory, } # Execute the tool @@ -148,10 +148,10 @@ def helper_function(): mock_get_provider.return_value = mock_provider directory = temp_directory_with_files["directory"] - expected_files = temp_directory_with_files["files"] + expected_files = temp_directory_with_files["absolute_file_paths"] # Step 1: Create a conversation thread manually with the expanded files - thread_id = create_thread("chat", {"prompt": "Initial analysis", "files": [directory]}) + thread_id = create_thread("chat", {"prompt": "Initial analysis", "absolute_file_paths": [directory]}) # Add a turn with the expanded files (simulating what the fix should do) success = add_turn( @@ -166,10 +166,10 @@ def helper_function(): # Step 2: Continue the conversation with the same directory continuation_args = { "prompt": "Now focus on the Swift files specifically", - "files": [directory], # Same directory again + "absolute_file_paths": [directory], # Same directory again "model": "flash", "continuation_id": thread_id, - "working_directory": directory, + "working_directory_absolute_path": directory, } # Mock to capture file filtering behavior @@ -217,10 +217,10 @@ def helper_function(): mock_storage.return_value = mock_client directory = temp_directory_with_files["directory"] - expected_files = temp_directory_with_files["files"] + expected_files = temp_directory_with_files["absolute_file_paths"] # Create a thread with expanded files - thread_id = create_thread("chat", {"prompt": "Initial analysis", "files": [directory]}) + thread_id = create_thread("chat", {"prompt": "Initial analysis", "absolute_file_paths": [directory]}) # Add a turn with expanded files success = add_turn( @@ -261,7 +261,7 @@ def helper_function(): python_file = temp_directory_with_files["python_file"] # Create a thread with some expanded files - thread_id = create_thread("chat", {"prompt": "Initial analysis", "files": [directory]}) + thread_id = create_thread("chat", {"prompt": "Initial analysis", "absolute_file_paths": [directory]}) # Add a turn with only some of the files (simulate partial embedding) swift_files = temp_directory_with_files["swift_files"] @@ -294,14 +294,14 @@ def helper_function(): mock_get_provider.return_value = mock_provider directory = temp_directory_with_files["directory"] - expected_files = temp_directory_with_files["files"] + expected_files = temp_directory_with_files["absolute_file_paths"] # Execute the tool request_args = { "prompt": "Analyze this code", - "files": [directory], + "absolute_file_paths": [directory], "model": "flash", - "working_directory": directory, + "working_directory_absolute_path": directory, } result = await tool.execute(request_args) diff --git a/tests/test_image_support_integration.py b/tests/test_image_support_integration.py index 498de7c..53d31d6 100644 --- a/tests/test_image_support_integration.py +++ b/tests/test_image_support_integration.py @@ -283,7 +283,7 @@ class TestImageSupportIntegration: "prompt": "What do you see in this image?", "images": [temp_image_path], "model": "gpt-4o", - "working_directory": working_directory, + "working_directory_absolute_path": working_directory, } ) diff --git a/tests/test_large_prompt_handling.py b/tests/test_large_prompt_handling.py index 9b1a5d1..ac078a6 100644 --- a/tests/test_large_prompt_handling.py +++ b/tests/test_large_prompt_handling.py @@ -60,7 +60,7 @@ class TestLargePromptHandling: temp_dir = tempfile.mkdtemp() try: with pytest.raises(ToolExecutionError) as exc_info: - await tool.execute({"prompt": large_prompt, "working_directory": temp_dir}) + await tool.execute({"prompt": large_prompt, "working_directory_absolute_path": temp_dir}) finally: shutil.rmtree(temp_dir, ignore_errors=True) @@ -83,7 +83,7 @@ class TestLargePromptHandling: try: try: result = await tool.execute( - {"prompt": normal_prompt, "model": "gemini-2.5-flash", "working_directory": temp_dir} + {"prompt": normal_prompt, "model": "gemini-2.5-flash", "working_directory_absolute_path": temp_dir} ) except ToolExecutionError as exc: output = json.loads(exc.payload if hasattr(exc, "payload") else str(exc)) @@ -114,9 +114,9 @@ class TestLargePromptHandling: result = await tool.execute( { "prompt": "", - "files": [temp_prompt_file], + "absolute_file_paths": [temp_prompt_file], "model": "gemini-2.5-flash", - "working_directory": temp_dir, + "working_directory_absolute_path": temp_dir, } ) except ToolExecutionError as exc: @@ -297,8 +297,8 @@ class TestLargePromptHandling: await tool.execute( { "prompt": "Test prompt", - "files": [temp_prompt_file, other_file], - "working_directory": os.path.dirname(temp_prompt_file), + "absolute_file_paths": [temp_prompt_file, other_file], + "working_directory_absolute_path": os.path.dirname(temp_prompt_file), } ) @@ -337,7 +337,7 @@ class TestLargePromptHandling: temp_dir = tempfile.mkdtemp() try: try: - result = await tool.execute({"prompt": exact_prompt, "working_directory": temp_dir}) + result = await tool.execute({"prompt": exact_prompt, "working_directory_absolute_path": temp_dir}) except ToolExecutionError as exc: output = json.loads(exc.payload if hasattr(exc, "payload") else str(exc)) else: @@ -355,7 +355,7 @@ class TestLargePromptHandling: temp_dir = tempfile.mkdtemp() try: try: - result = await tool.execute({"prompt": over_prompt, "working_directory": temp_dir}) + result = await tool.execute({"prompt": over_prompt, "working_directory_absolute_path": temp_dir}) except ToolExecutionError as exc: output = json.loads(exc.payload if hasattr(exc, "payload") else str(exc)) else: @@ -384,7 +384,7 @@ class TestLargePromptHandling: temp_dir = tempfile.mkdtemp() try: try: - result = await tool.execute({"prompt": "", "working_directory": temp_dir}) + result = await tool.execute({"prompt": "", "working_directory_absolute_path": temp_dir}) except ToolExecutionError as exc: output = json.loads(exc.payload if hasattr(exc, "payload") else str(exc)) else: @@ -428,7 +428,9 @@ class TestLargePromptHandling: temp_dir = tempfile.mkdtemp() try: try: - result = await tool.execute({"prompt": "", "files": [bad_file], "working_directory": temp_dir}) + result = await tool.execute( + {"prompt": "", "absolute_file_paths": [bad_file], "working_directory_absolute_path": temp_dir} + ) except ToolExecutionError as exc: output = json.loads(exc.payload if hasattr(exc, "payload") else str(exc)) else: @@ -477,9 +479,9 @@ class TestLargePromptHandling: result = await tool.execute( { "prompt": "Summarize the design decisions", - "files": [str(large_file)], + "absolute_file_paths": [str(large_file)], "model": "flash", - "working_directory": str(tmp_path), + "working_directory_absolute_path": str(tmp_path), "_model_context": dummy_context, } ) @@ -540,7 +542,7 @@ class TestLargePromptHandling: tool.prepare_prompt = mock_prepare_prompt result = await tool.execute( - {"prompt": small_user_prompt, "model": "flash", "working_directory": temp_dir} + {"prompt": small_user_prompt, "model": "flash", "working_directory_absolute_path": temp_dir} ) output = json.loads(result[0].text) @@ -572,7 +574,7 @@ class TestLargePromptHandling: try: try: result = await tool.execute( - {"prompt": large_user_input, "model": "flash", "working_directory": temp_dir} + {"prompt": large_user_input, "model": "flash", "working_directory_absolute_path": temp_dir} ) except ToolExecutionError as exc: output = json.loads(exc.payload if hasattr(exc, "payload") else str(exc)) @@ -590,7 +592,7 @@ class TestLargePromptHandling: { "prompt": small_user_input, "model": "gemini-2.5-flash", - "working_directory": temp_dir, + "working_directory_absolute_path": temp_dir, } ) except ToolExecutionError as exc: @@ -663,7 +665,7 @@ class TestLargePromptHandling: "prompt": f"{huge_conversation_history}\n\n=== CURRENT REQUEST ===\n{small_continuation_prompt}", "model": "flash", "continuation_id": "test_thread_123", - "working_directory": temp_dir, + "working_directory_absolute_path": temp_dir, } # Mock the conversation history embedding to simulate server.py behavior diff --git a/tests/test_mcp_error_handling.py b/tests/test_mcp_error_handling.py index e8267d6..a01dac2 100644 --- a/tests/test_mcp_error_handling.py +++ b/tests/test_mcp_error_handling.py @@ -45,9 +45,9 @@ async def test_tool_execution_error_sets_is_error_flag_for_mcp_response(monkeypa handler = mcp_server.request_handlers[CallToolRequest] arguments = { - "prompt": "Trigger working_directory validation failure", - "working_directory": "relative/path", # Not absolute -> ToolExecutionError from ChatTool - "files": [], + "prompt": "Trigger working_directory_absolute_path validation failure", + "working_directory_absolute_path": "relative/path", # Not absolute -> ToolExecutionError from ChatTool + "absolute_file_paths": [], "model": "gemini-2.5-flash", } diff --git a/tests/test_o3_pro_output_text_fix.py b/tests/test_o3_pro_output_text_fix.py index eeae5f1..a56ca0a 100644 --- a/tests/test_o3_pro_output_text_fix.py +++ b/tests/test_o3_pro_output_text_fix.py @@ -98,7 +98,7 @@ class TestO3ProOutputTextFix: "prompt": "What is 2 + 2?", "model": "o3-pro", "temperature": 1.0, - "working_directory": workdir, + "working_directory_absolute_path": workdir, } return await chat_tool.execute(arguments) diff --git a/tests/test_per_tool_model_defaults.py b/tests/test_per_tool_model_defaults.py index 747a93a..95d4e9a 100644 --- a/tests/test_per_tool_model_defaults.py +++ b/tests/test_per_tool_model_defaults.py @@ -296,7 +296,9 @@ class TestAutoModeErrorMessages: temp_dir = tempfile.mkdtemp() try: with pytest.raises(ToolExecutionError) as exc_info: - await tool.execute({"prompt": "test", "model": "auto", "working_directory": temp_dir}) + await tool.execute( + {"prompt": "test", "model": "auto", "working_directory_absolute_path": temp_dir} + ) finally: shutil.rmtree(temp_dir, ignore_errors=True) @@ -427,7 +429,7 @@ class TestRuntimeModelSelection: try: with pytest.raises(ToolExecutionError) as exc_info: await tool.execute( - {"prompt": "test", "model": "gpt-5-turbo", "working_directory": temp_dir} + {"prompt": "test", "model": "gpt-5-turbo", "working_directory_absolute_path": temp_dir} ) finally: shutil.rmtree(temp_dir, ignore_errors=True) @@ -527,7 +529,7 @@ class TestUnavailableModelFallback: tool = ChatTool() temp_dir = tempfile.mkdtemp() try: - result = await tool.execute({"prompt": "test", "working_directory": temp_dir}) + result = await tool.execute({"prompt": "test", "working_directory_absolute_path": temp_dir}) finally: shutil.rmtree(temp_dir, ignore_errors=True) diff --git a/tests/test_planner.py b/tests/test_planner.py index 081e1d0..b4b8eba 100644 --- a/tests/test_planner.py +++ b/tests/test_planner.py @@ -61,7 +61,7 @@ class TestPlannerTool: # Check that workflow-based planner includes model field and excludes some fields assert "model" in schema["properties"] # Workflow tools include model field assert "images" not in schema["properties"] # Excluded for planning - assert "files" not in schema["properties"] # Excluded for planning + assert "absolute_file_paths" not in schema["properties"] # Excluded for planning assert "temperature" not in schema["properties"] assert "thinking_mode" not in schema["properties"] diff --git a/tests/test_prompt_regression.py b/tests/test_prompt_regression.py index 2296635..956f54e 100644 --- a/tests/test_prompt_regression.py +++ b/tests/test_prompt_regression.py @@ -78,7 +78,7 @@ class TestPromptIntegration: @pytest.mark.integration @pytest.mark.asyncio async def test_chat_with_files(self): - """Test chat tool with files parameter using real API.""" + """Test chat tool with absolute_file_paths parameter using real API.""" skip_if_no_custom_api() tool = ChatTool() @@ -99,7 +99,11 @@ if __name__ == "__main__": try: result = await tool.execute( - {"prompt": "What does this Python code do?", "files": [temp_file], "model": "local-llama"} + { + "prompt": "What does this Python code do?", + "absolute_file_paths": [temp_file], + "model": "local-llama", + } ) assert len(result) == 1 @@ -291,7 +295,7 @@ class UserController: tool = ChatTool() - # Test with no files parameter + # Test with no absolute_file_paths parameter result = await tool.execute({"prompt": "Hello", "model": "local-llama"}) assert len(result) == 1 diff --git a/tests/test_thinking_modes.py b/tests/test_thinking_modes.py index 294854c..395d89e 100644 --- a/tests/test_thinking_modes.py +++ b/tests/test_thinking_modes.py @@ -74,7 +74,7 @@ class TestThinkingModes: try: result = await tool.execute( { - "files": ["/absolute/path/test.py"], + "absolute_file_paths": ["/absolute/path/test.py"], "prompt": "What is this?", "model": "o3-mini", "thinking_mode": "minimal", @@ -155,7 +155,7 @@ class TestThinkingModes: try: result = await tool.execute( { - "files": ["/absolute/path/test.py"], + "absolute_file_paths": ["/absolute/path/test.py"], "thinking_mode": "low", "prompt": "Test code review for validation purposes", "model": "o3-mini", @@ -314,7 +314,7 @@ class TestThinkingModes: try: result = await tool.execute( { - "files": ["/absolute/path/complex.py"], + "absolute_file_paths": ["/absolute/path/complex.py"], "prompt": "Analyze architecture", "thinking_mode": "high", "model": "o3-mini", diff --git a/tests/test_tools.py b/tests/test_tools.py index 89245a7..a361545 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -352,8 +352,8 @@ class TestAbsolutePathValidation: await tool.execute( { "prompt": "Explain this code", - "files": ["code.py"], # relative path without ./ - "working_directory": temp_dir, + "absolute_file_paths": ["code.py"], # relative path without ./ + "working_directory_absolute_path": temp_dir, } ) finally: diff --git a/tools/chat.py b/tools/chat.py index 50f5d9a..3e95f0d 100644 --- a/tools/chat.py +++ b/tools/chat.py @@ -27,15 +27,14 @@ from .simple.base import SimpleTool # Field descriptions matching the original Chat tool exactly CHAT_FIELD_DESCRIPTIONS = { "prompt": ( - "Your question or idea for collaborative thinking. Provide detailed context, including your goal, what you've tried, and any specific challenges. " + "Your question or idea for collaborative thinking to be sent to the external model. Provide detailed context, " + "including your goal, what you've tried, and any specific challenges. " "WARNING: Large inline code must NOT be shared in prompt. Provide full-path to files on disk as separate parameter." ), - "files": ( - "Absolute file or folder paths for code context. Required whenever you reference source code—supply the FULL absolute path (do not shorten)." - ), + "absolute_file_paths": ("Full, absolute file paths to relevant code in order to share with external model"), "images": "Image paths (absolute) or base64 strings for optional visual context.", - "working_directory": ( - "Absolute directory path where generated code artifacts are stored. The directory must already exist." + "working_directory_absolute_path": ( + "Absolute path to an existing directory where generated code artifacts can be saved." ), } @@ -44,9 +43,15 @@ class ChatRequest(ToolRequest): """Request model for Chat tool""" prompt: str = Field(..., description=CHAT_FIELD_DESCRIPTIONS["prompt"]) - files: Optional[list[str]] = Field(default_factory=list, description=CHAT_FIELD_DESCRIPTIONS["files"]) + absolute_file_paths: Optional[list[str]] = Field( + default_factory=list, + description=CHAT_FIELD_DESCRIPTIONS["absolute_file_paths"], + ) images: Optional[list[str]] = Field(default_factory=list, description=CHAT_FIELD_DESCRIPTIONS["images"]) - working_directory: str = Field(..., description=CHAT_FIELD_DESCRIPTIONS["working_directory"]) + working_directory_absolute_path: str = Field( + ..., + description=CHAT_FIELD_DESCRIPTIONS["working_directory_absolute_path"], + ) class ChatTool(SimpleTool): @@ -105,7 +110,7 @@ class ChatTool(SimpleTool): def get_input_schema(self) -> dict[str, Any]: """Generate input schema matching the original Chat tool expectations.""" - required_fields = ["prompt", "working_directory"] + required_fields = ["prompt", "working_directory_absolute_path"] if self.is_effective_auto_mode(): required_fields.append("model") @@ -116,19 +121,19 @@ class ChatTool(SimpleTool): "type": "string", "description": CHAT_FIELD_DESCRIPTIONS["prompt"], }, - "files": { + "absolute_file_paths": { "type": "array", "items": {"type": "string"}, - "description": CHAT_FIELD_DESCRIPTIONS["files"], + "description": CHAT_FIELD_DESCRIPTIONS["absolute_file_paths"], }, "images": { "type": "array", "items": {"type": "string"}, "description": CHAT_FIELD_DESCRIPTIONS["images"], }, - "working_directory": { + "working_directory_absolute_path": { "type": "string", - "description": CHAT_FIELD_DESCRIPTIONS["working_directory"], + "description": CHAT_FIELD_DESCRIPTIONS["working_directory_absolute_path"], }, "model": self.get_model_field_schema(), "temperature": { @@ -161,21 +166,25 @@ class ChatTool(SimpleTool): "type": "string", "description": CHAT_FIELD_DESCRIPTIONS["prompt"], }, - "files": { + "absolute_file_paths": { "type": "array", "items": {"type": "string"}, - "description": CHAT_FIELD_DESCRIPTIONS["files"], + "description": CHAT_FIELD_DESCRIPTIONS["absolute_file_paths"], }, "images": { "type": "array", "items": {"type": "string"}, "description": CHAT_FIELD_DESCRIPTIONS["images"], }, + "working_directory_absolute_path": { + "type": "string", + "description": CHAT_FIELD_DESCRIPTIONS["working_directory_absolute_path"], + }, } def get_required_fields(self) -> list[str]: """Required fields for ChatSimple tool""" - return ["prompt", "working_directory"] + return ["prompt", "working_directory_absolute_path"] # === Hook Method Implementations === @@ -209,17 +218,18 @@ class ChatTool(SimpleTool): if error: return error - working_directory = getattr(request, "working_directory", None) + working_directory = request.working_directory_absolute_path if working_directory: expanded = os.path.expanduser(working_directory) if not os.path.isabs(expanded): return ( - "Error: 'working_directory' must be an absolute path (you may use '~' which will be expanded). " + "Error: 'working_directory_absolute_path' must be an absolute path (you may use '~' which will be expanded). " f"Received: {working_directory}" ) if not os.path.isdir(expanded): return ( - "Error: 'working_directory' must reference an existing directory. " f"Received: {working_directory}" + "Error: 'working_directory_absolute_path' must reference an existing directory. " + f"Received: {working_directory}" ) return None @@ -235,12 +245,13 @@ class ChatTool(SimpleTool): block, remainder, _ = self._extract_generated_code_block(response) if block: sanitized_text = remainder.strip() + target_directory = request.working_directory_absolute_path try: - artifact_path = self._persist_generated_code_block(block, request.working_directory) + artifact_path = self._persist_generated_code_block(block, target_directory) except Exception as exc: # pragma: no cover - rare filesystem failures logger.error("Failed to persist generated code block: %s", exc, exc_info=True) warning = ( - f"WARNING: Unable to write zen_generated.code inside '{request.working_directory}'. " + f"WARNING: Unable to write zen_generated.code inside '{target_directory}'. " "Check the path permissions and re-run. The generated code block is included below for manual handling." ) @@ -326,7 +337,7 @@ class ChatTool(SimpleTool): expanded = os.path.expanduser(working_directory) target_dir = Path(expanded).resolve() if not target_dir.is_dir(): - raise FileNotFoundError(f"Working directory '{working_directory}' does not exist") + raise FileNotFoundError(f"Absolute working directory path '{working_directory}' does not exist") target_file = target_dir / "zen_generated.code" if target_file.exists(): diff --git a/tools/clink.py b/tools/clink.py index 148ab64..2922f47 100644 --- a/tools/clink.py +++ b/tools/clink.py @@ -38,9 +38,9 @@ class CLinkRequest(BaseModel): default=None, description="Optional role preset defined in the CLI configuration (defaults to 'default').", ) - files: list[str] = Field( + absolute_file_paths: list[str] = Field( default_factory=list, - description=COMMON_FIELD_DESCRIPTIONS["files"], + description=COMMON_FIELD_DESCRIPTIONS["absolute_file_paths"], ) images: list[str] = Field( default_factory=list, @@ -140,7 +140,7 @@ class CLinkTool(SimpleTool): "enum": self._all_roles or ["default"], "description": role_description, }, - "files": SchemaBuilder.SIMPLE_FIELD_SCHEMAS["files"], + "absolute_file_paths": SchemaBuilder.SIMPLE_FIELD_SCHEMAS["absolute_file_paths"], "images": SchemaBuilder.COMMON_FIELD_SCHEMAS["images"], "continuation_id": SchemaBuilder.COMMON_FIELD_SCHEMAS["continuation_id"], } @@ -183,7 +183,7 @@ class CLinkTool(SimpleTool): except KeyError as exc: self._raise_tool_error(str(exc)) - files = self.get_request_files(request) + absolute_file_paths = self.get_request_files(request) images = self.get_request_images(request) continuation_id = self.get_request_continuation_id(request) @@ -209,7 +209,7 @@ class CLinkTool(SimpleTool): role=role_config, prompt=prompt_text, system_prompt=system_prompt_text if system_prompt_text.strip() else None, - files=files, + files=absolute_file_paths, images=images, ) except CLIAgentError as exc: diff --git a/tools/planner.py b/tools/planner.py index 927b262..b20a20e 100644 --- a/tools/planner.py +++ b/tools/planner.py @@ -218,7 +218,7 @@ class PlannerTool(WorkflowTool): "temperature", # Planning doesn't need temperature control "thinking_mode", # Planning doesn't need thinking mode "images", # Planning doesn't use images - "files", # Planning doesn't use files + "absolute_file_paths", # Planning doesn't use file attachments ] # Build schema with proper field exclusion (following consensus pattern) diff --git a/tools/shared/base_models.py b/tools/shared/base_models.py index 128df2c..208ca74 100644 --- a/tools/shared/base_models.py +++ b/tools/shared/base_models.py @@ -29,7 +29,7 @@ COMMON_FIELD_DESCRIPTIONS = { "files, and findings so the agent can resume seamlessly." ), "images": "Optional absolute image paths or base64 blobs for visual context.", - "files": "Optional absolute file or folder paths (do not shorten).", + "absolute_file_paths": "Full paths to relevant code", } # Workflow-specific field descriptions diff --git a/tools/shared/base_tool.py b/tools/shared/base_tool.py index 91ac01e..d093598 100644 --- a/tools/shared/base_tool.py +++ b/tools/shared/base_tool.py @@ -667,7 +667,7 @@ class BaseTool(ABC): """ # Only validate files/paths if they exist in the request file_fields = [ - "files", + "absolute_file_paths", "file", "path", "directory", @@ -885,7 +885,7 @@ class BaseTool(ABC): def handle_prompt_file(self, files: Optional[list[str]]) -> tuple[Optional[str], Optional[list[str]]]: """ - Check for and handle prompt.txt in the files list. + Check for and handle prompt.txt in the absolute file paths list. If prompt.txt is found, reads its content and removes it from the files list. This file is treated specially as the main prompt, not as an embedded file. @@ -895,7 +895,7 @@ class BaseTool(ABC): mechanism to bypass token constraints while preserving response capacity. Args: - files: List of file paths (will be translated for current environment) + files: List of absolute file paths (will be translated for current environment) Returns: tuple: (prompt_content, updated_files_list) @@ -983,7 +983,7 @@ class BaseTool(ABC): f"MANDATORY ACTION REQUIRED: The prompt is too large for MCP's token limits (>{MCP_PROMPT_SIZE_LIMIT:,} characters). " "YOU MUST IMMEDIATELY save the prompt text to a temporary file named 'prompt.txt' in the working directory. " "DO NOT attempt to shorten or modify the prompt. SAVE IT AS-IS to 'prompt.txt'. " - "Then resend the request with the absolute file path to 'prompt.txt' in the files parameter (must be FULL absolute path - DO NOT SHORTEN), " + "Then resend the request, passing the absolute file path to 'prompt.txt' as part of the tool call, " "along with any other files you wish to share as context. Leave the prompt text itself empty or very brief in the new request. " "This is the ONLY way to handle large prompts - you MUST follow these exact steps." ), @@ -991,7 +991,7 @@ class BaseTool(ABC): "metadata": { "prompt_size": len(text), "limit": MCP_PROMPT_SIZE_LIMIT, - "instructions": "MANDATORY: Save prompt to 'prompt.txt' in current folder and include absolute path in files parameter. DO NOT modify or shorten the prompt.", + "instructions": "MANDATORY: Save prompt to 'prompt.txt' in current folder and provide full path when recalling this tool.", }, } return None diff --git a/tools/shared/schema_builders.py b/tools/shared/schema_builders.py index dd0146c..baed420 100644 --- a/tools/shared/schema_builders.py +++ b/tools/shared/schema_builders.py @@ -45,10 +45,10 @@ class SchemaBuilder: # Simple tool-specific field schemas (workflow tools use relevant_files instead) SIMPLE_FIELD_SCHEMAS = { - "files": { + "absolute_file_paths": { "type": "array", "items": {"type": "string"}, - "description": COMMON_FIELD_DESCRIPTIONS["files"], + "description": COMMON_FIELD_DESCRIPTIONS["absolute_file_paths"], }, } diff --git a/tools/simple/base.py b/tools/simple/base.py index 33a6697..2c6ed6b 100644 --- a/tools/simple/base.py +++ b/tools/simple/base.py @@ -3,7 +3,7 @@ Base class for simple MCP tools. Simple tools follow a straightforward pattern: 1. Receive request -2. Prepare prompt (with files, context, etc.) +2. Prepare prompt (with absolute file paths, context, etc.) 3. Call AI model 4. Format and return response @@ -50,7 +50,7 @@ class SimpleTool(BaseTool): "type": "string", "description": "Your question or idea...", }, - "files": SimpleTool.FILES_FIELD, + "absolute_file_paths": SimpleTool.FILES_FIELD, } def get_required_fields(self) -> List[str]: @@ -58,7 +58,7 @@ class SimpleTool(BaseTool): """ # Common field definitions that simple tools can reuse - FILES_FIELD = SchemaBuilder.SIMPLE_FIELD_SCHEMAS["files"] + FILES_FIELD = SchemaBuilder.SIMPLE_FIELD_SCHEMAS["absolute_file_paths"] IMAGES_FIELD = SchemaBuilder.COMMON_FIELD_SCHEMAS["images"] @abstractmethod @@ -79,7 +79,7 @@ class SimpleTool(BaseTool): "type": "string", "description": "The user's question or request", }, - "files": SimpleTool.FILES_FIELD, # Reuse common field + "absolute_file_paths": SimpleTool.FILES_FIELD, # Reuse common field "max_tokens": { "type": "integer", "minimum": 1, @@ -230,11 +230,14 @@ class SimpleTool(BaseTool): return None def get_request_files(self, request) -> list: - """Get files from request. Override for custom file handling.""" + """Get absolute file paths from request. Override for custom file handling.""" try: - return request.files if request.files is not None else [] + files = request.absolute_file_paths except AttributeError: + files = None + if files is None: return [] + return files def get_request_as_dict(self, request) -> dict: """Convert request to dictionary. Override for custom serialization.""" @@ -250,11 +253,10 @@ class SimpleTool(BaseTool): return {"prompt": self.get_request_prompt(request)} def set_request_files(self, request, files: list) -> None: - """Set files on request. Override for custom file setting.""" + """Set absolute file paths on request. Override for custom file setting.""" try: - request.files = files + request.absolute_file_paths = files except AttributeError: - # If request doesn't support file setting, ignore silently pass def get_actually_processed_files(self) -> list: @@ -882,7 +884,7 @@ Please provide a thoughtful, comprehensive response:""" Raises: ValueError: If prompt is too large for MCP transport """ - # Check for prompt.txt in files + # Check for prompt.txt in provided absolute file paths files = self.get_request_files(request) if files: prompt_content, updated_files = self.handle_prompt_file(files) @@ -950,7 +952,7 @@ Please provide a thoughtful, comprehensive response:""" """ import os - # Check if request has 'files' attribute (used by most tools) + # Check if request has absolute file paths attribute (legacy tools may still provide 'files') files = self.get_request_files(request) if files: for file_path in files: diff --git a/tools/tracer.py b/tools/tracer.py index 8c58822..5784aeb 100644 --- a/tools/tracer.py +++ b/tools/tracer.py @@ -227,7 +227,7 @@ class TracerTool(WorkflowTool): excluded_common_fields = [ "temperature", # Tracing doesn't need temperature control "thinking_mode", # Tracing doesn't need thinking mode - "files", # Tracing uses relevant_files instead + "absolute_file_paths", # Tracing uses relevant_files instead ] return WorkflowSchemaBuilder.build_schema(