Compare commits
No commits in common. "ba1b5b76c647772098c2d510c6d7b7b5cf224ba6" and "b707fa84f95f501772d2047cfad895e055af9105" have entirely different histories.
ba1b5b76c6
...
b707fa84f9
50
CLAUDE.md
50
CLAUDE.md
@ -1,50 +0,0 @@
|
|||||||
# PhoneWork — Development Guide
|
|
||||||
|
|
||||||
## Quick Reference
|
|
||||||
|
|
||||||
| Need | Where to look |
|
|
||||||
|------|---------------|
|
|
||||||
| Project architecture, deployment, bot commands | `README.md` |
|
|
||||||
| Claude Agent SDK usage patterns and tested examples | `docs/claude/` |
|
|
||||||
| SDK migration plan (subprocess → ClaudeSDKClient) | `.claude/plans/toasty-pondering-nova.md` |
|
|
||||||
| Feishu card / markdown formatting | `docs/feishu/` |
|
|
||||||
|
|
||||||
## Claude Agent SDK
|
|
||||||
|
|
||||||
When writing code that uses `claude-agent-sdk`, **first read `docs/claude/`**:
|
|
||||||
|
|
||||||
- `_sdk_test_common.py` — .env auth loading pattern (`setup_auth()`, `auth_env()`, `make_tmpdir()`)
|
|
||||||
- `test_query_read_edit.py` — `query()` one-shot: Read + Edit with `allowed_tools`
|
|
||||||
- `test_client_write_resume.py` — `ClaudeSDKClient`: Write + session resume via `resume=session_id`
|
|
||||||
- `test_hooks_audit_deny.py` — Hooks: `PostToolUse` audit + `PreToolUse` deny
|
|
||||||
|
|
||||||
### Key rules (verified by testing)
|
|
||||||
|
|
||||||
- Use `allowed_tools` for permission auto-approval. **Do not pass custom lists to `tools=`** — it causes CLI exit code 1.
|
|
||||||
- Auth: set `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN` in `.env`; pass via `ClaudeAgentOptions(env=auth_env())`. Clear `ANTHROPIC_API_KEY` to avoid auth conflicts.
|
|
||||||
- SDK does not strip ANSI escape codes from `ResultMessage.result` — handle in application layer if needed.
|
|
||||||
- On Windows, use manual `make_tmpdir()` / `remove_tmpdir()` instead of `tempfile.TemporaryDirectory()` context manager to avoid cleanup races with CLI subprocesses.
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
bot/ Feishu event handling, commands, message sending
|
|
||||||
orchestrator/ LangChain agent + tools (session management, shell, files, web)
|
|
||||||
agent/ Claude Code runner, session manager, task runner, scheduler, audit
|
|
||||||
router/ Multi-host routing (public VPS side)
|
|
||||||
host_client/ Host client (behind NAT, connects to router)
|
|
||||||
shared/ Wire protocol for router ↔ host communication
|
|
||||||
docs/claude/ Claude Agent SDK examples and reference tests
|
|
||||||
docs/feishu/ Feishu API reference
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd docs/claude
|
|
||||||
../../.venv/Scripts/python test_query_read_edit.py
|
|
||||||
../../.venv/Scripts/python test_client_write_resume.py
|
|
||||||
../../.venv/Scripts/python test_hooks_audit_deny.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Requires `.env` at project root with `ANTHROPIC_BASE_URL` and `ANTHROPIC_AUTH_TOKEN`.
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
"""Shared helpers for Claude Agent SDK tests.
|
|
||||||
|
|
||||||
Loads ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN from the project root .env
|
|
||||||
and injects them into the process environment and ClaudeAgentOptions.env.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
DOTENV_PATH = PROJECT_ROOT / ".env"
|
|
||||||
|
|
||||||
|
|
||||||
def load_dotenv(dotenv_path: Path = DOTENV_PATH) -> dict[str, str]:
|
|
||||||
"""Parse KEY=VALUE lines from a .env file (no third-party deps)."""
|
|
||||||
values: dict[str, str] = {}
|
|
||||||
if not dotenv_path.exists():
|
|
||||||
return values
|
|
||||||
for raw_line in dotenv_path.read_text(encoding="utf-8").splitlines():
|
|
||||||
line = raw_line.strip()
|
|
||||||
if not line or line.startswith("#") or "=" not in line:
|
|
||||||
continue
|
|
||||||
key, value = line.split("=", 1)
|
|
||||||
values[key.strip()] = value.strip().strip('"').strip("'")
|
|
||||||
return values
|
|
||||||
|
|
||||||
|
|
||||||
def setup_auth() -> tuple[bool, str]:
|
|
||||||
"""Inject .env auth vars into os.environ; clear ANTHROPIC_API_KEY to avoid conflicts."""
|
|
||||||
values = load_dotenv()
|
|
||||||
base_url = values.get("ANTHROPIC_BASE_URL")
|
|
||||||
auth_token = values.get("ANTHROPIC_AUTH_TOKEN")
|
|
||||||
oauth_token = values.get("CLAUDE_CODE_OAUTH_TOKEN")
|
|
||||||
|
|
||||||
if not base_url:
|
|
||||||
return False, "Missing ANTHROPIC_BASE_URL in .env"
|
|
||||||
if not auth_token and not oauth_token:
|
|
||||||
return False, "Missing ANTHROPIC_AUTH_TOKEN or CLAUDE_CODE_OAUTH_TOKEN in .env"
|
|
||||||
|
|
||||||
os.environ["ANTHROPIC_BASE_URL"] = base_url
|
|
||||||
if auth_token:
|
|
||||||
os.environ["ANTHROPIC_AUTH_TOKEN"] = auth_token
|
|
||||||
if oauth_token:
|
|
||||||
os.environ["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
|
||||||
os.environ.pop("ANTHROPIC_API_KEY", None)
|
|
||||||
return True, f"Auth loaded from {DOTENV_PATH}"
|
|
||||||
|
|
||||||
|
|
||||||
def auth_env() -> dict[str, str]:
|
|
||||||
"""Return env dict suitable for ClaudeAgentOptions(env=...)."""
|
|
||||||
return {
|
|
||||||
"ANTHROPIC_BASE_URL": os.environ["ANTHROPIC_BASE_URL"],
|
|
||||||
"ANTHROPIC_AUTH_TOKEN": os.environ.get("ANTHROPIC_AUTH_TOKEN", ""),
|
|
||||||
"CLAUDE_CODE_OAUTH_TOKEN": os.environ.get("CLAUDE_CODE_OAUTH_TOKEN", ""),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def make_tmpdir(prefix: str = "sdk-test-") -> Path:
|
|
||||||
"""Create a temporary directory manually (caller is responsible for cleanup)."""
|
|
||||||
return Path(tempfile.mkdtemp(prefix=prefix))
|
|
||||||
|
|
||||||
|
|
||||||
def remove_tmpdir(path: Path) -> None:
|
|
||||||
"""Remove a temporary directory, retrying once on Windows lock errors."""
|
|
||||||
try:
|
|
||||||
shutil.rmtree(path)
|
|
||||||
except OSError:
|
|
||||||
import time
|
|
||||||
time.sleep(1)
|
|
||||||
shutil.rmtree(path, ignore_errors=True)
|
|
||||||
@ -1,394 +0,0 @@
|
|||||||
# Agent SDK overview
|
|
||||||
|
|
||||||
Build production AI agents with Claude Code as a library
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
The Claude Code SDK has been renamed to the Claude Agent SDK. If you're migrating from the old SDK, see the [Migration Guide](/docs/en/agent-sdk/migration-guide).
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
Build AI agents that autonomously read files, run commands, search the web, edit code, and more. The Agent SDK gives you the same tools, agent loop, and context management that power Claude Code, programmable in Python and TypeScript.
|
|
||||||
|
|
||||||
```python
|
|
||||||
import asyncio
|
|
||||||
from claude_agent_sdk import query, ClaudeAgentOptions
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
async for message in query(
|
|
||||||
prompt="Find and fix the bug in auth.py",
|
|
||||||
options=ClaudeAgentOptions(allowed_tools=["Read", "Edit", "Bash"]),
|
|
||||||
):
|
|
||||||
print(message) # Claude reads the file, finds the bug, edits it
|
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
```
|
|
||||||
|
|
||||||
The Agent SDK includes built-in tools for reading files, running commands, and editing code, so your agent can start working immediately without you implementing tool execution. Dive into the quickstart or explore real agents built with the SDK:
|
|
||||||
|
|
||||||
<CardGroup cols={2}>
|
|
||||||
<Card title="Quickstart" icon="play" href="/docs/en/agent-sdk/quickstart">
|
|
||||||
Build a bug-fixing agent in minutes
|
|
||||||
</Card>
|
|
||||||
<Card title="Example agents" icon="star" href="https://github.com/anthropics/claude-agent-sdk-demos">
|
|
||||||
Email assistant, research agent, and more
|
|
||||||
</Card>
|
|
||||||
</CardGroup>
|
|
||||||
|
|
||||||
## Get started
|
|
||||||
|
|
||||||
<Steps>
|
|
||||||
<Step title="Install the SDK">
|
|
||||||
```bash
|
|
||||||
pip install claude-agent-sdk
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
<Step title="Set your API key">
|
|
||||||
Get an API key from the [Console](https://platform.claude.com/), then set it as an environment variable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export ANTHROPIC_API_KEY=your-api-key
|
|
||||||
```
|
|
||||||
|
|
||||||
The SDK also supports authentication via third-party API providers:
|
|
||||||
|
|
||||||
- **Amazon Bedrock**: set `CLAUDE_CODE_USE_BEDROCK=1` environment variable and configure AWS credentials
|
|
||||||
- **Google Vertex AI**: set `CLAUDE_CODE_USE_VERTEX=1` environment variable and configure Google Cloud credentials
|
|
||||||
- **Microsoft Azure**: set `CLAUDE_CODE_USE_FOUNDRY=1` environment variable and configure Azure credentials
|
|
||||||
|
|
||||||
See the setup guides for [Bedrock](https://code.claude.com/docs/en/amazon-bedrock), [Vertex AI](https://code.claude.com/docs/en/google-vertex-ai), or [Azure AI Foundry](https://code.claude.com/docs/en/azure-ai-foundry) for details.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
Unless previously approved, Anthropic does not allow third party developers to offer claude.ai login or rate limits for their products, including agents built on the Claude Agent SDK. Please use the API key authentication methods described in this document instead.
|
|
||||||
</Note>
|
|
||||||
</Step>
|
|
||||||
<Step title="Run your first agent">
|
|
||||||
This example creates an agent that lists files in your current directory using built-in tools.
|
|
||||||
|
|
||||||
```python
|
|
||||||
import asyncio
|
|
||||||
from claude_agent_sdk import query, ClaudeAgentOptions
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
async for message in query(
|
|
||||||
prompt="What files are in this directory?",
|
|
||||||
options=ClaudeAgentOptions(allowed_tools=["Bash", "Glob"]),
|
|
||||||
):
|
|
||||||
if hasattr(message, "result"):
|
|
||||||
print(message.result)
|
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
</Steps>
|
|
||||||
|
|
||||||
**Ready to build?** Follow the [Quickstart](/docs/en/agent-sdk/quickstart) to create an agent that finds and fixes bugs in minutes.
|
|
||||||
|
|
||||||
## Capabilities
|
|
||||||
|
|
||||||
Everything that makes Claude Code powerful is available in the SDK:
|
|
||||||
|
|
||||||
<Tabs>
|
|
||||||
<Tab title="Built-in tools">
|
|
||||||
Your agent can read files, run commands, and search codebases out of the box. Key tools include:
|
|
||||||
|
|
||||||
| Tool | What it does |
|
|
||||||
|------|--------------|
|
|
||||||
| **Read** | Read any file in the working directory |
|
|
||||||
| **Write** | Create new files |
|
|
||||||
| **Edit** | Make precise edits to existing files |
|
|
||||||
| **Bash** | Run terminal commands, scripts, git operations |
|
|
||||||
| **Glob** | Find files by pattern (`**/*.ts`, `src/**/*.py`) |
|
|
||||||
| **Grep** | Search file contents with regex |
|
|
||||||
| **WebSearch** | Search the web for current information |
|
|
||||||
| **WebFetch** | Fetch and parse web page content |
|
|
||||||
| **[AskUserQuestion](/docs/en/agent-sdk/user-input#handle-clarifying-questions)** | Ask the user clarifying questions with multiple choice options |
|
|
||||||
|
|
||||||
This example creates an agent that searches your codebase for TODO comments:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import asyncio
|
|
||||||
from claude_agent_sdk import query, ClaudeAgentOptions
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
async for message in query(
|
|
||||||
prompt="Find all TODO comments and create a summary",
|
|
||||||
options=ClaudeAgentOptions(allowed_tools=["Read", "Glob", "Grep"]),
|
|
||||||
):
|
|
||||||
if hasattr(message, "result"):
|
|
||||||
print(message.result)
|
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
```
|
|
||||||
|
|
||||||
</Tab>
|
|
||||||
<Tab title="Hooks">
|
|
||||||
Run custom code at key points in the agent lifecycle. SDK hooks use callback functions to validate, log, block, or transform agent behavior.
|
|
||||||
|
|
||||||
**Available hooks:** `PreToolUse`, `PostToolUse`, `Stop`, `SessionStart`, `SessionEnd`, `UserPromptSubmit`, and more.
|
|
||||||
|
|
||||||
This example logs all file changes to an audit file:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import asyncio
|
|
||||||
from datetime import datetime
|
|
||||||
from claude_agent_sdk import query, ClaudeAgentOptions, HookMatcher
|
|
||||||
|
|
||||||
|
|
||||||
async def log_file_change(input_data, tool_use_id, context):
|
|
||||||
file_path = input_data.get("tool_input", {}).get("file_path", "unknown")
|
|
||||||
with open("./audit.log", "a") as f:
|
|
||||||
f.write(f"{datetime.now()}: modified {file_path}\n")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
async for message in query(
|
|
||||||
prompt="Refactor utils.py to improve readability",
|
|
||||||
options=ClaudeAgentOptions(
|
|
||||||
permission_mode="acceptEdits",
|
|
||||||
hooks={
|
|
||||||
"PostToolUse": [
|
|
||||||
HookMatcher(matcher="Edit|Write", hooks=[log_file_change])
|
|
||||||
]
|
|
||||||
},
|
|
||||||
),
|
|
||||||
):
|
|
||||||
if hasattr(message, "result"):
|
|
||||||
print(message.result)
|
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
```
|
|
||||||
|
|
||||||
[Learn more about hooks →](/docs/en/agent-sdk/hooks)
|
|
||||||
</Tab>
|
|
||||||
<Tab title="Subagents">
|
|
||||||
Spawn specialized agents to handle focused subtasks. Your main agent delegates work, and subagents report back with results.
|
|
||||||
|
|
||||||
Define custom agents with specialized instructions. Include `Agent` in `allowedTools` since subagents are invoked via the Agent tool:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import asyncio
|
|
||||||
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
async for message in query(
|
|
||||||
prompt="Use the code-reviewer agent to review this codebase",
|
|
||||||
options=ClaudeAgentOptions(
|
|
||||||
allowed_tools=["Read", "Glob", "Grep", "Agent"],
|
|
||||||
agents={
|
|
||||||
"code-reviewer": AgentDefinition(
|
|
||||||
description="Expert code reviewer for quality and security reviews.",
|
|
||||||
prompt="Analyze code quality and suggest improvements.",
|
|
||||||
tools=["Read", "Glob", "Grep"],
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
):
|
|
||||||
if hasattr(message, "result"):
|
|
||||||
print(message.result)
|
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
```
|
|
||||||
|
|
||||||
Messages from within a subagent's context include a `parent_tool_use_id` field, letting you track which messages belong to which subagent execution.
|
|
||||||
|
|
||||||
[Learn more about subagents →](/docs/en/agent-sdk/subagents)
|
|
||||||
</Tab>
|
|
||||||
<Tab title="MCP">
|
|
||||||
Connect to external systems via the Model Context Protocol: databases, browsers, APIs, and [hundreds more](https://github.com/modelcontextprotocol/servers).
|
|
||||||
|
|
||||||
This example connects the [Playwright MCP server](https://github.com/microsoft/playwright-mcp) to give your agent browser automation capabilities:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import asyncio
|
|
||||||
from claude_agent_sdk import query, ClaudeAgentOptions
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
async for message in query(
|
|
||||||
prompt="Open example.com and describe what you see",
|
|
||||||
options=ClaudeAgentOptions(
|
|
||||||
mcp_servers={
|
|
||||||
"playwright": {"command": "npx", "args": ["@playwright/mcp@latest"]}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
):
|
|
||||||
if hasattr(message, "result"):
|
|
||||||
print(message.result)
|
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
```
|
|
||||||
|
|
||||||
[Learn more about MCP →](/docs/en/agent-sdk/mcp)
|
|
||||||
</Tab>
|
|
||||||
<Tab title="Permissions">
|
|
||||||
Control exactly which tools your agent can use. Allow safe operations, block dangerous ones, or require approval for sensitive actions.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
For interactive approval prompts and the `AskUserQuestion` tool, see [Handle approvals and user input](/docs/en/agent-sdk/user-input).
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
This example creates a read-only agent that can analyze but not modify code. `allowed_tools` pre-approves `Read`, `Glob`, and `Grep`.
|
|
||||||
|
|
||||||
```python
|
|
||||||
import asyncio
|
|
||||||
from claude_agent_sdk import query, ClaudeAgentOptions
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
async for message in query(
|
|
||||||
prompt="Review this code for best practices",
|
|
||||||
options=ClaudeAgentOptions(
|
|
||||||
allowed_tools=["Read", "Glob", "Grep"],
|
|
||||||
),
|
|
||||||
):
|
|
||||||
if hasattr(message, "result"):
|
|
||||||
print(message.result)
|
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
```
|
|
||||||
|
|
||||||
[Learn more about permissions →](/docs/en/agent-sdk/permissions)
|
|
||||||
</Tab>
|
|
||||||
<Tab title="Sessions">
|
|
||||||
Maintain context across multiple exchanges. Claude remembers files read, analysis done, and conversation history. Resume sessions later, or fork them to explore different approaches.
|
|
||||||
|
|
||||||
This example captures the session ID from the first query, then resumes to continue with full context:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import asyncio
|
|
||||||
from claude_agent_sdk import query, ClaudeAgentOptions
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
session_id = None
|
|
||||||
|
|
||||||
# First query: capture the session ID
|
|
||||||
async for message in query(
|
|
||||||
prompt="Read the authentication module",
|
|
||||||
options=ClaudeAgentOptions(allowed_tools=["Read", "Glob"]),
|
|
||||||
):
|
|
||||||
if hasattr(message, "subtype") and message.subtype == "init":
|
|
||||||
session_id = message.session_id
|
|
||||||
|
|
||||||
# Resume with full context from the first query
|
|
||||||
async for message in query(
|
|
||||||
prompt="Now find all places that call it", # "it" = auth module
|
|
||||||
options=ClaudeAgentOptions(resume=session_id),
|
|
||||||
):
|
|
||||||
if hasattr(message, "result"):
|
|
||||||
print(message.result)
|
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
```
|
|
||||||
|
|
||||||
[Learn more about sessions →](/docs/en/agent-sdk/sessions)
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
### Claude Code features
|
|
||||||
|
|
||||||
The SDK also supports Claude Code's filesystem-based configuration. To use these features, set `setting_sources=["project"]` in your options.
|
|
||||||
|
|
||||||
| Feature | Description | Location |
|
|
||||||
|---------|-------------|----------|
|
|
||||||
| [Skills](/docs/en/agent-sdk/skills) | Specialized capabilities defined in Markdown | `.claude/skills/*/SKILL.md` |
|
|
||||||
| [Slash commands](/docs/en/agent-sdk/slash-commands) | Custom commands for common tasks | `.claude/commands/*.md` |
|
|
||||||
| [Memory](/docs/en/agent-sdk/modifying-system-prompts) | Project context and instructions | `CLAUDE.md` or `.claude/CLAUDE.md` |
|
|
||||||
| [Plugins](/docs/en/agent-sdk/plugins) | Extend with custom commands, agents, and MCP servers | Programmatic via `plugins` option |
|
|
||||||
|
|
||||||
## Compare the Agent SDK to other Claude tools
|
|
||||||
|
|
||||||
The Claude Platform offers multiple ways to build with Claude. Here's how the Agent SDK fits in:
|
|
||||||
|
|
||||||
<Tabs>
|
|
||||||
<Tab title="Agent SDK vs Client SDK">
|
|
||||||
The [Anthropic Client SDK](/docs/en/api/client-sdks) gives you direct API access: you send prompts and implement tool execution yourself. The **Agent SDK** gives you Claude with built-in tool execution.
|
|
||||||
|
|
||||||
With the Client SDK, you implement a tool loop. With the Agent SDK, Claude handles it:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Client SDK: You implement the tool loop
|
|
||||||
response = client.messages.create(...)
|
|
||||||
while response.stop_reason == "tool_use":
|
|
||||||
result = your_tool_executor(response.tool_use)
|
|
||||||
response = client.messages.create(tool_result=result, **params)
|
|
||||||
|
|
||||||
# Agent SDK: Claude handles tools autonomously
|
|
||||||
async for message in query(prompt="Fix the bug in auth.py"):
|
|
||||||
print(message)
|
|
||||||
```
|
|
||||||
|
|
||||||
</Tab>
|
|
||||||
<Tab title="Agent SDK vs Claude Code CLI">
|
|
||||||
Same capabilities, different interface:
|
|
||||||
|
|
||||||
| Use case | Best choice |
|
|
||||||
|----------|-------------|
|
|
||||||
| Interactive development | CLI |
|
|
||||||
| CI/CD pipelines | SDK |
|
|
||||||
| Custom applications | SDK |
|
|
||||||
| One-off tasks | CLI |
|
|
||||||
| Production automation | SDK |
|
|
||||||
|
|
||||||
Many teams use both: CLI for daily development, SDK for production. Workflows translate directly between them.
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
## Changelog
|
|
||||||
|
|
||||||
View the full changelog for SDK updates, bug fixes, and new features:
|
|
||||||
|
|
||||||
- **Python SDK**: [view CHANGELOG.md](https://github.com/anthropics/claude-agent-sdk-python/blob/main/CHANGELOG.md)
|
|
||||||
|
|
||||||
## Reporting bugs
|
|
||||||
|
|
||||||
If you encounter bugs or issues with the Agent SDK:
|
|
||||||
|
|
||||||
- **Python SDK**: [report issues on GitHub](https://github.com/anthropics/claude-agent-sdk-python/issues)
|
|
||||||
|
|
||||||
## Branding guidelines
|
|
||||||
|
|
||||||
For partners integrating the Claude Agent SDK, use of Claude branding is optional. When referencing Claude in your product:
|
|
||||||
|
|
||||||
**Allowed:**
|
|
||||||
- "Claude Agent" (preferred for dropdown menus)
|
|
||||||
- "Claude" (when within a menu already labeled "Agents")
|
|
||||||
- "{YourAgentName} Powered by Claude" (if you have an existing agent name)
|
|
||||||
|
|
||||||
**Not permitted:**
|
|
||||||
- "Claude Code" or "Claude Code Agent"
|
|
||||||
- Claude Code-branded ASCII art or visual elements that mimic Claude Code
|
|
||||||
|
|
||||||
Your product should maintain its own branding and not appear to be Claude Code or any Anthropic product. For questions about branding compliance, contact the Anthropic [sales team](https://www.anthropic.com/contact-sales).
|
|
||||||
|
|
||||||
## License and terms
|
|
||||||
|
|
||||||
Use of the Claude Agent SDK is governed by [Anthropic's Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms), including when you use it to power products and services that you make available to your own customers and end users, except to the extent a specific component or dependency is covered by a different license as indicated in that component's LICENSE file.
|
|
||||||
|
|
||||||
## Next steps
|
|
||||||
|
|
||||||
<CardGroup cols={2}>
|
|
||||||
<Card title="Quickstart" icon="play" href="/docs/en/agent-sdk/quickstart">
|
|
||||||
Build an agent that finds and fixes bugs in minutes
|
|
||||||
</Card>
|
|
||||||
<Card title="Example agents" icon="star" href="https://github.com/anthropics/claude-agent-sdk-demos">
|
|
||||||
Email assistant, research agent, and more
|
|
||||||
</Card>
|
|
||||||
<Card title="Python SDK" icon="code" href="/docs/en/agent-sdk/python">
|
|
||||||
Full Python API reference and examples
|
|
||||||
</Card>
|
|
||||||
</CardGroup>
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
"""Example: ClaudeSDKClient — Write file + resume session.
|
|
||||||
|
|
||||||
Demonstrates:
|
|
||||||
- ClaudeSDKClient (stateful, interactive mode)
|
|
||||||
- Creating a file via Write tool
|
|
||||||
- Capturing session_id from SystemMessage
|
|
||||||
- Resuming a session with options.resume
|
|
||||||
- receive_response() for streaming messages
|
|
||||||
- Manual tmpdir management
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
cd docs/claude
|
|
||||||
../../.venv/Scripts/python test_client_write_resume.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from _sdk_test_common import auth_env, make_tmpdir, remove_tmpdir, setup_auth
|
|
||||||
|
|
||||||
|
|
||||||
async def drain(client):
|
|
||||||
"""Collect text from receive_response(), return (session_id, joined_text)."""
|
|
||||||
from claude_agent_sdk import AssistantMessage, ResultMessage, SystemMessage, TextBlock
|
|
||||||
|
|
||||||
texts: list[str] = []
|
|
||||||
session_id = None
|
|
||||||
async for msg in client.receive_response():
|
|
||||||
if isinstance(msg, SystemMessage) and msg.subtype == "init":
|
|
||||||
session_id = msg.data.get("session_id")
|
|
||||||
elif isinstance(msg, AssistantMessage):
|
|
||||||
for block in msg.content:
|
|
||||||
if isinstance(block, TextBlock):
|
|
||||||
texts.append(block.text)
|
|
||||||
elif isinstance(msg, ResultMessage):
|
|
||||||
session_id = session_id or msg.session_id
|
|
||||||
if msg.result:
|
|
||||||
texts.append(msg.result)
|
|
||||||
return session_id, "\n".join(texts)
|
|
||||||
|
|
||||||
|
|
||||||
async def main() -> int:
|
|
||||||
ok, msg = setup_auth()
|
|
||||||
print(msg)
|
|
||||||
if not ok:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
|
||||||
|
|
||||||
tmpdir = make_tmpdir("write-resume-")
|
|
||||||
try:
|
|
||||||
target = tmpdir / "session_test.txt"
|
|
||||||
|
|
||||||
# --- Turn 1: create file ---
|
|
||||||
print("--- Turn 1: Write ---")
|
|
||||||
async with ClaudeSDKClient(
|
|
||||||
ClaudeAgentOptions(
|
|
||||||
cwd=str(tmpdir),
|
|
||||||
allowed_tools=["Write", "Read"],
|
|
||||||
permission_mode="acceptEdits",
|
|
||||||
max_turns=3,
|
|
||||||
env=auth_env(),
|
|
||||||
)
|
|
||||||
) as client:
|
|
||||||
await client.query(
|
|
||||||
f"Use the Write tool to create session_test.txt "
|
|
||||||
f"in {tmpdir} with exactly the content 'Session 1'."
|
|
||||||
)
|
|
||||||
session_id, response = await drain(client)
|
|
||||||
print(f" Response: {response[:200]}")
|
|
||||||
|
|
||||||
if not target.exists():
|
|
||||||
print("FAIL: file not created")
|
|
||||||
return 2
|
|
||||||
|
|
||||||
print(f" File content: {target.read_text(encoding='utf-8')!r}")
|
|
||||||
print(f" Session ID: {session_id}")
|
|
||||||
|
|
||||||
if not session_id:
|
|
||||||
print("FAIL: no session_id captured")
|
|
||||||
return 3
|
|
||||||
|
|
||||||
# --- Turn 2: resume and verify context ---
|
|
||||||
print("\n--- Turn 2: Resume ---")
|
|
||||||
async with ClaudeSDKClient(
|
|
||||||
ClaudeAgentOptions(
|
|
||||||
cwd=str(tmpdir),
|
|
||||||
resume=session_id,
|
|
||||||
allowed_tools=["Read"],
|
|
||||||
permission_mode="acceptEdits",
|
|
||||||
max_turns=2,
|
|
||||||
env=auth_env(),
|
|
||||||
)
|
|
||||||
) as resumed:
|
|
||||||
await resumed.query(
|
|
||||||
"Without modifying files, tell me the exact filename and content you created."
|
|
||||||
)
|
|
||||||
_, resume_response = await drain(resumed)
|
|
||||||
print(f" Response: {resume_response[:300]}")
|
|
||||||
|
|
||||||
content_ok = target.read_text(encoding="utf-8").strip() == "Session 1"
|
|
||||||
resume_ok = "session_test.txt" in resume_response and "Session 1" in resume_response
|
|
||||||
|
|
||||||
if content_ok and resume_ok:
|
|
||||||
print("\nPASS")
|
|
||||||
return 0
|
|
||||||
print(f"\nFAIL: content_ok={content_ok} resume_ok={resume_ok}")
|
|
||||||
return 4
|
|
||||||
finally:
|
|
||||||
remove_tmpdir(tmpdir)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(asyncio.run(main()))
|
|
||||||
@ -1,111 +0,0 @@
|
|||||||
"""Example: Hooks — audit log + PreToolUse deny.
|
|
||||||
|
|
||||||
Demonstrates:
|
|
||||||
- PostToolUse hook for audit logging
|
|
||||||
- PreToolUse hook to deny dangerous commands
|
|
||||||
- HookMatcher pattern matching by tool name
|
|
||||||
- Hook callback signature and return format
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
cd docs/claude
|
|
||||||
../../.venv/Scripts/python test_hooks_audit_deny.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from _sdk_test_common import auth_env, make_tmpdir, remove_tmpdir, setup_auth
|
|
||||||
|
|
||||||
audit_log: list[dict] = []
|
|
||||||
|
|
||||||
|
|
||||||
async def audit_hook(input_data, tool_use_id, context):
|
|
||||||
"""PostToolUse: record every tool invocation."""
|
|
||||||
audit_log.append({
|
|
||||||
"tool": input_data.get("tool_name"),
|
|
||||||
"input": input_data.get("tool_input"),
|
|
||||||
})
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
async def deny_rm_hook(input_data, tool_use_id, context):
|
|
||||||
"""PreToolUse: block 'rm' commands."""
|
|
||||||
if input_data.get("tool_name") != "Bash":
|
|
||||||
return {}
|
|
||||||
command = input_data.get("tool_input", {}).get("command", "")
|
|
||||||
if "rm " in command:
|
|
||||||
return {
|
|
||||||
"hookSpecificOutput": {
|
|
||||||
"hookEventName": "PreToolUse",
|
|
||||||
"permissionDecision": "deny",
|
|
||||||
"permissionDecisionReason": "rm commands are blocked by policy",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
async def main() -> int:
|
|
||||||
ok, msg = setup_auth()
|
|
||||||
print(msg)
|
|
||||||
if not ok:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
from claude_agent_sdk import (
|
|
||||||
AssistantMessage,
|
|
||||||
ClaudeAgentOptions,
|
|
||||||
HookMatcher,
|
|
||||||
ResultMessage,
|
|
||||||
TextBlock,
|
|
||||||
query,
|
|
||||||
)
|
|
||||||
|
|
||||||
tmpdir = make_tmpdir("hooks-")
|
|
||||||
try:
|
|
||||||
(tmpdir / "data.txt").write_text("important data\n", encoding="utf-8")
|
|
||||||
|
|
||||||
print("--- Query with hooks ---")
|
|
||||||
opts = ClaudeAgentOptions(
|
|
||||||
cwd=str(tmpdir),
|
|
||||||
allowed_tools=["Bash", "Read"],
|
|
||||||
permission_mode="acceptEdits",
|
|
||||||
max_turns=4,
|
|
||||||
env=auth_env(),
|
|
||||||
hooks={
|
|
||||||
"PostToolUse": [HookMatcher(matcher="Bash|Read", hooks=[audit_hook])],
|
|
||||||
"PreToolUse": [HookMatcher(matcher="Bash", hooks=[deny_rm_hook])],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async for msg in query(
|
|
||||||
prompt=(
|
|
||||||
"Do these steps in order:\n"
|
|
||||||
"1. Read data.txt\n"
|
|
||||||
"2. Run 'echo hello'\n"
|
|
||||||
"3. Run 'rm data.txt'\n"
|
|
||||||
"Report what happened for each step."
|
|
||||||
),
|
|
||||||
options=opts,
|
|
||||||
):
|
|
||||||
if isinstance(msg, AssistantMessage):
|
|
||||||
for block in msg.content:
|
|
||||||
if isinstance(block, TextBlock):
|
|
||||||
print(f" Claude: {block.text[:300]}")
|
|
||||||
elif isinstance(msg, ResultMessage):
|
|
||||||
print(f" Result: {msg.result!r:.300}")
|
|
||||||
|
|
||||||
print(f"\nAudit log ({len(audit_log)} entries):")
|
|
||||||
for entry in audit_log:
|
|
||||||
print(f" {entry['tool']}: {entry['input']}")
|
|
||||||
|
|
||||||
file_exists = (tmpdir / "data.txt").exists()
|
|
||||||
print(f"\ndata.txt still exists: {file_exists}")
|
|
||||||
print("PASS" if file_exists else "FAIL: rm was not blocked")
|
|
||||||
return 0 if file_exists else 2
|
|
||||||
finally:
|
|
||||||
remove_tmpdir(tmpdir)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(asyncio.run(main()))
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
"""Example: query() one-shot — Read and Edit files.
|
|
||||||
|
|
||||||
Demonstrates:
|
|
||||||
- .env-based auth (ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN)
|
|
||||||
- query() async iterator for simple one-shot tasks
|
|
||||||
- allowed_tools for auto-approval
|
|
||||||
- permission_mode="acceptEdits"
|
|
||||||
- Reading and editing a file in cwd
|
|
||||||
- Manual tmpdir management (avoids Windows cleanup races)
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
cd docs/claude
|
|
||||||
../../.venv/Scripts/python test_query_read_edit.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from _sdk_test_common import auth_env, make_tmpdir, remove_tmpdir, setup_auth
|
|
||||||
|
|
||||||
|
|
||||||
async def main() -> int:
|
|
||||||
ok, msg = setup_auth()
|
|
||||||
print(msg)
|
|
||||||
if not ok:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
from claude_agent_sdk import (
|
|
||||||
AssistantMessage,
|
|
||||||
ClaudeAgentOptions,
|
|
||||||
ResultMessage,
|
|
||||||
TextBlock,
|
|
||||||
ToolUseBlock,
|
|
||||||
query,
|
|
||||||
)
|
|
||||||
|
|
||||||
tmpdir = make_tmpdir("read-edit-")
|
|
||||||
try:
|
|
||||||
test_file = tmpdir / "hello.txt"
|
|
||||||
test_file.write_text("hello world\n", encoding="utf-8")
|
|
||||||
print(f"cwd: {tmpdir}")
|
|
||||||
|
|
||||||
# --- Step 1: Read ---
|
|
||||||
print("\n--- Read ---")
|
|
||||||
opts = ClaudeAgentOptions(
|
|
||||||
cwd=str(tmpdir),
|
|
||||||
allowed_tools=["Read"],
|
|
||||||
permission_mode="acceptEdits",
|
|
||||||
max_turns=2,
|
|
||||||
env=auth_env(),
|
|
||||||
)
|
|
||||||
async for msg in query(prompt="Read hello.txt and show its content.", options=opts):
|
|
||||||
if isinstance(msg, AssistantMessage):
|
|
||||||
for block in msg.content:
|
|
||||||
if isinstance(block, ToolUseBlock):
|
|
||||||
print(f" ToolUse: {block.name}({block.input})")
|
|
||||||
elif isinstance(block, TextBlock):
|
|
||||||
print(f" Claude: {block.text[:200]}")
|
|
||||||
elif isinstance(msg, ResultMessage):
|
|
||||||
print(f" Result: {msg.result!r:.200}")
|
|
||||||
|
|
||||||
# --- Step 2: Edit ---
|
|
||||||
print("\n--- Edit ---")
|
|
||||||
opts = ClaudeAgentOptions(
|
|
||||||
cwd=str(tmpdir),
|
|
||||||
allowed_tools=["Read", "Edit"],
|
|
||||||
permission_mode="acceptEdits",
|
|
||||||
max_turns=3,
|
|
||||||
env=auth_env(),
|
|
||||||
)
|
|
||||||
async for msg in query(
|
|
||||||
prompt=f"Edit hello.txt in {tmpdir}. Add '# edited' as the first line.",
|
|
||||||
options=opts,
|
|
||||||
):
|
|
||||||
if isinstance(msg, AssistantMessage):
|
|
||||||
for block in msg.content:
|
|
||||||
if isinstance(block, ToolUseBlock):
|
|
||||||
print(f" ToolUse: {block.name}({block.input})")
|
|
||||||
elif isinstance(block, TextBlock):
|
|
||||||
print(f" Claude: {block.text[:200]}")
|
|
||||||
elif isinstance(msg, ResultMessage):
|
|
||||||
print(f" Result: {msg.result!r:.200}")
|
|
||||||
|
|
||||||
content = test_file.read_text(encoding="utf-8")
|
|
||||||
print(f"\nFile after edit:\n{content}")
|
|
||||||
ok = "# edited" in content
|
|
||||||
print("PASS" if ok else "FAIL")
|
|
||||||
return 0 if ok else 2
|
|
||||||
finally:
|
|
||||||
remove_tmpdir(tmpdir)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(asyncio.run(main()))
|
|
||||||
@ -7,4 +7,3 @@ langchain-community>=0.2.0
|
|||||||
pyyaml>=6.0.0
|
pyyaml>=6.0.0
|
||||||
rich>=13.0.0
|
rich>=13.0.0
|
||||||
httpx>=0.27.0
|
httpx>=0.27.0
|
||||||
claude-agent-sdk
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user