1045 lines
29 KiB
Markdown
1045 lines
29 KiB
Markdown
# ClaudeCode - DSPy Module for Claude Code SDK
|
||
|
||
A DSPy module that wraps the Claude Code Python SDK with a signature-driven interface. Each agent instance maintains a stateful conversation session, making it perfect for multi-turn agentic workflows.
|
||
|
||
Reference to the original Claude Agent Python SDK: [Claude Agent Python SDK](https://platform.claude.com/docs/en/agent-sdk/python)
|
||
|
||
## Features
|
||
|
||
- **Signature-driven** - Use DSPy signatures for type safety and clarity
|
||
- **Stateful sessions** - Each agent instance = one conversation session
|
||
- **Smart schema handling** - Automatically handles str vs Pydantic outputs
|
||
- **Rich outputs** - Get typed results + execution trace + token usage
|
||
- **Multi-turn conversations** - Context preserved across calls
|
||
- **Enhanced prompts** - Automatically includes signature docstrings + InputField/OutputField descriptions for better context
|
||
- **Async support** - Both sync and async execution modes
|
||
- **Modaic Hub Integration** - Push and pull agents from Modaic Hub
|
||
|
||
|
||
|
||
**Prerequisites:**
|
||
- Python 3.10+
|
||
- Anthropic API key set in `ANTHROPIC_API_KEY` environment variable
|
||
|
||
**Note:** The Claude Code CLI is automatically bundled with the `claude-agent-sdk` package - no separate installation required! The SDK uses the bundled CLI by default. If you prefer to use a system-wide installation or a specific version:
|
||
- Install separately: `curl -fsSL https://claude.ai/install.sh | bash`
|
||
- Specify custom path: `ClaudeAgentOptions(cli_path="/path/to/claude")`
|
||
|
||
## Quick Start with Modaic Hub (Recommended)
|
||
|
||
The fastest way to use ClaudeCode is to pull a pre-configured agent from Modaic Hub.
|
||
|
||
### 0. Installation
|
||
|
||
```bash
|
||
# install with uv
|
||
uv add claude-agent-sdk dspy modaic
|
||
```
|
||
|
||
### 1. Set up environment
|
||
|
||
```bash
|
||
# copy the example file
|
||
cp .env.example .env
|
||
|
||
# edit .env with your keys
|
||
ANTHROPIC_API_KEY="<YOUR_ANTHROPIC_API_KEY>"
|
||
MODAIC_TOKEN="<YOUR_MODAIC_TOKEN>" # Optional, for pushing to hub
|
||
```
|
||
|
||
### 2. Load from Modaic Hub
|
||
|
||
```python
|
||
from modaic import AutoProgram
|
||
from pydantic import BaseModel
|
||
import dspy
|
||
|
||
class FileList(BaseModel):
|
||
files: list[str]
|
||
|
||
class FileSignature(dspy.Signature):
|
||
message: str = dspy.InputField(desc="Request to process")
|
||
output: FileList = dspy.OutputField(desc="List of files")
|
||
|
||
# Load pre-compiled agent from hub
|
||
# Note: Only model is set via config, other params are kwargs
|
||
agent = AutoProgram.from_precompiled(
|
||
"farouk1/claude-code",
|
||
signature=FileSignature,
|
||
working_directory=".",
|
||
)
|
||
|
||
# Use it!
|
||
result = agent(message="List Python files here")
|
||
print(result.output.files) # Typed access
|
||
print(result.usage) # Token usage
|
||
```
|
||
|
||
### 3. Override Config Options
|
||
|
||
```python
|
||
from modaic import AutoProgram
|
||
import dspy
|
||
|
||
class MySignature(dspy.Signature):
|
||
message: str = dspy.InputField(desc="Request to process")
|
||
answer: str = dspy.OutputField(desc="Response")
|
||
|
||
# Load with custom configuration
|
||
# Model comes from config, other params are kwargs
|
||
agent = AutoProgram.from_precompiled(
|
||
"farouk1/claude-code",
|
||
config={"model": "claude-opus-4-5-20251101"},
|
||
signature=MySignature,
|
||
working_directory=".",
|
||
permission_mode="acceptEdits",
|
||
allowed_tools=["Read", "Write", "Bash"],
|
||
)
|
||
```
|
||
|
||
## Local Development
|
||
|
||
For local development and hacking on this project:
|
||
|
||
### Basic String Output
|
||
|
||
```python
|
||
from claude_dspy import ClaudeCode, ClaudeCodeConfig
|
||
|
||
# create config
|
||
config = ClaudeCodeConfig()
|
||
|
||
# create agent
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory="."
|
||
)
|
||
|
||
# use it
|
||
result = agent(message="What files are in this directory?")
|
||
print(result.answer) # String response
|
||
print(result.trace) # Execution items
|
||
print(result.usage) # Token counts
|
||
```
|
||
|
||
### Structured Output with Pydantic
|
||
|
||
```python
|
||
from claude_dspy import ClaudeCode, ClaudeCodeConfig
|
||
from pydantic import BaseModel, Field
|
||
import dspy
|
||
|
||
class BugReport(BaseModel):
|
||
severity: str = Field(description="critical, high, medium, or low")
|
||
description: str
|
||
affected_files: list[str]
|
||
|
||
# Create config with Pydantic output
|
||
config = ClaudeCodeConfig()
|
||
|
||
# Option 1: Pre-construct signature in your module (where BugReport is defined)
|
||
sig = dspy.Signature("message:str -> report:BugReport")
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature=sig,
|
||
working_directory="."
|
||
)
|
||
|
||
# Option 2: Use class-based signature (recommended)
|
||
class BugReportSignature(dspy.Signature):
|
||
"""Analyze bugs and generate report."""
|
||
message: str = dspy.InputField()
|
||
report: BugReport = dspy.OutputField()
|
||
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature=BugReportSignature,
|
||
working_directory="."
|
||
)
|
||
|
||
result = agent(message="Analyze the bug in error.log")
|
||
print(result.report.severity) # Typed access!
|
||
print(result.report.affected_files)
|
||
```
|
||
|
||
**Note**: String signatures like `"message:str -> report:BugReport"` only work with built-in types unless you use `dspy.Signature()`. For custom Pydantic models, either:
|
||
- Use `dspy.Signature("...")`
|
||
- Use class-based signatures (recommended)
|
||
|
||
### Push to Modaic Hub
|
||
|
||
```python
|
||
from claude_dspy import ClaudeCode, ClaudeCodeConfig
|
||
|
||
# create your agent
|
||
config = ClaudeCodeConfig(model="claude-opus-4-5-20251101")
|
||
|
||
agent = ClaudeCode(config)
|
||
|
||
# push to Modaic Hub
|
||
agent.push_to_hub("{USERNAME}/your-agent-name")
|
||
```
|
||
|
||
## API Reference
|
||
|
||
### ClaudeCodeConfig
|
||
|
||
Configuration object for ClaudeCode agents.
|
||
|
||
```python
|
||
class ClaudeCodeConfig:
|
||
def __init__(
|
||
self,
|
||
model: str = "claude-opus-4-5-20251101", # Default model
|
||
)
|
||
```
|
||
|
||
**Parameters:**
|
||
|
||
- **`model`** - Claude model to use (default: `"claude-opus-4-5-20251101"`)
|
||
|
||
**Important:** `ClaudeCodeConfig` only contains the model. All other parameters (`signature`, `working_directory`, `permission_mode`, `allowed_tools`, etc.) are passed as keyword arguments to `ClaudeCode.__init__()` or `AutoProgram.from_precompiled()`, not through the config object.
|
||
|
||
### ClaudeCode
|
||
|
||
Main agent class.
|
||
|
||
```python
|
||
class ClaudeCode(PrecompiledProgram):
|
||
def __init__(
|
||
self,
|
||
config: ClaudeCodeConfig,
|
||
**kwargs,
|
||
)
|
||
```
|
||
|
||
**Parameters:**
|
||
|
||
**Core Configuration:**
|
||
- **`config`** - ClaudeCodeConfig instance with model configuration
|
||
- **`signature`** (required) - DSPy signature defining input/output fields (must have exactly 1 input and 1 output)
|
||
- **`working_directory`** - Directory where Claude will execute commands (default: `"."`)
|
||
- **`permission_mode`** - Permission mode: `"default"`, `"acceptEdits"`, `"plan"`, `"bypassPermissions"`
|
||
- **`allowed_tools`** - List of allowed tool names (e.g., `["Read", "Write", "Bash", "Glob", "Grep"]`). See [Available Tools](#available-tools) section for complete list.
|
||
- **`disallowed_tools`** - List of disallowed tool names
|
||
- **`sandbox`** - Sandbox configuration dict. See [SDK docs](https://platform.claude.com/docs/en/agent-sdk/python#sandboxsettings) for details.
|
||
- **`system_prompt`** - Custom system prompt or preset config
|
||
- **`api_key`** - Anthropic API key (falls back to `ANTHROPIC_API_KEY` env var)
|
||
|
||
**MCP Servers:**
|
||
- **`mcp_servers`** - MCP server configurations for custom tools. See [MCP section](#using-mcp-servers) below.
|
||
|
||
**Session Management:**
|
||
- **`continue_conversation`** - Continue the most recent conversation (default: `False`)
|
||
- **`resume`** - Session ID to resume from a previous session
|
||
- **`max_turns`** - Maximum number of conversation turns
|
||
- **`fork_session`** - Fork to a new session when resuming (default: `False`)
|
||
|
||
**Advanced Options:**
|
||
- **`permission_prompt_tool_name`** - MCP tool name for permission prompts
|
||
- **`settings`** - Path to custom settings file
|
||
- **`add_dirs`** - Additional directories Claude can access
|
||
- **`env`** - Environment variables to pass to Claude Code
|
||
- **`extra_args`** - Additional CLI arguments
|
||
- **`max_buffer_size`** - Maximum bytes when buffering CLI stdout
|
||
- **`cli_path`** - Custom path to Claude Code CLI executable
|
||
|
||
**Callbacks and Hooks:**
|
||
- **`stderr`** - Callback function for stderr output: `Callable[[str], None]`
|
||
- **`can_use_tool`** - Permission callback for tool usage control
|
||
- **`hooks`** - Hook configurations for intercepting events. See [SDK docs](https://platform.claude.com/docs/en/agent-sdk/python#hook-types) for details.
|
||
|
||
**User and Settings:**
|
||
- **`user`** - User identifier
|
||
- **`include_partial_messages`** - Include partial message streaming events (default: `False`)
|
||
- **`setting_sources`** - Which settings to load: `["user", "project", "local"]`
|
||
|
||
**Subagents and Plugins:**
|
||
- **`agents`** - Programmatically defined subagents
|
||
- **`plugins`** - Custom plugins to load
|
||
|
||
#### Methods
|
||
|
||
##### `__call__(**kwargs) -> Prediction` (or `forward`)
|
||
|
||
Execute the agent with an input message.
|
||
|
||
**Arguments:**
|
||
- `**kwargs` - Must contain the input field specified in signature
|
||
|
||
**Returns:**
|
||
- `Prediction` object with:
|
||
- **Typed output field** - Named according to signature (e.g., `result.answer`)
|
||
- **`trace`** - `list[TraceItem]` - Execution trace
|
||
- **`usage`** - `Usage` - Token usage statistics
|
||
|
||
**Example:**
|
||
```python
|
||
config = ClaudeCodeConfig()
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory="."
|
||
)
|
||
|
||
result = agent(message="Hello")
|
||
print(result.answer) # Access typed output
|
||
print(result.trace) # List of execution items
|
||
print(result.usage) # Token usage stats
|
||
```
|
||
|
||
##### `push_to_hub(repo_id: str) -> None`
|
||
|
||
Push the agent to Modaic Hub.
|
||
|
||
**Arguments:**
|
||
- `repo_id` - Repository ID in format "username/repo-name"
|
||
|
||
**Example:**
|
||
```python
|
||
agent.push_to_hub("{USERNAME}/your-agent")
|
||
```
|
||
|
||
##### `aforward(**kwargs) -> Prediction`
|
||
|
||
Async version of `__call__()` for use in async contexts.
|
||
|
||
**Example:**
|
||
```python
|
||
async def main():
|
||
config = ClaudeCodeConfig()
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory="."
|
||
)
|
||
result = await agent.aforward(message="Hello")
|
||
print(result.answer)
|
||
```
|
||
|
||
#### Properties
|
||
|
||
##### `session_id: Optional[str]`
|
||
|
||
Get the session ID for this agent instance.
|
||
|
||
- Returns `None` until first call
|
||
- Persists across multiple calls
|
||
- Useful for debugging and logging
|
||
|
||
**Example:**
|
||
```python
|
||
config = ClaudeCodeConfig()
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory="."
|
||
)
|
||
|
||
print(agent.session_id) # None
|
||
|
||
result = agent(message="Hello")
|
||
print(agent.session_id) # 'eb1b2f39-e04c-4506-9398-b50053b1fd83'
|
||
```
|
||
|
||
##### `config: ClaudeCodeConfig`
|
||
|
||
Access to the agent's configuration (model only).
|
||
|
||
```python
|
||
print(agent.config.model) # 'claude-opus-4-5-20251101'
|
||
```
|
||
|
||
**Note:** Only the `model` is stored in config. Other parameters like `working_directory`, `permission_mode`, and `allowed_tools` are instance attributes, not config properties.
|
||
|
||
## Key Implementation Details
|
||
|
||
### Config vs Kwargs
|
||
|
||
The `ClaudeCodeConfig` **only** contains the `model` parameter. All other configuration options are passed as keyword arguments:
|
||
|
||
```python
|
||
from claude_dspy import ClaudeCode, ClaudeCodeConfig
|
||
|
||
# Config contains ONLY the model
|
||
config = ClaudeCodeConfig(model="claude-opus-4-5-20251101")
|
||
|
||
# All other params are kwargs
|
||
agent = ClaudeCode(
|
||
config, # Config with model
|
||
signature="message:str -> answer:str", # Kwarg
|
||
working_directory=".", # Kwarg
|
||
permission_mode="acceptEdits", # Kwarg
|
||
allowed_tools=["Read", "Write", "Bash"], # Kwarg
|
||
)
|
||
```
|
||
|
||
### AutoProgram.from_precompiled
|
||
|
||
When loading from Modaic Hub, the same pattern applies:
|
||
|
||
```python
|
||
from modaic import AutoProgram
|
||
from claude_dspy import ClaudeCodeConfig
|
||
import dspy
|
||
|
||
class MySignature(dspy.Signature):
|
||
message: str = dspy.InputField(desc="Request")
|
||
answer: str = dspy.OutputField(desc="Response")
|
||
|
||
# Option 1: Use default model (no config needed)
|
||
agent = AutoProgram.from_precompiled(
|
||
"farouk1/claude-code",
|
||
signature=MySignature, # Kwarg
|
||
working_directory=".", # Kwarg
|
||
permission_mode="acceptEdits", # Kwarg
|
||
allowed_tools=["Read", "Write", "Bash"], # Kwarg
|
||
)
|
||
|
||
# Option 2: Override model via config
|
||
config = ClaudeCodeConfig(model="claude-opus-4-5-20251101")
|
||
agent = AutoProgram.from_precompiled(
|
||
"farouk1/claude-code",
|
||
config=config, # Config with model
|
||
signature=MySignature, # Kwarg
|
||
working_directory=".", # Kwarg
|
||
permission_mode="acceptEdits", # Kwarg
|
||
allowed_tools=["Read", "Write", "Bash"], # Kwarg
|
||
)
|
||
```
|
||
|
||
### Available Tools
|
||
|
||
The `allowed_tools` parameter accepts any valid Claude Code tool name:
|
||
|
||
**File Operations:**
|
||
- `"Read"` - Read files and directories
|
||
- `"Write"` - Write and create files
|
||
- `"Edit"` - Edit existing files
|
||
|
||
**Command Execution:**
|
||
- `"Bash"` - Execute bash commands
|
||
|
||
**Code Search:**
|
||
- `"Glob"` - Search for files by pattern
|
||
- `"Grep"` - Search file contents
|
||
|
||
**Web Tools:**
|
||
- `"WebSearch"` - Search the web
|
||
- `"WebFetch"` - Fetch web content
|
||
|
||
**Other Tools:**
|
||
- `"NotebookEdit"` - Edit Jupyter notebooks
|
||
- And other Claude Code tools...
|
||
|
||
## Advanced Features
|
||
|
||
### Using MCP Servers
|
||
|
||
MCP (Model Context Protocol) servers allow you to add custom tools to Claude. The SDK supports creating in-process MCP servers with custom tools.
|
||
|
||
```python
|
||
from claude_dspy import ClaudeCode, ClaudeCodeConfig
|
||
from claude_agent_sdk import tool, create_sdk_mcp_server
|
||
from typing import Any
|
||
import dspy
|
||
|
||
# Define custom tools with @tool decorator
|
||
@tool("calculate", "Perform mathematical calculations", {"expression": str})
|
||
async def calculate(args: dict[str, Any]) -> dict[str, Any]:
|
||
try:
|
||
result = eval(args["expression"], {"__builtins__": {}})
|
||
return {
|
||
"content": [{"type": "text", "text": f"Result: {result}"}]
|
||
}
|
||
except Exception as e:
|
||
return {
|
||
"content": [{"type": "text", "text": f"Error: {str(e)}"}],
|
||
"is_error": True
|
||
}
|
||
|
||
# Create MCP server
|
||
calculator_server = create_sdk_mcp_server(
|
||
name="calculator",
|
||
version="1.0.0",
|
||
tools=[calculate]
|
||
)
|
||
|
||
# Use with ClaudeCode
|
||
config = ClaudeCodeConfig()
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory=".",
|
||
mcp_servers={"calc": calculator_server},
|
||
allowed_tools=["mcp__calc__calculate"] # MCP tools are prefixed with "mcp__<server>__<tool>"
|
||
)
|
||
|
||
result = agent(message="Calculate 123 * 456")
|
||
print(result.answer)
|
||
```
|
||
|
||
### Session Management
|
||
|
||
Resume and continue conversations from previous sessions:
|
||
|
||
```python
|
||
from claude_dspy import ClaudeCode, ClaudeCodeConfig
|
||
|
||
config = ClaudeCodeConfig()
|
||
|
||
# First conversation
|
||
agent1 = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory="."
|
||
)
|
||
result1 = agent1(message="Create a file called notes.txt")
|
||
session_id = agent1.session_id
|
||
print(f"Session ID: {session_id}")
|
||
|
||
# Resume the same conversation later
|
||
agent2 = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory=".",
|
||
resume=session_id # Resume from session ID
|
||
)
|
||
result2 = agent2(message="What file did we just create?")
|
||
print(result2.answer) # Claude remembers the previous context!
|
||
```
|
||
|
||
### Using Hooks
|
||
|
||
Intercept and modify tool execution with hooks:
|
||
|
||
```python
|
||
from claude_dspy import ClaudeCode, ClaudeCodeConfig
|
||
from typing import Any
|
||
|
||
async def pre_tool_logger(input_data: dict[str, Any], tool_use_id: str | None, context: Any) -> dict[str, Any]:
|
||
"""Log all tool usage before execution."""
|
||
tool_name = input_data.get('tool_name', 'unknown')
|
||
print(f"About to use tool: {tool_name}")
|
||
|
||
# Block dangerous commands
|
||
if tool_name == "Bash" and "rm -rf /" in str(input_data.get('tool_input', {})):
|
||
return {
|
||
'hookSpecificOutput': {
|
||
'hookEventName': 'PreToolUse',
|
||
'permissionDecision': 'deny',
|
||
'permissionDecisionReason': 'Dangerous command blocked'
|
||
}
|
||
}
|
||
return {}
|
||
|
||
config = ClaudeCodeConfig()
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory=".",
|
||
hooks={
|
||
'PreToolUse': [
|
||
{'matcher': 'Bash', 'hooks': [pre_tool_logger]}
|
||
]
|
||
},
|
||
allowed_tools=["Read", "Write", "Bash"]
|
||
)
|
||
|
||
result = agent(message="List files in this directory")
|
||
```
|
||
|
||
### Loading Project Settings
|
||
|
||
Control which filesystem settings to load:
|
||
|
||
```python
|
||
from claude_dspy import ClaudeCode, ClaudeCodeConfig
|
||
|
||
config = ClaudeCodeConfig()
|
||
|
||
# Load only project settings (e.g., CLAUDE.md files)
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory=".",
|
||
system_prompt={
|
||
"type": "preset",
|
||
"preset": "claude_code"
|
||
},
|
||
setting_sources=["project"], # Load .claude/settings.json and CLAUDE.md
|
||
allowed_tools=["Read", "Write"]
|
||
)
|
||
|
||
result = agent(message="Add a feature following project conventions")
|
||
```
|
||
|
||
## Usage Patterns
|
||
|
||
### Pattern 1: Multi-turn Conversation
|
||
|
||
Each agent instance maintains a stateful session:
|
||
|
||
```python
|
||
from claude_dspy import ClaudeCode, ClaudeCodeConfig
|
||
|
||
config = ClaudeCodeConfig()
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory=".",
|
||
)
|
||
|
||
# Turn 1
|
||
result1 = agent(message="What's the main bug?")
|
||
print(result1.answer)
|
||
|
||
# Turn 2 - has context from Turn 1
|
||
result2 = agent(message="How do we fix it?")
|
||
print(result2.answer)
|
||
|
||
# Turn 3 - has context from Turn 1 + 2
|
||
result3 = agent(message="Write tests for the fix")
|
||
print(result3.answer)
|
||
|
||
# All use same session_id
|
||
print(agent.session_id)
|
||
```
|
||
|
||
### Pattern 2: Fresh Context
|
||
|
||
Want a new conversation? Create a new agent:
|
||
|
||
```python
|
||
from claude_dspy import ClaudeCode, ClaudeCodeConfig
|
||
|
||
config = ClaudeCodeConfig()
|
||
|
||
# Agent 1 - Task A
|
||
agent1 = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory=".",
|
||
)
|
||
result1 = agent1(message="Analyze bug in module A")
|
||
|
||
# Agent 2 - Task B (no context from Agent 1)
|
||
agent2 = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory=".",
|
||
)
|
||
result2 = agent2(message="Analyze bug in module B")
|
||
```
|
||
|
||
### Pattern 3: Field Descriptions for Enhanced Context
|
||
|
||
Enhance prompts with signature docstrings and field descriptions - all automatically included in the prompt:
|
||
|
||
```python
|
||
import dspy
|
||
from claude_dspy import ClaudeCode, ClaudeCodeConfig
|
||
|
||
class MySignature(dspy.Signature):
|
||
"""Analyze code architecture.""" # Used as task description
|
||
|
||
message: str = dspy.InputField(
|
||
desc="Request to process" # Provides input context
|
||
)
|
||
analysis: str = dspy.OutputField(
|
||
desc="A detailed markdown report with sections: "
|
||
"1) Architecture overview, 2) Key components, 3) Dependencies" # Guides output format
|
||
)
|
||
|
||
config = ClaudeCodeConfig()
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature=MySignature,
|
||
working_directory=".",
|
||
)
|
||
result = agent(message="Analyze this codebase")
|
||
|
||
# The prompt sent to Claude will include:
|
||
# 1. Task: "Analyze code architecture." (from docstring)
|
||
# 2. Input context: "Request to process" (from InputField desc)
|
||
# 3. Your message: "Analyze this codebase"
|
||
# 4. Output guidance: "Please produce the following output: A detailed markdown report..." (from OutputField desc)
|
||
```
|
||
|
||
### Pattern 4: Inspecting Execution Trace
|
||
|
||
Access detailed execution information:
|
||
|
||
```python
|
||
from claude_dspy import ClaudeCode, ClaudeCodeConfig, ToolUseItem, ToolResultItem
|
||
|
||
config = ClaudeCodeConfig()
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory=".",
|
||
)
|
||
|
||
result = agent(message="Fix the bug")
|
||
|
||
# Filter trace by type
|
||
tool_uses = [item for item in result.trace if isinstance(item, ToolUseItem)]
|
||
for tool in tool_uses:
|
||
print(f"Tool: {tool.tool_name}")
|
||
print(f"Input: {tool.tool_input}")
|
||
|
||
tool_results = [item for item in result.trace if isinstance(item, ToolResultItem)]
|
||
for result_item in tool_results:
|
||
print(f"Result: {result_item.content}")
|
||
print(f"Error: {result_item.is_error}")
|
||
```
|
||
|
||
### Pattern 5: Token Usage Tracking
|
||
|
||
Monitor API usage:
|
||
|
||
```python
|
||
result = agent(message="...")
|
||
|
||
print(f"Input tokens: {result.usage.input_tokens}")
|
||
print(f"Cached tokens: {result.usage.cached_input_tokens}")
|
||
print(f"Output tokens: {result.usage.output_tokens}")
|
||
print(f"Total: {result.usage.total_tokens}")
|
||
```
|
||
|
||
### Pattern 6: Safe Execution with Permissions
|
||
|
||
Control what the agent can do:
|
||
|
||
```python
|
||
from claude_dspy import ClaudeCode, ClaudeCodeConfig
|
||
|
||
# Read-only (safest)
|
||
config = ClaudeCodeConfig()
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory=".",
|
||
permission_mode="default",
|
||
allowed_tools=["Read"], # Only allow reading files
|
||
)
|
||
|
||
# Auto-accept file edits
|
||
config = ClaudeCodeConfig(model="claude-opus-4-5-20251101")
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory=".",
|
||
permission_mode="acceptEdits",
|
||
allowed_tools=["Read", "Write"], # Allow reading and writing
|
||
)
|
||
|
||
# Full permissions with command execution
|
||
config = ClaudeCodeConfig()
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory=".",
|
||
permission_mode="acceptEdits",
|
||
allowed_tools=["Read", "Write", "Bash"], # All tools enabled
|
||
)
|
||
```
|
||
|
||
## Advanced Examples
|
||
|
||
### Example 1: Code Review Agent
|
||
|
||
```python
|
||
from pydantic import BaseModel, Field
|
||
from claude_dspy import ClaudeCode, ClaudeCodeConfig
|
||
|
||
class CodeReview(BaseModel):
|
||
summary: str = Field(description="High-level summary")
|
||
issues: list[str] = Field(description="List of issues found")
|
||
severity: str = Field(description="critical, high, medium, or low")
|
||
recommendations: list[str] = Field(description="Actionable recommendations")
|
||
|
||
config = ClaudeCodeConfig(model="claude-opus-4-5-20251101")
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature="message:str -> review:CodeReview",
|
||
working_directory="/path/to/project",
|
||
permission_mode="default",
|
||
allowed_tools=["Read"], # Read-only for code review
|
||
)
|
||
|
||
result = agent(message="Review the changes in src/main.py")
|
||
|
||
print(f"Severity: {result.review.severity}")
|
||
for issue in result.review.issues:
|
||
print(f"- {issue}")
|
||
```
|
||
|
||
### Example 2: Iterative Debugging
|
||
|
||
```python
|
||
from claude_dspy import ClaudeCode, ClaudeCodeConfig
|
||
|
||
config = ClaudeCodeConfig()
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature="message:str -> response:str",
|
||
working_directory=".",
|
||
permission_mode="acceptEdits",
|
||
allowed_tools=["Read", "Write", "Bash"],
|
||
)
|
||
|
||
# Turn 1: Find the bug
|
||
result1 = agent(message="Find the bug in src/calculator.py")
|
||
print(result1.response)
|
||
|
||
# Turn 2: Propose a fix
|
||
result2 = agent(message="What's the best way to fix it?")
|
||
print(result2.response)
|
||
|
||
# Turn 3: Implement the fix
|
||
result3 = agent(message="Implement the fix")
|
||
print(result3.response)
|
||
|
||
# Turn 4: Write tests
|
||
result4 = agent(message="Write tests for the fix")
|
||
print(result4.response)
|
||
```
|
||
|
||
### Example 3: Async Usage
|
||
|
||
```python
|
||
import asyncio
|
||
from claude_dspy import ClaudeCode, ClaudeCodeConfig
|
||
|
||
async def main():
|
||
config = ClaudeCodeConfig()
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature="message:str -> answer:str",
|
||
working_directory=".",
|
||
)
|
||
|
||
# Use aforward in async context
|
||
result = await agent.aforward(message="Analyze this code")
|
||
print(result.answer)
|
||
|
||
# Cleanup
|
||
await agent.disconnect()
|
||
|
||
asyncio.run(main())
|
||
```
|
||
|
||
## Trace Item Types
|
||
|
||
When accessing `result.trace`, you'll see various item types:
|
||
|
||
| Type | Fields | Description |
|
||
|------|--------|-------------|
|
||
| `AgentMessageItem` | `text`, `model` | Agent's text response |
|
||
| `ThinkingItem` | `text`, `model` | Agent's internal reasoning |
|
||
| `ToolUseItem` | `tool_name`, `tool_input`, `tool_use_id` | Tool invocation |
|
||
| `ToolResultItem` | `tool_name`, `tool_use_id`, `content`, `is_error` | Tool result |
|
||
| `ErrorItem` | `message`, `error_type` | Error that occurred |
|
||
|
||
## How It Works
|
||
|
||
### Signature <20> Claude Flow
|
||
|
||
```
|
||
1. Define signature: 'message:str -> answer:str'
|
||
|
||
2. ClaudeCode validates (must have 1 input, 1 output)
|
||
|
||
3. __init__ creates ClaudeSDKClient with options
|
||
|
||
4. forward(message="...") extracts message
|
||
|
||
5. If output field has desc <20> append to message
|
||
|
||
6. If output type ` str <20> generate JSON schema
|
||
|
||
7. Call client.query(message) with optional output_format
|
||
|
||
8. Iterate through receive_response(), collect messages
|
||
|
||
9. Parse response (JSON if Pydantic, str otherwise)
|
||
|
||
10. Return Prediction(output=..., trace=..., usage=...)
|
||
```
|
||
|
||
### Output Type Handling
|
||
|
||
**String output:**
|
||
```python
|
||
sig = dspy.Signature('message:str -> answer:str')
|
||
# No schema passed to Claude Code
|
||
# Response used as-is
|
||
```
|
||
|
||
**Pydantic output:**
|
||
```python
|
||
sig = dspy.Signature('message:str -> report:BugReport')
|
||
# JSON schema generated from BugReport
|
||
# Schema passed to Claude Code via output_format
|
||
# Response parsed with BugReport.model_validate_json()
|
||
```
|
||
|
||
### Prompt Building
|
||
|
||
ClaudeCode automatically builds rich prompts from your signature to provide maximum context to Claude:
|
||
|
||
```python
|
||
class MySignature(dspy.Signature):
|
||
"""Analyze code quality.""" # 1. Task description
|
||
|
||
message: str = dspy.InputField(
|
||
desc="Path to file or module" # 2. Input context
|
||
)
|
||
report: str = dspy.OutputField(
|
||
desc="Markdown report with issues and recommendations" # 3. Output guidance
|
||
)
|
||
|
||
config = ClaudeCodeConfig()
|
||
agent = ClaudeCode(
|
||
config,
|
||
signature=MySignature,
|
||
working_directory="."
|
||
)
|
||
result = agent(message="Analyze src/main.py") # 4. Your actual input
|
||
```
|
||
|
||
**The final prompt sent to Claude:**
|
||
```
|
||
Task: Analyze code quality.
|
||
|
||
Input context: Path to file or module
|
||
|
||
Analyze src/main.py
|
||
|
||
Please produce the following output: Markdown report with issues and recommendations
|
||
```
|
||
|
||
This automatic context enhancement helps Claude better understand:
|
||
- **What** the overall task is (docstring)
|
||
- **What** the input represents (InputField desc)
|
||
- **What** format the output should have (OutputField desc)
|
||
|
||
## Troubleshooting
|
||
|
||
### Error: "ClaudeCode requires exactly 1 input field"
|
||
|
||
Your signature has too many or too few fields. ClaudeCode expects exactly one input and one output:
|
||
|
||
```python
|
||
# L Wrong - multiple inputs
|
||
sig = dspy.Signature('context:str, question:str -> answer:str')
|
||
|
||
# Correct - single input
|
||
sig = dspy.Signature('message:str -> answer:str')
|
||
```
|
||
|
||
### Error: "Failed to parse Claude response as MyModel"
|
||
|
||
The model returned JSON that doesn't match your Pydantic schema. Check:
|
||
1. Schema is valid and clear
|
||
2. Field descriptions are helpful
|
||
3. Model has enough context to generate correct structure
|
||
|
||
### Error: "Claude Code CLI not found"
|
||
|
||
Install Claude Code CLI:
|
||
```bash
|
||
# Visit code.claude.com for installation instructions
|
||
# or use npm:
|
||
npm install -g @anthropic-ai/claude-code
|
||
```
|
||
|
||
### Async event loop issues
|
||
|
||
Use `aforward()` when already in an async context:
|
||
|
||
```python
|
||
# L Don't do this in async context
|
||
async def main():
|
||
result = agent(message="...") # Can cause issues
|
||
|
||
# Do this instead
|
||
async def main():
|
||
result = await agent.aforward(message="...")
|
||
```
|
||
|
||
## Design Philosophy
|
||
|
||
### Why 1 input, 1 output?
|
||
|
||
ClaudeCode is designed for conversational agentic workflows. The input is always a message/prompt, and the output is always a response. This keeps the interface simple and predictable.
|
||
|
||
For complex inputs, compose them into the message:
|
||
|
||
```python
|
||
# Instead of: 'context:str, question:str -> answer:str'
|
||
message = f"Context: {context}\n\nQuestion: {question}"
|
||
result = agent(message=message)
|
||
```
|
||
|
||
### Why stateful sessions?
|
||
|
||
Agents often need multi-turn context (e.g., "fix the bug" <20> "write tests for it"). Stateful sessions make this natural without manual history management.
|
||
|
||
Want fresh context? Create a new agent instance.
|
||
|
||
### Why return trace + usage?
|
||
|
||
Observability is critical for agentic systems. You need to know:
|
||
- What tools were used
|
||
- What the agent was thinking
|
||
- How many tokens were consumed
|
||
- If any errors occurred
|
||
|
||
The trace provides full visibility into agent execution.
|
||
|
||
## Comparison with CodexAgent
|
||
|
||
| Feature | CodexAgent | ClaudeCode |
|
||
|---------|-----------|-------------|
|
||
| SDK | OpenAI Codex SDK | Claude Code Python SDK |
|
||
| Thread management | Built-in thread ID | Session-based (ClaudeSDKClient) |
|
||
| Streaming | Yes | Yes (via receive_response) |
|
||
| Async support | No | Yes (aforward) |
|
||
| Tool types | Codex-specific | Claude Code tools (Bash, Read, Write, etc.) |
|
||
| Sandbox | Simple mode enum | Detailed config dict |
|
||
| Permission control | Sandbox modes | Permission modes + allowed_tools |
|
||
| Configuration | Direct parameters | Config object (ClaudeCodeConfig) |
|
||
|
||
## Examples Directory
|
||
|
||
Check out the `examples/` directory for more:
|
||
|
||
- `basic_string_output.py` - Simple string output
|
||
- `pydantic_output.py` - Structured Pydantic output
|
||
- `multi_turn_conversation.py` - Multi-turn conversation
|
||
- `output_field_description.py` - Using output field descriptions
|
||
- `inspect_trace.py` - Inspecting execution trace
|
||
- `code_review_agent.py` - Advanced code review agent
|
||
|
||
## Contributing
|
||
|
||
Issues and PRs welcome! This is an implementation of Claude Code SDK integration with DSPy.
|
||
|
||
## License
|
||
|
||
See LICENSE file.
|
||
|
||
## Related Documentation
|
||
|
||
- [Claude Agent SDK - Python Reference](https://platform.claude.com/docs/en/agent-sdk/python) - Complete SDK API reference
|
||
- [Claude Agent SDK - Overview](https://platform.claude.com/docs/en/agent-sdk/overview) - SDK concepts and guides
|
||
- [DSPy Documentation](https://dspy-docs.vercel.app/) - DSPy framework documentation
|
||
- [Claude Code CLI](https://code.claude.com) - Claude Code command-line interface
|
||
|
||
---
|
||
|
||
**Note:** This is a community implementation of Claude Code SDK integration with DSPy, inspired by the CodexAgent design pattern.
|