Skip to content

Commit 4f1c6eb

Browse files
zimegClaude
andcommitted
Merge branch 'main' into clack/coding-agent-claude-agent-sdk
Adds the claude-agent-sdk row to the framework table alongside pydantic-ai. Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
2 parents 3a8b7c5 + a6a0578 commit 4f1c6eb

69 files changed

Lines changed: 4670 additions & 0 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
@@ -5,6 +5,7 @@ updates:
55
- "/"
66
- "/claude-agent-sdk"
77
- "/openai-agents-sdk"
8+
- "/pydantic-ai"
89
schedule:
910
interval: "daily"
1011
cooldown:

‎.github/workflows/ci.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ jobs:
1818
directory:
1919
- claude-agent-sdk
2020
- openai-agents-sdk
21+
- pydantic-ai
2122
defaults:
2223
run:
2324
working-directory: ${{ matrix.directory }}
@@ -45,6 +46,7 @@ jobs:
4546
directory:
4647
- claude-agent-sdk
4748
- openai-agents-sdk
49+
- pydantic-ai
4850
defaults:
4951
run:
5052
working-directory: ${{ matrix.directory }}

‎AGENTS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ A monorepo for a **Coding Agent** sample for Slack, built with Bolt for Python.
88

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

1213
Implementations share a structure and user experience, not code. Keep each one obvious on its own, written in its framework's idiom, rather than adding shared helpers.
1314

@@ -34,6 +35,7 @@ pytest
3435
.github/ # Shared CI workflows and dependabot config
3536
claude-agent-sdk/ # Claude Agent SDK implementation
3637
openai-agents-sdk/ # OpenAI Agents SDK implementation
38+
pydantic-ai/ # Pydantic AI implementation
3739
```
3840

3941
CI runs ruff and pytest against each directory with a matrix in `.github/workflows/ci.yml`. Dependabot watches each directory separately. When adding an implementation, add its directory to both job matrices, to `.github/dependabot.yml`, and to the framework table in the root `README.md`.

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ A sample Slack agent that showcases [Slack Code](https://docs.slack.dev/ai/slack
1111
| --------------------- | -------------------- | -------------------------------------------- | -------------------------------------------------------------------------- |
1212
| **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) |
1313
| **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

‎pydantic-ai/.env.sample‎

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

‎pydantic-ai/.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

‎pydantic-ai/.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/

‎pydantic-ai/.slack/config.json‎

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": "3d533454-d0ab-41a8-a99d-e0ac11c33ad9"
6+
}

‎pydantic-ai/.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+
}

‎pydantic-ai/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+
## Pydantic AI specifics
6+
7+
**Agents (`agent/agent.py`)** are two Pydantic AI `Agent`s: a routing `request_agent` whose only tool is `create_code_channel`, and the coding `agent` with every other tool. Neither sets a model. A `SelectModel` capability picks one when a run starts, by which API key is set: Anthropic (`claude-haiku-4-5` to route, `claude-sonnet-5-5` to code) when `ANTHROPIC_API_KEY` is set, else OpenAI (`gpt-6-luna` and `gpt-6.1-sol`). So no model client is created at import. Tools are async functions, one per file under `agent/tools/`, passed with `tools=[...]`. Each takes `RunContext[AgentDeps]` (or `RequestDeps`, both in `agent/sessions.py`) for the Slack client and session, and Pydantic AI reads its docstring for the description the model sees. The coding agent lists `AnthropicCompaction` and `OpenAICompaction`, so a long session compacts its oldest conversation instead of outgrowing the context window. Each only applies to its own provider's models.
8+
9+
**Ending a run.** `create_code_channel` and `archive_channel` are output functions (`ToolOutput`), not plain tools, so a successful call ends the run as its output with no further model call. That's Pydantic AI's way to end a run with a tool call. A Slack error in either raises `ModelRetry`, which sends the error back to the model to recover from, such as by replying in place.
10+
11+
**Runs** go through `agent.iter()` from async Bolt listeners. Coding turns allow `MAX_TURNS` model requests with `UsageLimits`. A turn that uses them all ends with a message, and keeps its history by reading the run's messages when `UsageLimitExceeded` is raised.
12+
13+
**Turns** go through `agent/turns.py`, which doesn't import Pydantic AI. A turn that raises posts a warning and moves the session back to `active`, keeping the history from the last finished turn. A tool that raises doesn't fail the turn: the `tool_execute_error` hook logs it and tells the model the tool failed with `ToolFailed`. 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 `guardrails` capability (`agent/guardrails.py`) skips every tool call once it's set, with `SkipToolExecution`, and tells the model to wrap up. `archive_channel` is an output function, so a hook on the output does the same for it. The turn posts nothing when it finishes.
16+
17+
**Conversation history** is the run's `all_messages()`, a `list[ModelMessage]` stored on the `CodeChannelSession` (`agent/sessions.py`), keyed by code channel. Follow-up turns pass it as `message_history` with the new text as the prompt.
18+
19+
**Command approvals.** `run_command_needs_approval` in `agent/guardrails.py` is a `before_tool_execute` hook for `run_command` that raises `ApprovalRequired` when `command_needs_approval()` from `agent/approvals.py` says so. That ends the run with `DeferredToolRequests` as its output. A paused run is a `PausedRun` (the messages, the requests, and the usage so far), parked in `pending_store` with the ts of its Approve/Deny message, so a click only resumes the run its buttons were posted for. Resuming passes `deferred_tool_results` with `True` or a `ToolDenied`, and the usage, so the turn keeps counting toward `MAX_TURNS`. Every pending call needs an answer, so if one step asked for more than one command, the others are answered as not run and the model can ask again. A newer message in the channel retires a pending approval, so a late click can't resume an older run over it.
20+
21+
## Testing
22+
23+
Tests are async (`pytest-asyncio` in auto mode) and mock the Slack client with `Mock(AsyncWebClient)`. When mocking Pydantic AI objects, use `Mock(spec=...)` with the real class (`AgentRunResult`) so a call to a method that doesn't exist fails the test. `tests/test_runs.py` runs the real agents against `tests/fake_model.py`, a `FunctionModel` that returns scripted tool calls and replies, swapped in with `Agent.override`, so the Pydantic AI seams are tested without calling a provider. `models.ALLOW_MODEL_REQUESTS = False` in `tests/conftest.py` keeps a test from reaching a real model by mistake.

0 commit comments

Comments
 (0)