9.0 KiB
9.0 KiB
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
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
# 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"
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:
# 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:
# 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:
options = ClaudeAgentOptions(
allowed_tools=["Bash", "Task", "Read", "Write"], # Include Task
agents={...}
)
Async/Await Patterns
✅ Use async with for Streaming
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
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:
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:
#!/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
project/
├── .claude/
│ └── agents/
│ ├── investigator.md
│ └── fixer.md
├── main.py
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
options = ClaudeAgentOptions(
agents={
"investigator": AgentDefinition(
description="...",
prompt="...",
tools=[...]
)
}
)
Permission Management
✅ Choose Appropriate Permission Mode
# 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
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
# Orchestrator won't know how to use Task tool
options = ClaudeAgentOptions(
agents={...}
# Missing system_prompt="claude_code"
)
❌ Tool/Prompt Mismatch
# 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
# Assumes agent returns JSON
json_data = json.loads(message.content[0].text) # May crash
❌ Not Validating Agent Names
# 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
# 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
options = ClaudeAgentOptions(
max_budget_usd=1.00 # Stop after $1
)
✅ Limit Turns for Simple Tasks
options = ClaudeAgentOptions(
max_turns=3 # Prevent infinite loops
)
Testing
✅ Validate Agent Definitions
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
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
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
"""
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
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
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")