Skip to navigation

Coding Agent Adapters

Claude Agent SDK, Codex, OpenCode, GitHub Copilot SDK, and ACP client adapters, with room workspaces and retention

These adapters run a coding agent, usually one local process per room, and serve Band’s tools to it over MCP or the agent’s own tool protocol. They share two mechanisms, room workspaces and room retention, described first. Every adapter follows the shared adapter construction rules and takes the feature keywords.

Room Workspaces

ClaudeSDKAdapter, CodexAdapter, and the ACP client adapters (ACPClientAdapter, CopilotACPAdapter, CursorACPAdapter, OmpACPAdapter) give each room its own workspace directory. The workspace comes from workspace_for_room(room_id), which must return an absolute path. Without a resolver, the SDK creates .band-workspaces/<room_id> under the process working directory.

from band import create_room_workspace_resolver
from band.adapters import CodexAdapter, CodexAdapterConfig
adapter = CodexAdapter(
CodexAdapterConfig(workspace_for_room=create_room_workspace_resolver("/srv/rooms")),
)

create_room_workspace_resolver(root) creates <root>/<room_id> on first use and rejects a room ID that would escape root with ValueError: room id cannot escape the workspace root. A resolver that returns a relative path raises ValueError: workspace_for_room must return an absolute path when a room is first resolved, and one that hands the same directory to two rooms raises ValueError: workspace_for_room assigned '<path>' to both '<room>' and '<room>'.

To keep the isolation guarantee, Codex and the ACP adapters reject transports that share one process across rooms:

AdapterRejected inputError
CodexAdaptercwd in CodexAdapterConfigValueError: cwd is not supported; use workspace_for_room or the default
CodexAdaptertransport="ws"ValueError: only stdio Codex transport guarantees room process isolation
CodexAdapterclient_factoryValueError: custom Codex clients cannot guarantee room process isolation
ACPClientAdapterConfighost or portValidationError (a ValueError): Value error, TCP ACP transport cannot guarantee room process isolation
ACPClientAdapterspawn_processValueError: custom ACP transports cannot guarantee room process isolation

ACPClientAdapterConfig and its presets accept cwd as a root and turn it into one child directory per room. ClaudeSDKAdapterConfig.cwd is different: it names one existing directory that every room shares, which is the pre-5.0.0 behavior; omit it for isolated room workspaces. On both, setting cwd together with the adapter’s workspace_for_room argument raises ValueError: set either cwd or workspace_for_room, not both.

Room Retention

ClaudeSDKAdapterConfig, CodexAdapterConfig, and the ACP client configs carry a retention: RoomRetentionConfig field, from band.core.retention.config, with idle_timeout_s: float | None = None and transition_timeout_s: float. idle_timeout_s must be greater than 0 when set, and transition_timeout_s bounds how long a caller waits for one room acquisition or cleanup to report its result, after which the unfinished step stays owned for a retry. The config defaults transition_timeout_s to 60.0 for ClaudeSDKAdapterConfig and CodexAdapterConfig, and 30.0 for the ACP client configs; a bare RoomRetentionConfig() uses 10.0. The field is the gate for an adapter-owned room retention lifecycle that no shipped adapter enables yet: as of band-sdk 6.0.1 every provider rejects a non-None idle_timeout_s at config construction, because none can prove that a disconnect would not cancel the agent’s asynchronous work. The error names the provider, for example Codex idle retention is unavailable: native idle and terminal state do not account for asynchronous hooks and delegated work; shutdown can cancel that work. Leave retention.idle_timeout_s unset. Leave the field at its default.

ClaudeSDKAdapter

Adapter for the Claude Agent SDK, with Band tools served to Claude Code over a room-bound MCP endpoint.

