Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:00:18 +08:00
commit 765529cd13
69 changed files with 18291 additions and 0 deletions

View File

@@ -0,0 +1,406 @@
# Claude Agent SDK Best Practices
This guide captures best practices and common patterns for building effective SDK applications.
## Agent Definition
### ✅ Use Programmatic Registration
**Recommended:** Define agents via `agents` parameter
```python
options = ClaudeAgentOptions(
agents={
"investigator": AgentDefinition(
description="Analyzes errors autonomously",
prompt="You are an error investigator...",
tools=["Read", "Grep", "Glob", "Bash"]
)
}
)
```
**Not Recommended:** Relying on filesystem auto-discovery
```python
# SDK can auto-discover .claude/agents/*.md
# but programmatic registration is clearer and more maintainable
options = ClaudeAgentOptions()
```
### ✅ Set Orchestrator System Prompt
**Critical:** Orchestrators must use `system_prompt="claude_code"`
```python
options = ClaudeAgentOptions(
system_prompt="claude_code", # Knows how to use Task tool
allowed_tools=["Bash", "Task", "Read", "Write"],
agents={...}
)
```
**Why:** The claude_code preset includes knowledge of the Task tool for delegating to subagents.
### ✅ Match Agent Names
Ensure agent names in `agents={}` match references in prompts:
```python
# Define agent
options = ClaudeAgentOptions(
agents={"markdown-investigator": AgentDefinition(...)}
)
# Reference in prompt
await client.query("Use the 'markdown-investigator' subagent...")
```
## Tool Configuration
### ✅ Restrict Subagent Tools
Limit subagent tools to minimum needed:
```python
# Read-only analyzer
"analyzer": AgentDefinition(
tools=["Read", "Grep", "Glob"]
)
# Code modifier
"fixer": AgentDefinition(
tools=["Read", "Edit", "Bash"]
)
```
### ✅ Give Orchestrator Task Tool
Orchestrators need Task tool to delegate:
```python
options = ClaudeAgentOptions(
allowed_tools=["Bash", "Task", "Read", "Write"], # Include Task
agents={...}
)
```
## Async/Await Patterns
### ✅ Use async with for Streaming
```python
async with ClaudeSDKClient(options=options) as client:
await client.query(prompt)
async for message in client.receive_response():
if isinstance(message, AssistantMessage):
# Process messages
pass
```
### ✅ Handle Multiple Message Types
```python
async for message in client.receive_response():
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
text = block.text
elif isinstance(message, ResultMessage):
print(f"Cost: ${message.total_cost_usd:.4f}")
print(f"Duration: {message.duration_ms}ms")
```
## Error Handling
### ✅ Validate Agent Responses
Don't assume agents return expected format:
```python
investigation_report = None
async for message in client.receive_response():
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
# Try to extract JSON
try:
investigation_report = json.loads(block.text)
except json.JSONDecodeError:
# Handle non-JSON response
continue
if not investigation_report:
raise RuntimeError("Agent did not return valid report")
```
### ✅ Use uv Script Headers
For standalone SDK scripts, use uv inline script metadata:
```python
#!/usr/bin/env -S uv run --script --quiet
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "claude-agent-sdk>=0.1.6",
# ]
# ///
```
## Project Structure
### ✅ Organize Agent Definitions
Option 1: Store in markdown files, load programmatically
```text
project/
├── .claude/
│ └── agents/
│ ├── investigator.md
│ └── fixer.md
├── main.py
```
```python
def load_agent_definition(path: str) -> AgentDefinition:
# Parse frontmatter and content
# Return AgentDefinition
investigator = load_agent_definition(".claude/agents/investigator.md")
options = ClaudeAgentOptions(agents={"investigator": investigator})
```
Option 2: Define inline
```python
options = ClaudeAgentOptions(
agents={
"investigator": AgentDefinition(
description="...",
prompt="...",
tools=[...]
)
}
)
```
## Permission Management
### ✅ Choose Appropriate Permission Mode
```python
# Automated workflows (auto-approve edits)
options = ClaudeAgentOptions(
permission_mode="acceptEdits"
)
# Interactive development (ask for approval)
options = ClaudeAgentOptions(
permission_mode="default",
can_use_tool=permission_callback
)
# Read-only mode (use tool restrictions)
options = ClaudeAgentOptions(
allowed_tools=["Read", "Grep", "Glob"] # Only read tools
)
```
### ✅ Use Hooks for Complex Logic
Prefer hooks over permission callbacks for:
- Adding context
- Reviewing outputs
- Stopping on errors
```python
options = ClaudeAgentOptions(
hooks={
"PreToolUse": [
HookMatcher(matcher="Bash", hooks=[check_command])
],
"PostToolUse": [
HookMatcher(matcher="Bash", hooks=[review_output])
]
}
)
```
## Common Anti-Patterns
### ❌ Missing System Prompt on Orchestrator
```python
# Orchestrator won't know how to use Task tool
options = ClaudeAgentOptions(
agents={...}
# Missing system_prompt="claude_code"
)
```
### ❌ Tool/Prompt Mismatch
```python
# Tells agent to modify files but only allows read tools
options = ClaudeAgentOptions(
system_prompt="Fix any bugs you find",
allowed_tools=["Read", "Grep"] # Can't actually fix
)
```
### ❌ Assuming Agent Output Format
```python
# Assumes agent returns JSON
json_data = json.loads(message.content[0].text) # May crash
```
### ❌ Not Validating Agent Names
```python
# Define as "investigator" but reference as "markdown-investigator"
options = ClaudeAgentOptions(
agents={"investigator": AgentDefinition(...)}
)
await client.query("Use 'markdown-investigator'...") # Won't work
```
## Performance Optimization
### ✅ Use Appropriate Models
```python
# Fast, cheap tasks
"simple-agent": AgentDefinition(model="haiku", ...)
# Complex reasoning
"complex-agent": AgentDefinition(model="sonnet", ...)
# Inherit from main agent
"helper-agent": AgentDefinition(model="inherit", ...)
```
### ✅ Set Budget Limits
```python
options = ClaudeAgentOptions(
max_budget_usd=1.00 # Stop after $1
)
```
### ✅ Limit Turns for Simple Tasks
```python
options = ClaudeAgentOptions(
max_turns=3 # Prevent infinite loops
)
```
## Testing
### ✅ Validate Agent Definitions
```python
def test_agent_configuration():
"""Ensure agent definitions are valid."""
options = get_sdk_options()
# Check orchestrator has claude_code preset
# Note: Can be string "claude_code" or dict {"type": "preset", "preset": "claude_code"}
assert options.system_prompt in ("claude_code", {"type": "preset", "preset": "claude_code"})
# Check orchestrator has Task tool
assert "Task" in options.allowed_tools
# Check agents are registered
assert "investigator" in options.agents
assert "fixer" in options.agents
```
### ✅ Test Tool Restrictions
```python
def test_subagent_tools():
"""Ensure subagents have correct tools."""
options = get_sdk_options()
investigator = options.agents["investigator"]
assert "Read" in investigator.tools
assert "Write" not in investigator.tools # Read-only
```
## Documentation
### ✅ Document Agent Purposes
```python
options = ClaudeAgentOptions(
agents={
"investigator": AgentDefinition(
# Clear, specific description
description=(
"Autonomous analyzer that determines if markdown errors "
"are fixable or false positives"
),
prompt="...",
tools=[...]
)
}
)
```
### ✅ Document Workflow
```python
"""
Intelligent Markdown Linting Orchestrator
Architecture:
- Orchestrator (main): Strategic coordination
- Investigator subagent: Autonomous error analysis
- Fixer subagent: Execute fixes with context
Workflow:
1. Discovery: Run linter, parse output
2. Triage: Classify errors (simple vs ambiguous)
3. Investigation: Investigator analyzes ambiguous errors
4. Fixing: Fixer applies fixes based on investigation
5. Verification: Re-run linter to confirm fixes
"""
```
## Debugging
### ✅ Log Agent Communication
```python
all_response_text = []
async for message in client.receive_response():
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
all_response_text.append(block.text)
print(f"Agent: {block.text}")
# Save full transcript for debugging
with open("debug_transcript.txt", "w") as f:
f.write("\n\n".join(all_response_text))
```
### ✅ Track Costs
```python
async for message in client.receive_response():
if isinstance(message, ResultMessage):
if message.total_cost_usd:
print(f"Total cost: ${message.total_cost_usd:.4f}")
if message.duration_ms:
print(f"Duration: {message.duration_ms}ms")
```