diff options
Diffstat (limited to 'packages/multillm-claude')
| -rw-r--r-- | packages/multillm-claude/README.md | 188 | ||||
| -rw-r--r-- | packages/multillm-claude/src/multillm_claude/provider.py | 97 |
2 files changed, 277 insertions, 8 deletions
diff --git a/packages/multillm-claude/README.md b/packages/multillm-claude/README.md index a2e242b..e5a47ad 100644 --- a/packages/multillm-claude/README.md +++ b/packages/multillm-claude/README.md @@ -112,6 +112,128 @@ Specify the model after the provider prefix: - `claude/claude-sonnet-4-20250514` - Claude Sonnet - `claude/claude-opus-4-20250514` - Claude Opus +## Tool Support + +### Supported Tools + +These tools work correctly with the Claude Agent SDK provider: + +| Tool | Description | Permission Required | +|------|-------------|---------------------| +| `Bash` | Execute bash commands | Yes | +| `Read` | Read files from filesystem | No | +| `Write` | Create or overwrite files | Yes | +| `Edit` | Edit existing files | Yes | +| `Glob` | Find files by pattern | No | +| `Grep` | Search file contents | No | +| `Task` | Launch sub-agents | Varies | +| `WebFetch` | Fetch web content | No | +| `WebSearch` | Search the web | No | +| `NotebookEdit` | Edit Jupyter notebooks | Yes | +| `KillShell` | Kill background shells | Yes | +| `EnterPlanMode` | Enter planning mode | No | +| `ExitPlanMode` | Exit planning mode | No | + +### Interactive Tools + +| Tool | CLI Support | Notes | +|------|-------------|-------| +| `AskUserQuestion` | ✅ **Auto-converts** | CLI automatically uses custom `ask_user` tool | +| Custom tools | ✅ Full support | Provide Tool objects with interactive handlers | + +**About Interactive Tools with Claude:** + +The Claude Agent SDK's built-in `AskUserQuestion` runs in a subprocess and can't access our terminal's stdin/stdout for interactive prompting. To solve this, **multillm-cli automatically provides a custom interactive tool** when you request `AskUserQuestion`. + +**What happens:** + +When you use `--allowed-tools AskUserQuestion`, the CLI: +1. Removes the built-in AskUserQuestion (which doesn't work interactively) +2. Adds a custom `ask_user` tool with an interactive handler +3. The agent uses this tool instead, with full interactive support + +**Usage:** + +```bash +# Request AskUserQuestion - CLI auto-provides working alternative +multillm -m claude/default \ + -p "Ask me about my preferences and create a summary" \ + --allowed-tools AskUserQuestion \ + --permission-mode acceptEdits +``` + +**What you'll see:** +``` +ℹ️ Using custom 'ask_user' tool instead of AskUserQuestion for interactive prompting + +====================================================================== +❓ QUESTION FROM ASSISTANT +====================================================================== + +What is your favorite programming language? + +Suggested options: + 1. Python + 2. JavaScript + 3. Rust + +Your answer: 1 +====================================================================== +``` + +**Programmatic usage:** + +For programmatic use, provide your own `ask_user` tool with an interactive handler: + +```python +import multillm + +# Define interactive ask_user tool +ask_user_tool = multillm.Tool( + name="ask_user", + description="Ask the user a question", + parameters={ + "type": "object", + "properties": { + "question": {"type": "string"}, + "options": {"type": "array", "items": {"type": "string"}} + }, + "required": ["question"] + }, + handler=lambda args: { + "answer": input(f"\n{args['question']}\nYour answer: ") + } +) + +# Use with Claude +async for msg in client.run( + "claude/default", + "Ask me questions", + options=multillm.AgentOptions(max_turns=10), + tools=[ask_user_tool] # Provide custom tool +): + if msg.type == "text": + print(msg.content) +``` + +**Why not use the built-in AskUserQuestion?** + +The SDK's built-in `AskUserQuestion` is designed for Claude Code CLI's interactive mode where the subprocess has special stdin/stdout handling. In multillm, this doesn't work because: +- The SDK subprocess can't access our terminal +- We can't intercept and respond to built-in tool calls +- The tool returns an error instead of prompting + +**Solution:** Use custom tools (which the CLI provides automatically!) + +**Comparison:** + +| Approach | Works? | How | +|----------|--------|-----| +| `--allowed-tools AskUserQuestion` | ✅ Yes | CLI auto-converts to custom tool | +| Custom `ask_user` Tool | ✅ Yes | Provide Tool object with handler | +| SDK built-in (direct) | ❌ No | Subprocess can't access stdin/stdout | +| Chat provider `ask_user` | ✅ Yes | Via agentwrap | + ## Agent Options ```python @@ -135,3 +257,69 @@ When streaming with `client.run()`: | `tool_result` | Result from a tool | | `result` | Final result | | `system` | System messages | + +## Debugging + +### Enable Debug Mode + +Set the `MULTILLM_DEBUG` environment variable to see detailed error information and SDK options: + +```bash +export MULTILLM_DEBUG=1 +python your_script.py +``` + +This will show: +- Detailed error messages with full stderr/stdout +- SDK configuration options +- Full Python tracebacks + +### Common Issues + +**Error: "Command failed with exit code 1"** + +The provider now captures and displays all available error information to stderr. Look for: + +``` +====================================================================== +CLAUDE AGENT SDK ERROR +====================================================================== +Error: <detailed error message> + +Error Details: + stderr: <actual error from subprocess> + stdout: <subprocess output> + exit_code: 1 +====================================================================== +``` + +**Authentication Errors:** +```bash +# Check authentication status +claude login --check + +# Re-authenticate if needed +claude login +``` + +**Permission Errors:** + +Always specify `permission_mode` when using tools: + +```python +result = await client.single( + "claude/default", + "List files", + allowed_tools=["Bash"], + permission_mode="acceptEdits" # Required! +) +``` + +### Full Debugging Guide + +See [DEBUGGING.md](./DEBUGGING.md) for comprehensive debugging information, including: +- How to read error messages +- Common issues and solutions +- Debug mode usage +- Testing error handling +- Reporting bugs diff --git a/packages/multillm-claude/src/multillm_claude/provider.py b/packages/multillm-claude/src/multillm_claude/provider.py index 051f845..f74564e 100644 --- a/packages/multillm-claude/src/multillm_claude/provider.py +++ b/packages/multillm-claude/src/multillm_claude/provider.py @@ -1,4 +1,5 @@ import os +import sys from typing import Any, AsyncIterator from claude_agent_sdk import ( @@ -67,6 +68,17 @@ class ClaudeAgentProvider(BaseAgentProvider): if merged_config.get("api_key"): env["ANTHROPIC_API_KEY"] = merged_config["api_key"] + # Enable debug mode if requested + if os.environ.get("MULTILLM_DEBUG") or os.environ.get("DEBUG"): + env["DEBUG"] = "1" + print(f"[DEBUG] Claude Agent SDK options:", file=sys.stderr) + print(f" System prompt: {options.system_prompt if options else None}", file=sys.stderr) + print(f" Max turns: {options.max_turns if options else None}", file=sys.stderr) + print(f" Allowed tools: {options.allowed_tools if options else None}", file=sys.stderr) + print(f" Permission mode: {options.permission_mode if options else None}", file=sys.stderr) + print(f" Working dir: {options.working_directory if options else None}", file=sys.stderr) + print(f" Environment vars: {list(env.keys())}", file=sys.stderr) + if options is None: return ClaudeAgentOptions(env=env) if env else ClaudeAgentOptions() @@ -170,16 +182,85 @@ class ClaudeAgentProvider(BaseAgentProvider): yield parsed except ProcessError as e: - error_msg = f"Claude Agent SDK process error: {e}" - if hasattr(e, 'stderr') and e.stderr: - error_msg += f"\nStderr: {e.stderr}" - if hasattr(e, 'stdout') and e.stdout: - error_msg += f"\nStdout: {e.stdout}" - raise ProviderError(error_msg) from e + # Build detailed error message + error_parts = [f"Claude Agent SDK process error: {e}"] + + # Print to stderr immediately so user sees it + print(f"\n{'='*70}", file=sys.stderr) + print("CLAUDE AGENT SDK ERROR", file=sys.stderr) + print(f"{'='*70}", file=sys.stderr) + print(f"Error: {e}", file=sys.stderr) + + # Collect all available error information + error_info = {} + for attr in ['stderr', 'stdout', 'exit_code', 'command', 'output', 'message', 'args']: + if hasattr(e, attr): + val = getattr(e, attr) + if val: + error_info[attr] = val + + # Print all error details to stderr + if error_info: + print("\nError Details:", file=sys.stderr) + for key, val in error_info.items(): + print(f" {key}: {val}", file=sys.stderr) + # Also add to error message + error_parts.append(f"{key}: {val}") + + # Check exception's __dict__ for any other attributes + if hasattr(e, '__dict__'): + other_attrs = {k: v for k, v in e.__dict__.items() if k not in error_info and not k.startswith('_')} + if other_attrs: + print("\nAdditional Info:", file=sys.stderr) + for key, val in other_attrs.items(): + print(f" {key}: {val}", file=sys.stderr) + error_parts.append(f"{key}: {val}") + + print(f"{'='*70}\n", file=sys.stderr) + + raise ProviderError("\n".join(error_parts)) from e + except ClaudeSDKError as e: - raise ProviderError(f"Claude Agent SDK error: {e}") from e + # Print to stderr immediately + print(f"\n{'='*70}", file=sys.stderr) + print("CLAUDE SDK ERROR", file=sys.stderr) + print(f"{'='*70}", file=sys.stderr) + print(f"Error: {e}", file=sys.stderr) + + # Get all attributes from the error + error_parts = [f"Claude Agent SDK error: {e}"] + if hasattr(e, '__dict__'): + for key, val in e.__dict__.items(): + if not key.startswith('_') and val: + print(f" {key}: {val}", file=sys.stderr) + error_parts.append(f"{key}: {val}") + + print(f"{'='*70}\n", file=sys.stderr) + + raise ProviderError("\n".join(error_parts)) from e + except Exception as e: - raise ProviderError(f"Unexpected error: {e}") from e + # Print unexpected errors to stderr + print(f"\n{'='*70}", file=sys.stderr) + print("UNEXPECTED ERROR IN CLAUDE PROVIDER", file=sys.stderr) + print(f"{'='*70}", file=sys.stderr) + print(f"Type: {type(e).__name__}", file=sys.stderr) + print(f"Error: {e}", file=sys.stderr) + + if hasattr(e, '__dict__'): + print("\nError attributes:", file=sys.stderr) + for key, val in e.__dict__.items(): + if not key.startswith('_'): + print(f" {key}: {val}", file=sys.stderr) + + # Print full traceback + import traceback + print("\nTraceback:", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + + print(f"{'='*70}\n", file=sys.stderr) + + raise ProviderError(f"Unexpected error: {type(e).__name__}: {e}") from e async def run_interactive( self, |