from band.adapters import ClaudeApprovalOptions, ClaudeCLIOptions, ClaudeSDKAdapter, ClaudeSDKAdapterConfig
adapter = ClaudeSDKAdapter(
config: ClaudeSDKAdapterConfig | None = None,
*,
history_converter: ClaudeSDKHistoryConverter | None = None,
additional_tools: list[CustomToolDef] | None = None,
workspace_for_room: WorkspaceResolver | None = None,
**features: Unpack[FeatureKwargs],
)
ClaudeSDKAdapterConfig fieldTypeDefaultDescription
modelstr | NoneNoneClaude model ID or family alias ("sonnet", "opus", "haiku", "inherit"). None pins "claude-sonnet-5-5"
fallback_modelstr | NoneNoneModel the CLI uses when the primary model is unavailable
custom_sectionstr | NoneNoneAdditional instructions appended to the system prompt
max_thinking_tokensint | NoneNoneExtended-thinking budget, greater than 0. None leaves extended thinking off
effortstr | NoneNoneResponse effort level, typed as the installed claude-agent-sdk’s own literal. None uses the model default
permission_modeClaudePermissionMode"acceptEdits"Claude Code’s native permission mode: "default", "acceptEdits", "plan", "bypassPermissions", "dontAsk", or "auto". "dontAsk" denies every prompt and cannot be combined with approvals. "auto" starts in "default" when the account or model cannot run it
cwdPath | NoneNoneOne existing directory shared by every room’s Claude Code session. Omit it for one workspace per room; see Room Workspaces. A missing directory raises ValidationError with Path does not point to a directory
setting_sourcestuple[str, ...]()Host settings Claude Code loads, from "user", "project", and "local". Empty loads none, isolating the agent from ~/.claude and ./.claude; pass ("user", "project") to opt in
turn_timeout_sfloat | NoneNoneSeconds a turn may run before it is interrupted and a timeout failure is posted to the room. None leaves turns unbounded
send_message_dedup_ttl_secondsfloat30.0Window in which identical band_send_message calls collapse into one post, absorbing CLI or MCP transport retries. 0 disables it
turn_settle_timeout_sfloat10.0Seconds a leave or stop lets a room’s turn wind down on its own before it is cancelled. Must be 0 or greater
retentionRoomRetentionConfigRoomRetentionConfig(transition_timeout_s=60.0)See Room Retention
cliClaudeCLIOptionsClaudeCLIOptions()How the Claude CLI process launches; see below
approvalsClaudeApprovalOptions | NoneNoneBand’s room approval layer for Claude’s native tools. None leaves approvals to permission_mode

ClaudeApprovalOptions holds the room approval settings. Band’s own tools are never gated.

ClaudeApprovalOptions fieldTypeDefaultDescription
mode"manual" | "auto_accept" | "auto_decline"required"manual" asks the room, which answers with /approve <token> or /decline <token>; the other two decide without asking
text_notificationsboolTruePost a room message for each automatic decision
wait_timeout_sfloat300.0Seconds a manual approval waits before timeout_decision applies. Must be less than turn_timeout_s when that is set
timeout_decision"accept" | "decline""decline"Decision applied when a manual approval times out
max_pending_per_roomint50Pending approvals kept per room; the oldest is declined when a new one would exceed it
authorized_sendersfrozenset[str] | NoneNoneSender IDs allowed to approve or decline. None admits any room participant
ClaudeCLIOptions fieldTypeDefaultDescription
cli_pathstr | NoneNoneclaude executable to launch instead of the one bundled with claude-agent-sdk
plugin_dirstuple[str, ...]()Local Claude Code plugin folders to load; their skills load without changing cwd or setting_sources
add_dirstuple[str, ...]()Additional directories Claude may access
envdict[str, str]{}Extra environment variables for the CLI process only
extra_argsdict[str, str | None]{}Additional CLI flags by bare name, such as {"flag": "value"}, or {"flag": None} for a bare flag. A dashed key raises extra_args keys are bare flag names: ...; a flag the adapter owns or that would bypass its permission gating, such as model or dangerously-skip-permissions, raises extra_args may not set adapter-owned CLI flags: ...

