You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 9c34553
Browse filesBrowse the repository at this point in the historyBrowse files
@@ -53,8 +55,8 @@ The flow: a mention or direct message outside a code channel runs a small routin
53
55
Implementations share structure, not code, so these parts are kept the same by copying them and changing them everywhere at once:
54
56
55
57
-**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.
58
60
-**Prompts.**`agent/prompts.py` is the same word for word in every implementation.
59
61
-**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`.
60
62
-**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`.
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