Skip to content

Commit 9c34553

Browse files
authored
feat: add the Claude Agent SDK coding agent (#16)
1 parent a6a0578 commit 9c34553

70 files changed

Lines changed: 4973 additions & 6 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/dependabot.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ updates:
33
- package-ecosystem: "pip"
44
directories:
55
- "/"
6+
- "/claude-agent-sdk"
67
- "/openai-agents-sdk"
78
- "/pydantic-ai"
89
schedule:

‎.github/workflows/ci.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jobs:
1616
strategy:
1717
matrix:
1818
directory:
19+
- claude-agent-sdk
1920
- openai-agents-sdk
2021
- pydantic-ai
2122
defaults:
@@ -43,6 +44,7 @@ jobs:
4344
strategy:
4445
matrix:
4546
directory:
47+
- claude-agent-sdk
4648
- openai-agents-sdk
4749
- pydantic-ai
4850
defaults:

‎AGENTS.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Guidance for coding agents working in this repository.
66

77
A monorepo for a **Coding Agent** sample for Slack, built with Bolt for Python. Each framework lives in its own directory as a standalone sample:
88

9+
- `claude-agent-sdk/` — built with the **Claude Agent SDK**
910
- `openai-agents-sdk/` — built with the **OpenAI Agents SDK**
1011
- `pydantic-ai/` — built with **Pydantic AI**
1112

@@ -32,6 +33,7 @@ pytest
3233

3334
```
3435
.github/ # Shared CI workflows and dependabot config
36+
claude-agent-sdk/ # Claude Agent SDK implementation
3537
openai-agents-sdk/ # OpenAI Agents SDK implementation
3638
pydantic-ai/ # Pydantic AI implementation
3739
```
@@ -53,8 +55,8 @@ The flow: a mention or direct message outside a code channel runs a small routin
5355
Implementations share structure, not code, so these parts are kept the same by copying them and changing them everywhere at once:
5456

5557
- **The seam between `listeners/` and `agent/`.** Listeners import only from the `agent` package, never from the agent framework, so `listeners/` is the same in every implementation. `agent/__init__.py` provides `answer_request`, `find_session`, `run_turn`, `resume_turn`, `stop_turn`, and `end_session`.
56-
- **Framework-free files.** `agent/turns.py`, `agent/context.py`, `agent/approvals.py`, `agent/sessions.py`, `agent/workspace.py`, `agent/outcome.py`, and `agent/prompts.py` don't import the agent framework. The framework lives in `agent/agent.py`, `agent/guardrails.py`, and `agent/tools/`. `turns.py` calls only `run_request(prompt, deps)`, which returns the reply text, and `start_run(history, text, deps)` and `resume_run(paused, approved, deps)`, which return the `RunOutcome` in `agent/outcome.py`. The paused run is a handle `turns.py` passes back without looking inside, so each framework's way of pausing fits behind it.
57-
- **What the framework side keeps true.** `turns.py` relies on these, so every implementation keeps them: `create_code_channel` sets `deps.code_channel_id`, and `archive_channel` sets `session.archived`. Every code channel tool skips its call once `session.stopped` is set. A paused run's history already records the pending command as not run, so `turns.py` can drop a paused run without resuming it. Beyond that, `turns.py` only checks whether `history` is empty, to know a turn is the first.
58+
- **Framework-free files.** `agent/turns.py`, `agent/context.py`, `agent/approvals.py`, `agent/sessions.py`, `agent/workspace.py`, `agent/outcome.py`, and `agent/prompts.py` don't import the agent framework. The framework lives in `agent/agent.py`, `agent/guardrails.py`, and `agent/tools/`, and the Claude Agent SDK adds `agent/runtime.py` for the deps its tools can't be passed. `turns.py` calls only `run_request(prompt, deps)`, which returns the reply text, and `start_run(history, text, deps)` and `resume_run(paused, approved, deps)`, which return the `RunOutcome` in `agent/outcome.py`. The paused run is a handle `turns.py` passes back without looking inside, so each framework's way of pausing fits behind it.
59+
- **What the framework side keeps true.** `turns.py` relies on these, so every implementation keeps them: `create_code_channel` sets `deps.code_channel_id`, and `archive_channel` sets `session.archived`. Every code channel tool skips its call once `session.stopped` is set. `turns.py` can drop a paused run without resuming it, since the session's history then shows its pending command as not run: the history already records it that way, or the next run answers it that way before anything else. Beyond that, `turns.py` only checks whether `history` is empty, to know a turn is the first.
5860
- **Prompts.** `agent/prompts.py` is the same word for word in every implementation.
5961
- **Tools.** Every implementation offers the same tools with the same names, arguments, and descriptions, written with its framework's tool decorator. File tools stay inside the clone and can't write into `.git`.
6062
- **Command approvals.** `command_needs_approval(command)` is plain Python and the same in every implementation, wrapped in the framework's own approval hook. Read-only commands and local git changes run without asking. Anything that runs the repository's code or reaches outside the clone asks first. Commands run without a shell, so the check and the run split a command the same way. On Windows, `.cmd` and `.bat` programs like `npm` are the exception, since Windows runs them through `cmd.exe`.

‎README.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ A sample Slack agent that showcases [Slack Code](https://docs.slack.dev/ai/slack
77
88
## Choose your framework
99

10-
| App | Directory | Get started | Framework |
11-
| --------------------- | -------------------- | -------------------------------------------- | --------------------------------------------------------------- |
12-
| **OpenAI Agents SDK** | `openai-agents-sdk/` | [View README](./openai-agents-sdk/README.md) | [openai-agents](https://openai.github.io/openai-agents-python/) |
13-
| **Pydantic AI** | `pydantic-ai/` | [View README](./pydantic-ai/README.md) | [pydantic-ai](https://pydantic.dev/docs/ai/overview/) |
10+
| App | Directory | Get started | Framework |
11+
| --------------------- | -------------------- | -------------------------------------------- | -------------------------------------------------------------------------- |
12+
| **Claude Agent SDK** | `claude-agent-sdk/` | [View README](./claude-agent-sdk/README.md) | [claude-agent-sdk](https://platform.claude.com/docs/en/agent-sdk/overview) |
13+
| **OpenAI Agents SDK** | `openai-agents-sdk/` | [View README](./openai-agents-sdk/README.md) | [openai-agents](https://openai.github.io/openai-agents-python/) |
14+
| **Pydantic AI** | `pydantic-ai/` | [View README](./pydantic-ai/README.md) | [pydantic-ai](https://pydantic.dev/docs/ai/overview/) |
1415

1516
## What it can do
1617

‎claude-agent-sdk/.env.sample‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Required, set your Anthropic API key.
2+
ANTHROPIC_API_KEY=YOUR_ANTHROPIC_API_KEY
3+
4+
# Optional, uncomment and set when running without the Slack CLI (python3 app.py).
5+
# SLACK_APP_TOKEN=YOUR_SLACK_APP_TOKEN
6+
# SLACK_BOT_TOKEN=YOUR_SLACK_BOT_TOKEN
7+
8+
# Required for OAuth (app_oauth.py). Set your app's OAuth credentials.
9+
# SLACK_CLIENT_ID=YOUR_SLACK_CLIENT_ID
10+
# SLACK_CLIENT_SECRET=YOUR_SLACK_CLIENT_SECRET
11+
# SLACK_SIGNING_SECRET=YOUR_SLACK_SIGNING_SECRET
12+
13+
# Optional, uncomment and set when using a custom Slack instance.
14+
# SLACK_API_URL=YOUR_SLACK_API_URL

‎claude-agent-sdk/.gitignore‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Build
2+
build/
3+
dist/
4+
docs/_sources/
5+
docs/.doctrees
6+
7+
# Claude Code
8+
.claude/*.local.json
9+
10+
# Coverage
11+
.coverage
12+
cov_*
13+
coverage.xml
14+
15+
# Editors
16+
.idea/
17+
18+
# Environment
19+
.env*
20+
!.env.sample
21+
env*/
22+
venv/
23+
.venv*
24+
25+
# Logs
26+
logs/
27+
28+
# macOS
29+
.DS_Store
30+
31+
# OAuth
32+
data/
33+
34+
# Python
35+
__pycache__/
36+
*.db
37+
*.egg
38+
*.egg-info/
39+
*.eggs/
40+
*.py[cod]
41+
*.so
42+
.mypy_cache/
43+
.pytype/
44+
.python-version
45+
*~
46+
pip
47+
48+
# Testing
49+
.cache
50+
.pytest_cache/
51+
.tox
52+
tmp.txt

‎claude-agent-sdk/.slack/.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
apps.dev.json
2+
cache/
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"manifest": {
3+
"source": "local"
4+
},
5+
"project_id": "e609f4b0-5cdd-4023-8d3a-a5bd0de0f817"
6+
}

‎claude-agent-sdk/.slack/hooks.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"hooks": {
3+
"get-hooks": "python3 -m slack_cli_hooks.hooks.get_hooks"
4+
}
5+
}

‎claude-agent-sdk/AGENTS.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# AGENTS.md
2+
3+
Guidance for coding agents working in this directory. Refer to the root `../AGENTS.md` for repository-wide structure and commands.
4+
5+
## Claude Agent SDK specifics
6+
7+
**Agents (`agent/agent.py`)** are two sets of `ClaudeAgentOptions` with pinned models: a routing agent (`claude-haiku-4-5`) whose only tool is `create_code_channel`, and the coding agent (`claude-sonnet-5-5`) with every other tool. Tools are async `@tool` functions, one per file under `agent/tools/`, each with a JSON Schema for its arguments. They're served in-process by `create_sdk_mcp_server()`, so the model sees each as `mcp__coding-agent__<name>`. Both agents set `tools=[]`, `setting_sources=[]`, and `strict_mcp_config=True`, so they get none of Claude Code's built-in tools and none of the settings, `CLAUDE.md` files, or MCP servers on the machine. `verbatim_prompts=True` keeps an `@path` in a Slack message from reading a local file.
8+
9+
**Deps** reach the tools and hooks through `current_deps` in `agent/runtime.py`, a `ContextVar`, since the SDK calls a tool with only its arguments. `_run` in `agent/agent.py` sets it before the client starts, so the tasks the client starts see it.
10+
11+
**Runs** use a `ClaudeSDKClient` per run from async Bolt listeners, which runs the Claude Code CLI bundled with the SDK. Coding turns allow `MAX_TURNS` model calls, and a turn that uses them all (`error_max_turns`) ends with a message, keeping its history. A `PostToolUse` hook returns `continue_: False` to end the run early once there's nothing left to do: after `create_code_channel` succeeds for the routing agent, and after `archive_channel` for the coding agent. A result with `is_error` raises, so the turn posts a warning.
12+
13+
**Turns** go through `agent/turns.py`, which doesn't import the SDK. A turn that raises posts a warning and moves the session back to `active`, keeping the history from the last finished turn. Model-written replies use `markdown_text`, since the model writes standard markdown.
14+
15+
**Stopping** sets `session.stopped`, and so does archiving the channel from Slack. The `PreToolUse` hook from `gate_tool_calls` (`agent/guardrails.py`) then denies every tool call and tells the model to wrap up, and the turn posts nothing when it finishes.
16+
17+
**Conversation history** lives in the Claude Code session transcript. `session.history` is the list of each run's `ResultMessage`, and the next run resumes from the last one's `session_id`.
18+
19+
**Command approvals.** The `PreToolUse` hook wraps `command_needs_approval()` from `agent/approvals.py` and returns `defer` for a `run_command` that needs approval, which ends the run with `deferred_tool_use` on its result. A paused run is a `PausedRun` (its history, ending with that result), parked in `pending_store` with the ts of its Approve/Deny message, so a click only resumes the run its buttons were posted for. `resume_run` resumes the session without a prompt: the CLI asks the hook about the waiting call again, and the hook answers with the decision. A newer message in the channel retires a pending approval, so a late click can't resume an older run over it. The next run then finds the waiting call on the last result, and the hook hands it to `answer_unanswered`, the `can_use_tool` callback, which denies it as not run and interrupts that old turn, so no model call is spent on it before the new message.
20+
21+
## Testing
22+
23+
Tests are async (`pytest-asyncio` in auto mode) and mock the Slack client with `Mock(AsyncWebClient)`. When mocking SDK objects, use `Mock(spec=...)` or `MagicMock(spec=...)` with the real class (`ClaudeSDKClient`), or build the real dataclass (`ResultMessage`), so a call to a method that doesn't exist fails the test. `tests/test_runs.py` runs the real agents and the bundled CLI against `tests/fake_model.py`, a local stand-in for the Anthropic Messages API that returns scripted tool calls and replies, so the SDK seams are tested without calling Anthropic.

0 commit comments

Comments
 (0)