Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ version: 2
updates:
- package-ecosystem: "npm"
directories:
- "/claude-agent-sdk"
- "/openai-agents-sdk"
schedule:
interval: "daily"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
strategy:
matrix:
directory:
- claude-agent-sdk
- openai-agents-sdk
defaults:
run:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
strategy:
matrix:
directory:
- claude-agent-sdk
- openai-agents-sdk
defaults:
run:
Expand Down
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,3 @@
# Slack CLI
*/.slack/apps.json
*/.slack/apps.dev.json
*/.slack/cache/
*/.slack/config.json
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Guidance for coding agents working in this repository.

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

- `claude-agent-sdk/` — built with the **Claude Agent SDK**
- `openai-agents-sdk/` — built with the **OpenAI Agents SDK**

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.
Expand Down Expand Up @@ -37,6 +38,7 @@ ES modules with kebab-case filenames, JSDoc types checked by `tsc --checkJs`, Bi

```
.github/ # Shared CI workflows and dependabot config
claude-agent-sdk/ # Claude Agent SDK implementation
openai-agents-sdk/ # OpenAI Agents SDK implementation
```

Expand All @@ -58,7 +60,7 @@ Implementations share structure, not code, so these parts are kept the same by c

- **The seam between `listeners/` and `agent/`.** Listeners import only from `agent/index.js`, never from the agent framework, so `listeners/` is the same in every implementation. `agent/index.js` exports `answerRequest`, `findSession`, `runTurn`, `resumeTurn`, `stopTurn`, and `endSession`.
- **Framework-free files.** `agent/turns.js`, `agent/context.js`, `agent/approvals.js`, `agent/sessions.js`, `agent/workspace.js`, `agent/outcome.js`, and `agent/prompts.js` don't import the agent framework. The framework lives in `agent/agent.js`, `agent/guardrails.js`, and `agent/tools/`. `turns.js` calls only `runRequest(prompt, deps)`, which returns the reply text, and `startRun(history, text, deps)` and `resumeRun(paused, approved, deps)`, which return the `RunOutcome` in `agent/outcome.js`. It calls them through its exported `runs` object, so tests can stand in for them. The paused run is a handle `turns.js` passes back without looking inside, so each framework's way of pausing fits behind it.
- **What the framework side keeps true.** `turns.js` relies on these, so every implementation keeps them: `create_code_channel` sets `deps.codeChannelId`, 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.js` can drop a paused run without resuming it. Beyond that, `turns.js` only checks whether `history` is empty, to know a turn is the first.
- **What the framework side keeps true.** `turns.js` relies on these, so every implementation keeps them: `create_code_channel` sets `deps.codeChannelId`, and `archive_channel` sets `session.archived`. Every code channel tool skips its call once `session.stopped` is set. `turns.js` 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.js` only checks whether `history` is empty, to know a turn is the first.
- **Prompts.** `agent/prompts.js` is the same word for word in every implementation.
- **Tools.** Every implementation offers the same tools with the same names, arguments, and descriptions, written with its framework's tool helper. File tools stay inside the clone and can't write into `.git`. A tool that throws tells the model what went wrong.
- **Command approvals.** `commandNeedsApproval(command)` is plain JavaScript 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, and `splitCommand` in `agent/workspace.js` splits a command the same way for the check and the run. On Windows, Node won't start `.cmd` and `.bat` programs like `npm` without a shell, so those don't run there.
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ A sample Slack agent that showcases [Slack Code](https://docs.slack.dev/ai/slack
## Choose your framework

| App | Directory | Get started | Framework |
| --------------------- | -------------------- | -------------------------------------------- | ----------------------------------------------------------- |
| **OpenAI Agents SDK** | `openai-agents-sdk/` | [View README](./openai-agents-sdk/README.md) | [openai-agents](https://openai.github.io/openai-agents-js/) |
| App | Directory | Get started | Framework |
| --------------------- | -------------------- | -------------------------------------------- | -------------------------------------------------------------------------- |
| **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) |
| **OpenAI Agents SDK** | `openai-agents-sdk/` | [View README](./openai-agents-sdk/README.md) | [openai-agents](https://openai.github.io/openai-agents-js/) |

## What it can do

Expand Down
15 changes: 15 additions & 0 deletions claude-agent-sdk/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Required, set your Anthropic API key.
ANTHROPIC_API_KEY=YOUR_ANTHROPIC_API_KEY

# Optional, uncomment and set when running without the Slack CLI (npm start).
# SLACK_APP_TOKEN=YOUR_SLACK_APP_TOKEN
# SLACK_BOT_TOKEN=YOUR_SLACK_BOT_TOKEN

# Optional, uncomment and set for OAuth mode (node app-oauth.js).
# SLACK_CLIENT_ID=YOUR_SLACK_CLIENT_ID
# SLACK_CLIENT_SECRET=YOUR_SLACK_CLIENT_SECRET
# SLACK_SIGNING_SECRET=YOUR_SLACK_SIGNING_SECRET
# SLACK_STATE_SECRET=YOUR_SLACK_STATE_SECRET

# Optional, uncomment and set when using a custom Slack instance.
# SLACK_API_URL=YOUR_SLACK_API_URL
18 changes: 18 additions & 0 deletions claude-agent-sdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Claude Code
.claude/*.local.json

# Environment
.env*
!.env.sample

# Logs
logs/

# macOS
.DS_Store

# Node.js
node_modules/

# OAuth
data/
2 changes: 2 additions & 0 deletions claude-agent-sdk/.slack/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
apps.dev.json
cache/
6 changes: 6 additions & 0 deletions claude-agent-sdk/.slack/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"manifest": {
"source": "local"
},
"project_id": "452c1566-00e1-424d-bcca-3ee66ffe370b"
}
5 changes: 5 additions & 0 deletions claude-agent-sdk/.slack/hooks.json
Comment thread
zimeg marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"hooks": {
"get-hooks": "npx -q --no-install -p @slack/cli-hooks slack-cli-get-hooks"
}
}
23 changes: 23 additions & 0 deletions claude-agent-sdk/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# AGENTS.md

Guidance for coding agents working in this directory. Refer to the root `../AGENTS.md` for repository-wide structure and commands.

## Claude Agent SDK specifics

**Agents (`agent/agent.js`)** are two sets of `query()` options with pinned models: a routing `requestAgent` (`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 `tool()` definitions with zod arguments, one per file under `agent/tools/`. Each file exports a function of the run's deps (`AgentDeps` or `RequestDeps`, both in `agent/sessions.js`) that returns the tool, since the SDK calls a tool with only its arguments. Each run serves its tools with `createSdkMcpServer()`, so the model sees each as `mcp__coding-agent__<name>`. Optional arguments are `.optional()`, and the model leaves them out. Both agents set `tools: []`, `settingSources: []`, and `strictMcpConfig: 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. `verbatimPrompts: true` keeps an `@path` in a Slack message from reading a local file.

**Runs** call the SDK's `query()` once per run from 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` throws, so the turn posts a warning. Every run also names its MCP server in `--mcp-config`: the SDK registers the server only once the CLI starts, and resuming a deferred call looks for its tool before that.

**Turns** go through `agent/turns.js`, which doesn't import the SDK. A turn that throws 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.

**Tool errors.** A tool that throws reaches the model as an error result with the error's message, which the SDK's MCP server does for every tool. A Slack API error a tool expects, a `WebAPIPlatformError`, is returned to the model as the tool's result.

**Stopping** sets `session.stopped`, and so does archiving the channel from Slack. The `PreToolUse` hook from `gateToolCalls` (`agent/guardrails.js`) then denies every tool call and tells the model to wrap up, and the turn posts nothing when it finishes.

**Conversation history** lives in the Claude Code session transcript. `session.history` is the list of each run's result message, and the next run resumes from the last one's `session_id`.

**Command approvals.** The `PreToolUse` hook wraps `commandNeedsApproval()` from `agent/approvals.js` 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 `pendingStore` with the ts of its Approve/Deny message, so a click only resumes the run its buttons were posted for. `resumeRun` resumes the session with a prompt that sends nothing: 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 `answerUnanswered`, the `canUseTool` callback, which denies it as not run and interrupts that old turn, so no model call is spent on it before the new message.

## Testing

Tests use Node's test runner (`node --test`) and fake the Slack client with `fakeClient()` from `tests/helpers.js`, which mirrors the real client's methods. Listener tests stand in for the agent's runs with `t.mock.method(runs, 'startRun', ...)`, and `tests/agent/agent.test.js` stands in for the SDK with `t.mock.method(sdk, 'query', ...)`. `tests/invoke.js` calls a tool through its MCP server, the way a run does. `tests/agent/runs.test.js` runs the real agents and the bundled CLI against `tests/fake-model.js`, a local stand-in for the Anthropic Messages API that returns scripted tool calls and replies, passed to each run through the `env` option, so the SDK seams are tested without calling Anthropic.
21 changes: 21 additions & 0 deletions claude-agent-sdk/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Slack Technologies, LLC

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading
Loading