aboutsummaryrefslogtreecommitdiffstats
path: root/packages/multillm-claude
diff options
context:
space:
mode:
authorLouis Burda <dev@sinitax.com>2026-02-02 08:10:56 +0100
committerLouis Burda <dev@sinitax.com>2026-02-02 08:11:17 +0100
commitd69c5b355c450e2c79b62b8a1a7946f375ac207d (patch)
treea20cc4b977e400b2cd08b25f5ea9581156524356 /packages/multillm-claude
parent43ddca6e4de9ed2b8615dedd9a31ee42881fdcb5 (diff)
downloadmultillm-main.tar.gz
multillm-main.zip
Add agentwrap provider and allow tools for singleHEADmain
Diffstat (limited to 'packages/multillm-claude')
-rw-r--r--packages/multillm-claude/README.md188
-rw-r--r--packages/multillm-claude/src/multillm_claude/provider.py97
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,