mirror of
https://github.com/anthropics/claude-code-sdk-python.git
synced 2025-12-23 09:19:52 +00:00
Compare commits
15 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3eb12c5a37 | ||
|
|
57e8b6ecd5 | ||
|
|
04347495b8 | ||
|
|
a3df944128 | ||
|
|
91e65b1927 | ||
|
|
27575ae2ca | ||
|
|
a0ce44a3fa | ||
|
|
904c2ec33c | ||
|
|
eba5675328 | ||
|
|
5752f38834 | ||
|
|
0ae5c3285c | ||
|
|
f834ba9e15 | ||
|
|
a1c338726f | ||
|
|
d2b3477a4e | ||
|
|
3cbb9e56be |
16 changed files with 470 additions and 42 deletions
9
.claude/agents/test-agent.md
Normal file
9
.claude/agents/test-agent.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
name: test-agent
|
||||||
|
description: A simple test agent for SDK testing
|
||||||
|
tools: Read
|
||||||
|
---
|
||||||
|
|
||||||
|
# Test Agent
|
||||||
|
|
||||||
|
You are a simple test agent. When asked a question, provide a brief, helpful answer.
|
||||||
49
.dockerignore
Normal file
49
.dockerignore
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Testing/Coverage
|
||||||
|
.coverage
|
||||||
|
.pytest_cache/
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
46
.github/workflows/create-release-tag.yml
vendored
46
.github/workflows/create-release-tag.yml
vendored
|
|
@ -24,12 +24,6 @@ jobs:
|
||||||
VERSION="${BRANCH_NAME#release/v}"
|
VERSION="${BRANCH_NAME#release/v}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Get previous release tag
|
|
||||||
id: previous_tag
|
|
||||||
run: |
|
|
||||||
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo "")
|
|
||||||
echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Create and push tag
|
- name: Create and push tag
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
@ -46,14 +40,34 @@ jobs:
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
# Create release with auto-generated notes
|
VERSION="${{ steps.extract_version.outputs.version }}"
|
||||||
gh release create "v${{ steps.extract_version.outputs.version }}" \
|
|
||||||
--title "Release v${{ steps.extract_version.outputs.version }}" \
|
|
||||||
--generate-notes \
|
|
||||||
--notes-start-tag "${{ steps.previous_tag.outputs.previous_tag }}" \
|
|
||||||
--notes "Published to PyPI: https://pypi.org/project/claude-agent-sdk/${{ steps.extract_version.outputs.version }}/
|
|
||||||
|
|
||||||
### Installation
|
# Extract changelog section for this version to a temp file
|
||||||
\`\`\`bash
|
awk -v ver="$VERSION" '
|
||||||
pip install claude-agent-sdk==${{ steps.extract_version.outputs.version }}
|
/^## / {
|
||||||
\`\`\`"
|
if (found) exit
|
||||||
|
if ($2 == ver) found=1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
found { print }
|
||||||
|
' CHANGELOG.md > release_notes.md
|
||||||
|
|
||||||
|
# Append install instructions
|
||||||
|
{
|
||||||
|
echo ""
|
||||||
|
echo "---"
|
||||||
|
echo ""
|
||||||
|
echo "**PyPI:** https://pypi.org/project/claude-agent-sdk/VERSION/"
|
||||||
|
echo ""
|
||||||
|
echo '```bash'
|
||||||
|
echo "pip install claude-agent-sdk==VERSION"
|
||||||
|
echo '```'
|
||||||
|
} >> release_notes.md
|
||||||
|
|
||||||
|
# Replace VERSION placeholder
|
||||||
|
sed -i "s/VERSION/$VERSION/g" release_notes.md
|
||||||
|
|
||||||
|
# Create release with notes from file
|
||||||
|
gh release create "v$VERSION" \
|
||||||
|
--title "v$VERSION" \
|
||||||
|
--notes-file release_notes.md
|
||||||
|
|
|
||||||
18
.github/workflows/test.yml
vendored
18
.github/workflows/test.yml
vendored
|
|
@ -81,6 +81,24 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
python -m pytest e2e-tests/ -v -m e2e
|
python -m pytest e2e-tests/ -v -m e2e
|
||||||
|
|
||||||
|
test-e2e-docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: test # Run after unit tests pass
|
||||||
|
# Run e2e tests in Docker to catch container-specific issues like #406
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build Docker test image
|
||||||
|
run: docker build -f Dockerfile.test -t claude-sdk-test .
|
||||||
|
|
||||||
|
- name: Run e2e tests in Docker
|
||||||
|
env:
|
||||||
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
run: |
|
||||||
|
docker run --rm -e ANTHROPIC_API_KEY \
|
||||||
|
claude-sdk-test python -m pytest e2e-tests/ -v -m e2e
|
||||||
|
|
||||||
test-examples:
|
test-examples:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: test-e2e # Run after e2e tests
|
needs: test-e2e # Run after e2e tests
|
||||||
|
|
|
||||||
27
CHANGELOG.md
27
CHANGELOG.md
|
|
@ -1,5 +1,32 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.18
|
||||||
|
|
||||||
|
### Internal/Other Changes
|
||||||
|
|
||||||
|
- **Docker-based test infrastructure**: Added Docker support for running e2e tests in containerized environments, helping catch Docker-specific issues (#424)
|
||||||
|
- Updated bundled Claude CLI to version 2.0.72
|
||||||
|
|
||||||
|
## 0.1.17
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
- **UserMessage UUID field**: Added `uuid` field to `UserMessage` response type, making it easier to use the `rewind_files()` method by providing direct access to message identifiers needed for file checkpointing (#418)
|
||||||
|
|
||||||
|
### Internal/Other Changes
|
||||||
|
|
||||||
|
- Updated bundled Claude CLI to version 2.0.70
|
||||||
|
|
||||||
|
## 0.1.16
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **Rate limit detection**: Fixed parsing of the `error` field in `AssistantMessage`, enabling applications to detect and handle API errors like rate limits. Previously, the `error` field was defined but never populated from CLI responses (#405)
|
||||||
|
|
||||||
|
### Internal/Other Changes
|
||||||
|
|
||||||
|
- Updated bundled Claude CLI to version 2.0.68
|
||||||
|
|
||||||
## 0.1.15
|
## 0.1.15
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
|
||||||
29
Dockerfile.test
Normal file
29
Dockerfile.test
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Dockerfile for running SDK tests in a containerized environment
|
||||||
|
# This helps catch Docker-specific issues like #406
|
||||||
|
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
# Install dependencies for Claude CLI and git (needed for some tests)
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Claude Code CLI
|
||||||
|
RUN curl -fsSL https://claude.ai/install.sh | bash
|
||||||
|
ENV PATH="/root/.local/bin:$PATH"
|
||||||
|
|
||||||
|
# Set up working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy the SDK source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Install SDK with dev dependencies
|
||||||
|
RUN pip install -e ".[dev]"
|
||||||
|
|
||||||
|
# Verify CLI installation
|
||||||
|
RUN claude -v
|
||||||
|
|
||||||
|
# Default: run unit tests
|
||||||
|
CMD ["python", "-m", "pytest", "tests/", "-v"]
|
||||||
|
|
@ -38,15 +38,88 @@ async def test_agent_definition():
|
||||||
async for message in client.receive_response():
|
async for message in client.receive_response():
|
||||||
if isinstance(message, SystemMessage) and message.subtype == "init":
|
if isinstance(message, SystemMessage) and message.subtype == "init":
|
||||||
agents = message.data.get("agents", [])
|
agents = message.data.get("agents", [])
|
||||||
assert isinstance(
|
assert isinstance(agents, list), (
|
||||||
agents, list
|
f"agents should be a list of strings, got: {type(agents)}"
|
||||||
), f"agents should be a list of strings, got: {type(agents)}"
|
)
|
||||||
assert (
|
assert "test-agent" in agents, (
|
||||||
"test-agent" in agents
|
f"test-agent should be available, got: {agents}"
|
||||||
), f"test-agent should be available, got: {agents}"
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.e2e
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_filesystem_agent_loading():
|
||||||
|
"""Test that filesystem-based agents load via setting_sources and produce full response.
|
||||||
|
|
||||||
|
This is the core test for issue #406. It verifies that when using
|
||||||
|
setting_sources=["project"] with a .claude/agents/ directory containing
|
||||||
|
agent definitions, the SDK:
|
||||||
|
1. Loads the agents (they appear in init message)
|
||||||
|
2. Produces a full response with AssistantMessage
|
||||||
|
3. Completes with a ResultMessage
|
||||||
|
|
||||||
|
The bug in #406 causes the iterator to complete after only the
|
||||||
|
init SystemMessage, never yielding AssistantMessage or ResultMessage.
|
||||||
|
"""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
# Create a temporary project with a filesystem agent
|
||||||
|
project_dir = Path(tmpdir)
|
||||||
|
agents_dir = project_dir / ".claude" / "agents"
|
||||||
|
agents_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
# Create a test agent file
|
||||||
|
agent_file = agents_dir / "fs-test-agent.md"
|
||||||
|
agent_file.write_text(
|
||||||
|
"""---
|
||||||
|
name: fs-test-agent
|
||||||
|
description: A filesystem test agent for SDK testing
|
||||||
|
tools: Read
|
||||||
|
---
|
||||||
|
|
||||||
|
# Filesystem Test Agent
|
||||||
|
|
||||||
|
You are a simple test agent. When asked a question, provide a brief, helpful answer.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
options = ClaudeAgentOptions(
|
||||||
|
setting_sources=["project"],
|
||||||
|
cwd=project_dir,
|
||||||
|
max_turns=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = []
|
||||||
|
async with ClaudeSDKClient(options=options) as client:
|
||||||
|
await client.query("Say hello in exactly 3 words")
|
||||||
|
async for msg in client.receive_response():
|
||||||
|
messages.append(msg)
|
||||||
|
|
||||||
|
# Must have at least init, assistant, result
|
||||||
|
message_types = [type(m).__name__ for m in messages]
|
||||||
|
|
||||||
|
assert "SystemMessage" in message_types, "Missing SystemMessage (init)"
|
||||||
|
assert "AssistantMessage" in message_types, (
|
||||||
|
f"Missing AssistantMessage - got only: {message_types}. "
|
||||||
|
"This may indicate issue #406 (silent failure with filesystem agents)."
|
||||||
|
)
|
||||||
|
assert "ResultMessage" in message_types, "Missing ResultMessage"
|
||||||
|
|
||||||
|
# Find the init message and check for the filesystem agent
|
||||||
|
for msg in messages:
|
||||||
|
if isinstance(msg, SystemMessage) and msg.subtype == "init":
|
||||||
|
agents = msg.data.get("agents", [])
|
||||||
|
# Agents are returned as strings (just names)
|
||||||
|
assert "fs-test-agent" in agents, (
|
||||||
|
f"fs-test-agent not loaded from filesystem. Found: {agents}"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
# On Windows, wait for file handles to be released before cleanup
|
||||||
|
if sys.platform == "win32":
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.e2e
|
@pytest.mark.e2e
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_setting_sources_default():
|
async def test_setting_sources_default():
|
||||||
|
|
@ -74,12 +147,12 @@ async def test_setting_sources_default():
|
||||||
async for message in client.receive_response():
|
async for message in client.receive_response():
|
||||||
if isinstance(message, SystemMessage) and message.subtype == "init":
|
if isinstance(message, SystemMessage) and message.subtype == "init":
|
||||||
output_style = message.data.get("output_style")
|
output_style = message.data.get("output_style")
|
||||||
assert (
|
assert output_style != "local-test-style", (
|
||||||
output_style != "local-test-style"
|
f"outputStyle should NOT be from local settings (default is no settings), got: {output_style}"
|
||||||
), f"outputStyle should NOT be from local settings (default is no settings), got: {output_style}"
|
)
|
||||||
assert (
|
assert output_style == "default", (
|
||||||
output_style == "default"
|
f"outputStyle should be 'default', got: {output_style}"
|
||||||
), f"outputStyle should be 'default', got: {output_style}"
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
# On Windows, wait for file handles to be released before cleanup
|
# On Windows, wait for file handles to be released before cleanup
|
||||||
|
|
@ -121,9 +194,9 @@ This is a test command.
|
||||||
async for message in client.receive_response():
|
async for message in client.receive_response():
|
||||||
if isinstance(message, SystemMessage) and message.subtype == "init":
|
if isinstance(message, SystemMessage) and message.subtype == "init":
|
||||||
commands = message.data.get("slash_commands", [])
|
commands = message.data.get("slash_commands", [])
|
||||||
assert (
|
assert "testcmd" not in commands, (
|
||||||
"testcmd" not in commands
|
f"testcmd should NOT be available with user-only sources, got: {commands}"
|
||||||
), f"testcmd should NOT be available with user-only sources, got: {commands}"
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
# On Windows, wait for file handles to be released before cleanup
|
# On Windows, wait for file handles to be released before cleanup
|
||||||
|
|
@ -159,11 +232,11 @@ async def test_setting_sources_project_included():
|
||||||
async for message in client.receive_response():
|
async for message in client.receive_response():
|
||||||
if isinstance(message, SystemMessage) and message.subtype == "init":
|
if isinstance(message, SystemMessage) and message.subtype == "init":
|
||||||
output_style = message.data.get("output_style")
|
output_style = message.data.get("output_style")
|
||||||
assert (
|
assert output_style == "local-test-style", (
|
||||||
output_style == "local-test-style"
|
f"outputStyle should be from local settings, got: {output_style}"
|
||||||
), f"outputStyle should be from local settings, got: {output_style}"
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
# On Windows, wait for file handles to be released before cleanup
|
# On Windows, wait for file handles to be released before cleanup
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
|
|
||||||
107
examples/filesystem_agents.py
Normal file
107
examples/filesystem_agents.py
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Example of loading filesystem-based agents via setting_sources.
|
||||||
|
|
||||||
|
This example demonstrates how to load agents defined in .claude/agents/ files
|
||||||
|
using the setting_sources option. This is different from inline AgentDefinition
|
||||||
|
objects - these agents are loaded from markdown files on disk.
|
||||||
|
|
||||||
|
This example tests the scenario from issue #406 where filesystem-based agents
|
||||||
|
loaded via setting_sources=["project"] may silently fail in certain environments.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
./examples/filesystem_agents.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from claude_agent_sdk import (
|
||||||
|
AssistantMessage,
|
||||||
|
ClaudeAgentOptions,
|
||||||
|
ClaudeSDKClient,
|
||||||
|
ResultMessage,
|
||||||
|
SystemMessage,
|
||||||
|
TextBlock,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_agents(msg: SystemMessage) -> list[str]:
|
||||||
|
"""Extract agent names from system message init data."""
|
||||||
|
if msg.subtype == "init":
|
||||||
|
agents = msg.data.get("agents", [])
|
||||||
|
# Agents can be either strings or dicts with a 'name' field
|
||||||
|
result = []
|
||||||
|
for a in agents:
|
||||||
|
if isinstance(a, str):
|
||||||
|
result.append(a)
|
||||||
|
elif isinstance(a, dict):
|
||||||
|
result.append(a.get("name", ""))
|
||||||
|
return result
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Test loading filesystem-based agents."""
|
||||||
|
print("=== Filesystem Agents Example ===")
|
||||||
|
print("Testing: setting_sources=['project'] with .claude/agents/test-agent.md")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Use the SDK repo directory which has .claude/agents/test-agent.md
|
||||||
|
sdk_dir = Path(__file__).parent.parent
|
||||||
|
|
||||||
|
options = ClaudeAgentOptions(
|
||||||
|
setting_sources=["project"],
|
||||||
|
cwd=sdk_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
message_types: list[str] = []
|
||||||
|
agents_found: list[str] = []
|
||||||
|
|
||||||
|
async with ClaudeSDKClient(options=options) as client:
|
||||||
|
await client.query("Say hello in exactly 3 words")
|
||||||
|
|
||||||
|
async for msg in client.receive_response():
|
||||||
|
message_types.append(type(msg).__name__)
|
||||||
|
|
||||||
|
if isinstance(msg, SystemMessage) and msg.subtype == "init":
|
||||||
|
agents_found = extract_agents(msg)
|
||||||
|
print(f"Init message received. Agents loaded: {agents_found}")
|
||||||
|
|
||||||
|
elif isinstance(msg, AssistantMessage):
|
||||||
|
for block in msg.content:
|
||||||
|
if isinstance(block, TextBlock):
|
||||||
|
print(f"Assistant: {block.text}")
|
||||||
|
|
||||||
|
elif isinstance(msg, ResultMessage):
|
||||||
|
print(
|
||||||
|
f"Result: subtype={msg.subtype}, cost=${msg.total_cost_usd or 0:.4f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=== Summary ===")
|
||||||
|
print(f"Message types received: {message_types}")
|
||||||
|
print(f"Total messages: {len(message_types)}")
|
||||||
|
|
||||||
|
# Validate the results
|
||||||
|
has_init = "SystemMessage" in message_types
|
||||||
|
has_assistant = "AssistantMessage" in message_types
|
||||||
|
has_result = "ResultMessage" in message_types
|
||||||
|
has_test_agent = "test-agent" in agents_found
|
||||||
|
|
||||||
|
print()
|
||||||
|
if has_init and has_assistant and has_result:
|
||||||
|
print("SUCCESS: Received full response (init, assistant, result)")
|
||||||
|
else:
|
||||||
|
print("FAILURE: Did not receive full response")
|
||||||
|
print(f" - Init: {has_init}")
|
||||||
|
print(f" - Assistant: {has_assistant}")
|
||||||
|
print(f" - Result: {has_result}")
|
||||||
|
|
||||||
|
if has_test_agent:
|
||||||
|
print("SUCCESS: test-agent was loaded from filesystem")
|
||||||
|
else:
|
||||||
|
print("WARNING: test-agent was NOT loaded (may not exist in .claude/agents/)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "claude-agent-sdk"
|
name = "claude-agent-sdk"
|
||||||
version = "0.1.15"
|
version = "0.1.18"
|
||||||
description = "Python SDK for Claude Code"
|
description = "Python SDK for Claude Code"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|
|
||||||
77
scripts/test-docker.sh
Executable file
77
scripts/test-docker.sh
Executable file
|
|
@ -0,0 +1,77 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Run SDK tests in a Docker container
|
||||||
|
# This helps catch Docker-specific issues like #406
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/test-docker.sh [unit|e2e|all]
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# ./scripts/test-docker.sh unit # Run unit tests only
|
||||||
|
# ANTHROPIC_API_KEY=sk-... ./scripts/test-docker.sh e2e # Run e2e tests
|
||||||
|
# ANTHROPIC_API_KEY=sk-... ./scripts/test-docker.sh all # Run all tests
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 [unit|e2e|all]"
|
||||||
|
echo ""
|
||||||
|
echo "Commands:"
|
||||||
|
echo " unit - Run unit tests only (no API key needed)"
|
||||||
|
echo " e2e - Run e2e tests (requires ANTHROPIC_API_KEY)"
|
||||||
|
echo " all - Run both unit and e2e tests"
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 unit"
|
||||||
|
echo " ANTHROPIC_API_KEY=sk-... $0 e2e"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Building Docker test image..."
|
||||||
|
docker build -f Dockerfile.test -t claude-sdk-test .
|
||||||
|
|
||||||
|
case "${1:-unit}" in
|
||||||
|
unit)
|
||||||
|
echo ""
|
||||||
|
echo "Running unit tests in Docker..."
|
||||||
|
docker run --rm claude-sdk-test \
|
||||||
|
python -m pytest tests/ -v
|
||||||
|
;;
|
||||||
|
e2e)
|
||||||
|
if [ -z "$ANTHROPIC_API_KEY" ]; then
|
||||||
|
echo "Error: ANTHROPIC_API_KEY environment variable is required for e2e tests"
|
||||||
|
echo ""
|
||||||
|
echo "Usage: ANTHROPIC_API_KEY=sk-... $0 e2e"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo "Running e2e tests in Docker..."
|
||||||
|
docker run --rm -e ANTHROPIC_API_KEY \
|
||||||
|
claude-sdk-test python -m pytest e2e-tests/ -v -m e2e
|
||||||
|
;;
|
||||||
|
all)
|
||||||
|
echo ""
|
||||||
|
echo "Running unit tests in Docker..."
|
||||||
|
docker run --rm claude-sdk-test \
|
||||||
|
python -m pytest tests/ -v
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [ -n "$ANTHROPIC_API_KEY" ]; then
|
||||||
|
echo "Running e2e tests in Docker..."
|
||||||
|
docker run --rm -e ANTHROPIC_API_KEY \
|
||||||
|
claude-sdk-test python -m pytest e2e-tests/ -v -m e2e
|
||||||
|
else
|
||||||
|
echo "Skipping e2e tests (ANTHROPIC_API_KEY not set)"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done!"
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
"""Bundled Claude Code CLI version."""
|
"""Bundled Claude Code CLI version."""
|
||||||
|
|
||||||
__cli_version__ = "2.0.62"
|
__cli_version__ = "2.0.74"
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ def parse_message(data: dict[str, Any]) -> Message:
|
||||||
case "user":
|
case "user":
|
||||||
try:
|
try:
|
||||||
parent_tool_use_id = data.get("parent_tool_use_id")
|
parent_tool_use_id = data.get("parent_tool_use_id")
|
||||||
|
uuid = data.get("uuid")
|
||||||
if isinstance(data["message"]["content"], list):
|
if isinstance(data["message"]["content"], list):
|
||||||
user_content_blocks: list[ContentBlock] = []
|
user_content_blocks: list[ContentBlock] = []
|
||||||
for block in data["message"]["content"]:
|
for block in data["message"]["content"]:
|
||||||
|
|
@ -74,10 +75,12 @@ def parse_message(data: dict[str, Any]) -> Message:
|
||||||
)
|
)
|
||||||
return UserMessage(
|
return UserMessage(
|
||||||
content=user_content_blocks,
|
content=user_content_blocks,
|
||||||
|
uuid=uuid,
|
||||||
parent_tool_use_id=parent_tool_use_id,
|
parent_tool_use_id=parent_tool_use_id,
|
||||||
)
|
)
|
||||||
return UserMessage(
|
return UserMessage(
|
||||||
content=data["message"]["content"],
|
content=data["message"]["content"],
|
||||||
|
uuid=uuid,
|
||||||
parent_tool_use_id=parent_tool_use_id,
|
parent_tool_use_id=parent_tool_use_id,
|
||||||
)
|
)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
|
|
@ -120,6 +123,7 @@ def parse_message(data: dict[str, Any]) -> Message:
|
||||||
content=content_blocks,
|
content=content_blocks,
|
||||||
model=data["message"]["model"],
|
model=data["message"]["model"],
|
||||||
parent_tool_use_id=data.get("parent_tool_use_id"),
|
parent_tool_use_id=data.get("parent_tool_use_id"),
|
||||||
|
error=data["message"].get("error"),
|
||||||
)
|
)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
raise MessageParseError(
|
raise MessageParseError(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
"""Version information for claude-agent-sdk."""
|
"""Version information for claude-agent-sdk."""
|
||||||
|
|
||||||
__version__ = "0.1.15"
|
__version__ = "0.1.18"
|
||||||
|
|
|
||||||
|
|
@ -264,8 +264,10 @@ class ClaudeSDKClient:
|
||||||
async def rewind_files(self, user_message_id: str) -> None:
|
async def rewind_files(self, user_message_id: str) -> None:
|
||||||
"""Rewind tracked files to their state at a specific user message.
|
"""Rewind tracked files to their state at a specific user message.
|
||||||
|
|
||||||
Requires file checkpointing to be enabled via the `enable_file_checkpointing` option
|
Requires:
|
||||||
when creating the ClaudeSDKClient.
|
- `enable_file_checkpointing=True` to track file changes
|
||||||
|
- `extra_args={"replay-user-messages": None}` to receive UserMessage
|
||||||
|
objects with `uuid` in the response stream
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_message_id: UUID of the user message to rewind to. This should be
|
user_message_id: UUID of the user message to rewind to. This should be
|
||||||
|
|
@ -273,11 +275,14 @@ class ClaudeSDKClient:
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```python
|
```python
|
||||||
options = ClaudeAgentOptions(enable_file_checkpointing=True)
|
options = ClaudeAgentOptions(
|
||||||
|
enable_file_checkpointing=True,
|
||||||
|
extra_args={"replay-user-messages": None},
|
||||||
|
)
|
||||||
async with ClaudeSDKClient(options) as client:
|
async with ClaudeSDKClient(options) as client:
|
||||||
await client.query("Make some changes to my files")
|
await client.query("Make some changes to my files")
|
||||||
async for msg in client.receive_response():
|
async for msg in client.receive_response():
|
||||||
if isinstance(msg, UserMessage):
|
if isinstance(msg, UserMessage) and msg.uuid:
|
||||||
checkpoint_id = msg.uuid # Save this for later
|
checkpoint_id = msg.uuid # Save this for later
|
||||||
|
|
||||||
# Later, rewind to that point
|
# Later, rewind to that point
|
||||||
|
|
|
||||||
|
|
@ -562,6 +562,7 @@ class UserMessage:
|
||||||
"""User message."""
|
"""User message."""
|
||||||
|
|
||||||
content: str | list[ContentBlock]
|
content: str | list[ContentBlock]
|
||||||
|
uuid: str | None = None
|
||||||
parent_tool_use_id: str | None = None
|
parent_tool_use_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,21 @@ class TestMessageParser:
|
||||||
assert isinstance(message.content[0], TextBlock)
|
assert isinstance(message.content[0], TextBlock)
|
||||||
assert message.content[0].text == "Hello"
|
assert message.content[0].text == "Hello"
|
||||||
|
|
||||||
|
def test_parse_user_message_with_uuid(self):
|
||||||
|
"""Test parsing a user message with uuid field (issue #414).
|
||||||
|
|
||||||
|
The uuid field is needed for file checkpointing with rewind_files().
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
"type": "user",
|
||||||
|
"uuid": "msg-abc123-def456",
|
||||||
|
"message": {"content": [{"type": "text", "text": "Hello"}]},
|
||||||
|
}
|
||||||
|
message = parse_message(data)
|
||||||
|
assert isinstance(message, UserMessage)
|
||||||
|
assert message.uuid == "msg-abc123-def456"
|
||||||
|
assert len(message.content) == 1
|
||||||
|
|
||||||
def test_parse_user_message_with_tool_use(self):
|
def test_parse_user_message_with_tool_use(self):
|
||||||
"""Test parsing a user message with tool_use block."""
|
"""Test parsing a user message with tool_use block."""
|
||||||
data = {
|
data = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue