Files
claude-code/README.md

1031 lines
28 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.
## 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
## Installation
```bash
# install with uv
uv add claude-agent-sdk dspy modaic nest-asyncio
```
**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.
### 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
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()
agent = ClaudeCode(
config,
signature="message:str -> report:BugReport",
working_directory="."
)
result = agent(message="Analyze the bug in error.log")
print(result.report.severity) # Typed access!
print(result.report.affected_files)
```
### 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,
signature="message:str -> answer:str",
working_directory=".",
permission_mode="acceptEdits",
)
# Test it locally
result = agent(message="Test my agent")
print(result.answer)
# 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("your-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.