An invalid combination raises ValidationError at config construction, for example permission_mode='dontAsk' denies tool calls without consulting approvals; set approvals=None or approvals.wait_timeout_s must be less than turn_timeout_s, or the turn times out before the approval does.

from band.adapters import ClaudeApprovalOptions, ClaudeSDKAdapter, ClaudeSDKAdapterConfig
adapter = ClaudeSDKAdapter(
ClaudeSDKAdapterConfig(
permission_mode="default",
approvals=ClaudeApprovalOptions(mode="manual"),
),
)

Emits TOOL_CALLS, THOUGHTS, and USAGE. See Approval Mode for the room approval flow and Claude Agent SDK for a worked example.

CodexAdapter

Adapter for the OpenAI Codex app-server over JSON-RPC. It runs one stdio Codex process per room; see Room Workspaces.

from band.adapters import CodexAdapter, CodexAdapterConfig
adapter = CodexAdapter(
config: CodexAdapterConfig | None = None,
*,
additional_tools: list[CustomToolDef] | None = None,
history_converter: CodexHistoryConverter | None = None,
client_factory: Callable[[CodexAdapterConfig], CodexClientProtocol] | None = None,
**features: Unpack[FeatureKwargs],
)

client_factory must stay unset: any value raises ValueError. Room replies come only from Band tools: Codex’s native assistant text is posted as thought telemetry when Emit.THOUGHTS is on and never counts as the reply, so a turn that answers only in native text reaches the missing-reply verdict described in Turn Outcome. Emits all four kinds.

CodexAdapterConfig

CodexAdapterConfig has 44 fields and is a pydantic-settings model: every field can be set as a constructor argument or through a CODEX_-prefixed environment variable, and a constructor argument always wins. The table lists the most used; the full list is in codex.py at band-sdk-v6.0.1.

ParameterTypeDefaultDescription
modelstr | NoneNoneModel ID; discovered from Codex when unset, falling back to "gpt-6-luna"
workspace_for_roomWorkspaceResolver | NoneNonePer-room workspace resolver; see Room Workspaces. Never read from the environment
reasoning_effortstr | NoneNoneAn effort the selected model supports, such as "low", "medium", or "high". The config does not validate it; /reasoning <effort> in a room checks it against the efforts Codex reports for the current model
personalitystr"pragmatic""friendly", "pragmatic", or "none"
approval_policystr"never"Codex’s own approval policy
sandboxstr | NoneNone"read-only", "workspace-write", "danger-full-access", or "external-sandbox"
approval_modestr"manual"Band room approvals: "manual", "auto_accept", or "auto_decline"
approval_wait_timeout_sfloat300.0Seconds a manual approval waits before approval_timeout_decision ("decline" by default) applies
custom_sectionstr""Additional instructions added to the system prompt
turn_timeout_sfloat180.0Seconds allowed for one turn
turn_settle_timeout_sfloat10.0Seconds a leave or stop lets a room’s turn wind down on its own before it is cancelled. Must be 0 or greater
client_close_timeout_sfloat | None10.0Upper bound for one attempt to close a room’s Codex client; a close that runs over stays owned and the next cleanup retries it. None removes the bound
skill_rootslist[str][]Absolute paths of extra Codex skill folders, registered when Codex starts. A relative path raises ValidationError (skill_roots must be absolute paths: ...); environment variable CODEX_SKILL_ROOTS takes a JSON list
codex_commandtuple[str, ...] | NoneNoneCommand that starts the Codex app-server; environment variable CODEX_COMMAND takes a whitespace-split string
transportstr"stdio"The only value the adapter accepts is "stdio"
retentionRoomRetentionConfigRoomRetentionConfig(transition_timeout_s=60.0)See Room Retention

fallback_send_agent_text and assistant_text_mode were removed in 5.0.0; passing either raises ValidationError (Extra inputs are not permitted), and CODEX_FALLBACK_SEND_AGENT_TEXT and CODEX_ASSISTANT_TEXT_MODE are no longer read. CODEX_CWD is ignored, because cwd is rejected. See Codex for a worked example.

OpencodeAdapter

Bridges each room to a session on an OpenCode HTTP server and serves Band tools to it over an in-process MCP server shared by all rooms.

from band.adapters import OpencodeAdapter, OpencodeAdapterConfig
adapter = OpencodeAdapter(
config: OpencodeAdapterConfig | None = None,
*,
additional_tools: list[CustomToolDef] | None = None,
history_converter: OpencodeHistoryConverter | None = None,
client_factory: Callable[[OpencodeAdapterConfig], OpencodeClientProtocol] | None = None,
**features: Unpack[FeatureKwargs],
)

The adapter talks to one OpenCode server, http://127.0.0.1:4096 by default, and creates one session per room. Emits TOOL_CALLS, TASK_EVENTS, and USAGE.

OpencodeAdapterConfig

OpencodeAdapterConfig is a pydantic-settings model with the OPENCODE_ environment prefix; a constructor argument always wins.

ParameterTypeDefaultDescription
base_urlstr"http://127.0.0.1:4096"OpenCode server URL
directorystr | NoneNoneDirectory the OpenCode server works in, sent with every request
workspacestr | NoneNoneOpenCode workspace, sent with every request
provider_idstr | NoneNoneOpenCode provider ID
model_idstr | NoneNoneOpenCode model ID
agentstr | NoneNoneOpenCode agent name
variantstr | NoneNoneModel variant forwarded with each prompt
custom_sectionstr""Additional instructions added to the prompt
include_base_instructionsboolFalseInclude the Band base instructions in the prompt
fallback_send_agent_textboolTruePost the model’s final text as the reply when no Band tool replied. Still supported on OpenCode; removed from Codex
turn_timeout_sfloat300.0Seconds allowed for one turn
approval_modestr"manual""manual" forwards permission prompts to the room, which answers approve, always, or reject; "auto_accept" approves each once; "auto_decline" rejects. Band’s own MCP tools are always approved
approval_wait_timeout_sfloat300.0Seconds a manual approval waits before approval_timeout_reply applies
approval_timeout_replystr"reject""once", "always", or "reject"
question_modestr"manual""manual" posts OpenCode questions to the room; "auto_reject" declines them
question_wait_timeout_sfloat300.0Seconds a question waits for an answer from the room
session_title_prefixstr"Band"Prefix of the OpenCode session title, <prefix>: <agent name> / <room_id>
mcp_server_namestr"band"Name the Band MCP server is registered under in OpenCode

See OpenCode for a worked example.

CopilotSDKAdapter

Maps each room to a persistent GitHub Copilot SDK session. Copilot’s built-in shell and file tools are disabled; the model sees Band tools and your additional_tools.

from band.adapters import CopilotSDKAdapter, CopilotSDKAdapterConfig
adapter = CopilotSDKAdapter(
config: CopilotSDKAdapterConfig | None = None,
*,
history_converter: HistoryConverter[CopilotSDKSessionState] | None = None,
additional_tools: list[CustomToolDef] | None = None,
client: Any | None = None,
client_factory: Callable[[], Any] | None = None,
provider: ProviderConfig | None = None,
ask_user: UserInputHandler | Literal["room"] | None = None,
**features: Unpack[FeatureKwargs],
)
ArgumentTypeDefaultDescription
clientAny | NoneNoneA Copilot client you own, shared with other code. With client_factory too, construction raises BandConfigError: Pass either client (shared, caller-owned) or client_factory (adapter-owned), not both.
client_factoryCallable[[], Any] | NoneNoneFactory for a Copilot client the adapter owns
providerProviderConfig | NoneNoneBring-your-own-key model provider, such as ProviderConfig(type="openai", base_url=..., api_key=...). config.model then names the provider’s model
ask_userUserInputHandler | "room" | NoneNoneWhere Copilot’s questions to the user go. None keeps Copilot’s ask_user tool disabled; "room" posts the question to the room; a callable answers on behalf of someone outside the room. Any other value raises BandConfigError

CopilotSDKAdapterConfig

ParameterTypeDefaultDescription
modelstr | NoneNoneCopilot model
custom_sectionstr""Additional instructions added to the prompt
reasoning_effortstr | NoneNoneReasoning effort passed to Copilot
inject_history_on_resume_failureboolTrueWhen a room’s session cannot be resumed, replay the room history into the fresh session
github_tokenstr | NoneNoneGitHub token passed to the Copilot SDK
use_logged_in_userbool | NoneNoneUse the signed-in GitHub user instead of a token
session_id_prefixstr | NoneNoneNamespace for session IDs, to separate agents sharing one host
base_directorystr | NoneNoneCopilot state directory, to separate agents sharing one host
turn_timeout_sfloat120.0Seconds allowed for one turn

Emits TOOL_CALLS, THOUGHTS, and USAGE. The adapter still posts the model’s final text as the reply when no Band tool replied. See GitHub Copilot for a worked example.

ACPClientAdapter

Bridges each room to an external ACP agent process, one stdio subprocess per room.

from band.adapters import ACPClientAdapter, ACPClientAdapterConfig
adapter = ACPClientAdapter(
config: ACPClientAdapterConfig,
*,
additional_tools: list[CustomToolDef] | None = None,
workspace_for_room: WorkspaceResolver | None = None,
profile: ACPClientProfile | None = None,
resolve_session_config: SessionConfigResolver | None = None,
resolve_permission: PermissionResolver | None = None,
client_capabilities: ClientCapabilities | None = None,
spawn_process: SpawnProcess | None = None,
**features: Unpack[FeatureKwargs],
)
ACPClientAdapterConfig fieldTypeDefaultDescription
commandtuple[str, ...]requiredCommand that starts the ACP agent over stdio; a single string is one argv element. Empty, it raises ValueError: ACP stdio transport requires a command
envdict[str, str] | NoneNoneExtra subprocess environment variables
cwdstr | NoneNoneRoot under which each room gets its own workspace directory; see Room Workspaces
mcp_serverstuple[...]()Extra MCP server configs forwarded to each ACP session: HTTP, SSE, ACP, or stdio entries
inject_band_toolsboolTrueInject the room-bound Band MCP server into each ACP session, over HTTP or SSE depending on what the agent advertises at initialize
auth_methodstr | NoneNoneACP auth method to call after initialize
custom_sectionstr""Additional instructions added to the adapter prompt
use_unstable_protocolboolFalseSpeak ACP’s unstable protocol methods
turn_timeout_sfloat300.0Seconds allowed for one turn. On timeout the room gets ACP agent response timed out after 300.0s
turn_settle_timeout_sfloat10.0Seconds a leave or stop lets a room’s turn wind down on its own before it is cancelled. Must be 0 or greater
modelstr | NoneNoneModel selected from each session’s advertised catalog; a value the catalog does not offer fails the turn
reasoning_effortstr | NoneNoneReasoning effort selected the same way
retentionRoomRetentionConfigRoomRetentionConfig(transition_timeout_s=30.0)See Room Retention
ArgumentTypeDescription
additional_toolslist[CustomToolDef] | NoneExtra tools served through the injected Band MCP server
workspace_for_roomWorkspaceResolver | NonePer-room workspace resolver; see Room Workspaces
profileACPClientProfile | NoneHook for runtime-specific ACP extension methods and notifications
resolve_session_configSessionConfigResolver | NoneAsync callable that receives an ACPConfigRequest (room_id, session_id, config_options) and returns the option values to select per room. Cannot be combined with config.model or config.reasoning_effort: ValueError: set either model/reasoning_effort or resolve_session_config, not both
resolve_permissionPermissionResolver | NoneAsync callable that receives an ACPPermissionRequest and returns an offered option ID or None. Unset, the adapter selects an allow option, preferring allow once
client_capabilitiesClientCapabilities | NoneCapabilities advertised at initialize

host and port in the config and spawn_process in the constructor raise ValueError when set; see Room Workspaces. With emit unset, the adapter posts thoughts, tool calls, and plan updates from the ACP session-update stream into the room; see What Each Adapter Supports. Room replies come only from Band tools: native agent text is thought telemetry, and a turn that ends without a band_send_message or band_no_reply call reaches the missing-reply verdict in Turn Outcome. The retired assistant_text_mode field raises Extra inputs are not permitted.

See ACP Client Adapter for a worked example.

ACP Presets

CopilotACPAdapter, CursorACPAdapter, and OmpACPAdapter are ACPClientAdapter presets for one runtime each. Their configs subclass ACPClientAdapterConfig and add the runtime’s own settings.

from band.adapters import (
CopilotACPAdapter,
CopilotACPAdapterConfig,
CursorACPAdapter,
CursorACPAdapterConfig,
OmpACPAdapter,
OmpACPAdapterConfig,
)
CopilotACPAdapter(config: CopilotACPAdapterConfig | None = None, *, additional_tools: list[CustomToolDef] | None = None, workspace_for_room: WorkspaceResolver | None = None, resolve_session_config: SessionConfigResolver | None = None, resolve_permission: PermissionResolver | None = None, **features)
CursorACPAdapter(config: CursorACPAdapterConfig | None = None, *, additional_tools: list[CustomToolDef] | None = None, workspace_for_room: WorkspaceResolver | None = None, resolve_session_config: SessionConfigResolver | None = None, **features)
OmpACPAdapter(config: OmpACPAdapterConfig | None = None, *, additional_tools: list[CustomToolDef] | None = None, workspace_for_room: WorkspaceResolver | None = None, resolve_session_config: SessionConfigResolver | None = None, resolve_permission: PermissionResolver | None = None, spawn_process: SpawnProcess | None = None, **features)
PresetDefault commandApprovalsCredentials
CopilotACPAdapter("copilot", "--acp")resolve_permission; unset selects an allow optiongithub_token sets GITHUB_TOKEN for the subprocess unless env already has it
CursorACPAdapter("agent", "acp")approval_mode, question_mode, and plan_mode, each defaulting to "manual", which posts the decision to the room. question_mode also takes "auto_first" and "auto_cancel". decision_timeout_s defaults to 300.0 and must be less than turn_timeout_s, which defaults to 900.0; max_pending_decisions (default 10) caps pending decisions per room, and decision_authorized_senders (default None, anyone) restricts who may answer. Band’s own tools are pre-approvedapi_key sets CURSOR_API_KEY, auth_token sets CURSOR_AUTH_TOKEN; both together raise set either api_key or auth_token, not both. auth_method accepts "cursor_login"
OmpACPAdapter("omp", "acp", "--approval-mode", "always-ask")approval_mode is "always-ask" (default) or "yolo", which gives the agent full access to its host, and the adapter appends --approval-mode <approval_mode>. command may not carry --yolo, --auto-approve, or --approval-mode write or yolo (Unsafe OMP flag '<flag>' is not allowed). resolve_permission handles tool calls and OMP approval forms. use_unstable_protocol is fixed to Truemodel is a provider-qualified OMP model, such as "openai/gpt-5.4-mini", passed as --model rather than selected from the session catalog. api_key goes in that provider’s environment variable and requires model (api_key needs model: the provider-qualified model picks which provider key env var carries the key); an env entry for the variable wins. Other credentials pass through env

Every preset config inherits env, cwd, mcp_servers, inject_band_tools, custom_section, turn_timeout_s, turn_settle_timeout_s, model, reasoning_effort, and retention from ACPClientAdapterConfig. workspace_for_room and resolve_session_config are adapter arguments. See GitHub Copilot CLI and Cursor for worked examples.