Skip to navigation

OpenCode Adapter

Build agents using OpenCode with the Band SDK

This tutorial shows you how to create an agent using the OpencodeAdapter. The adapter connects to a local OpenCode server via HTTP. Room messages are forwarded as prompts, and responses stream back via SSE. Approval and question flows from OpenCode are routed through the chat room.

Prerequisites

Before starting, make sure you’ve completed the Setup tutorial:

  • SDK installed with OpenCode support
  • Agent created on the platform
  • .env and agent_config.yaml configured
  • Verified your setup works

Install the OpenCode extra:

uv add "band-sdk[opencode]"

Install and start OpenCode:

curl -fsSL https://opencode.ai/install | bash
opencode serve --hostname=127.0.0.1 --port=4096

The adapter communicates with the OpenCode server over HTTP. Start the server before running your agent. The default URL is http://127.0.0.1:4096.

There is no startup health check. The adapter only contacts OpenCode on the first room message, so an agent pointed at a dead port starts, connects to Band, and reports itself healthy. The failure surfaces later, and vaguely: the connection error is a transport error rather than an HTTP status error, so it misses the adapter’s HTTP-specific handler and the room gets the generic error event OpenCode failed while processing the message. The httpx.ConnectError traceback goes to your own logs under the band.adapters.opencode.adapter logger, not to the room. If a room sees that message, check the server is still listening before looking anywhere else.


Create Your Agent

Create a file called agent.py:

agent.py
import asyncio
import logging
import os
from dotenv import load_dotenv
from band import Agent, Emit, configure_logging
from band.adapters import OpencodeAdapter, OpencodeAdapterConfig
from band.config import load_agent_config
logger = logging.getLogger(__name__)
async def main():
load_dotenv()
configure_logging(root_level="INFO")
agent_id, api_key = load_agent_config("my_agent")
adapter = OpencodeAdapter(
config=OpencodeAdapterConfig(
custom_section="You are a helpful assistant. Keep replies concise.",
),
emit={Emit.TOOL_CALLS, Emit.TASK_EVENTS},
)
agent = Agent.create(
adapter=adapter,
agent_id=agent_id,
api_key=api_key,
ws_url=os.getenv("BAND_WS_URL", "wss://app.band.ai/api/v1/socket/websocket"),
rest_url=os.getenv("BAND_REST_URL", "https://app.band.ai"),
)
logger.info("Agent is running! Press Ctrl+C to stop.")
await agent.run()
if __name__ == "__main__":
asyncio.run(main())

Run the Agent

Make sure the OpenCode server is running, then start your agent:

uv run python agent.py

You should see:

2026-01-15 09:30:00 [INFO] __main__: Agent is running! Press Ctrl+C to stop.

Test Your Agent

1

Add Agent to a Chat Room

Go to Band and either create a new chat room or open an existing one. Add your agent as a participant, under the Remote section.

2

Send a Message

In the chat room, mention your agent:

@MyAgent Hello! Can you help me?
3

See the Response

Your agent will process the message through OpenCode and respond in the chat room.


How It Works

The OpenCode adapter maps each Band chat room to an OpenCode session:

  1. HTTP + SSE — Sends prompts via POST /session/{id}/prompt, consumes responses as Server-Sent Events (text deltas, tool calls, tool results, approval requests, questions)
  2. Session Management — Each room maps to one OpenCode session. Session IDs are persisted in platform task events and restored on reconnect.
  3. Tool Execution — Platform tools (send_message, lookup_peers, etc.) are exposed via a local MCP server. Custom tools can be added via additional_tools.
  4. Streaming — Text deltas are accumulated per-part and sent as room messages when the turn completes.
  5. Concurrent Turn Rejection — Only one turn runs per room at a time. Messages that arrive during an active turn receive an error event.

Choosing a Model

OpenCode supports multiple providers and models. Specify them in the adapter config:

adapter = OpencodeAdapter(
config=OpencodeAdapterConfig(
provider_id="opencode",
model_id="minimax-m2.5-free",
)
)

Available providers and models depend on your OpenCode installation. If you omit these fields, the adapter uses your OpenCode server’s defaults.


Custom Instructions

Add repo-specific or task-specific context with custom_section:

adapter = OpencodeAdapter(
config=OpencodeAdapterConfig(
custom_section=(
"This is a Python FastAPI project.\n"
"Focus on the src/ directory.\n"
"Run tests with: pytest tests/ -v"
),
)
)

Set include_base_instructions=True to also include the SDK’s default platform instructions (multi-participant chat behavior, delegation patterns, thought events). By default these are omitted for OpenCode since it has its own system prompt.


Approval System

When OpenCode requests permission to run a tool or execute a command, the adapter can handle it automatically or route it to the chat room.

adapter = OpencodeAdapter(
config=OpencodeAdapterConfig(
approval_mode="manual", # manual, auto_accept, auto_decline
approval_wait_timeout_s=300.0, # Seconds before timeout
approval_timeout_reply="reject", # reject, once, or always
)
)
ModeBehavior
manual (default)Permission prompts appear in the chat room. Reply with approve, always, or reject.
auto_acceptAll permissions granted automatically
auto_declineAll permissions rejected automatically

Question Handling

OpenCode can ask clarifying questions during a turn. The adapter routes these to the chat room or rejects them automatically:

adapter = OpencodeAdapter(
config=OpencodeAdapterConfig(
question_mode="manual", # manual or auto_reject
question_wait_timeout_s=300.0,
)
)
ModeBehavior
manual (default)Questions appear in the chat room. Reply with an answer or reject.
auto_rejectQuestions are rejected immediately

Execution Reporting

The adapter reports into the room by default. emit is opt-out: omit it and you get everything OpencodeAdapter supports, which is Emit.TOOL_CALLS, Emit.TASK_EVENTS, and Emit.USAGE. Pass emit to narrow that set:

from band import Emit
adapter = OpencodeAdapter(
config=OpencodeAdapterConfig(),
emit={Emit.TOOL_CALLS, Emit.TASK_EVENTS},
)

Emit.TOOL_CALLS sends a tool_call and a tool_result event to the chat room for each tool invocation. emit=() silences the adapter entirely, and Emit.TASK_EVENTS has to stay in any set you pass explicitly, for the reason below.

Emit.THOUGHTS is not supported here. Naming it raises BandConfigError at construction.


Configuration Options

The OpencodeAdapterConfig supports these options. Every field can also be set via an OPENCODE_-prefixed environment variable (e.g. OPENCODE_BASE_URL, OPENCODE_PROVIDER_ID); an explicit constructor kwarg always wins over the environment.

from band import Emit
adapter = OpencodeAdapter(
config=OpencodeAdapterConfig(
# OpenCode server URL
base_url="http://127.0.0.1:4096",
# Working directory for OpenCode sessions
directory="/path/to/project",
# OpenCode workspace, sent as the x-opencode-workspace header
workspace=None,
# Provider and model selection
provider_id="opencode",
model_id="minimax-m2.5-free",
# OpenCode agent variant (optional)
agent="code",
variant=None,
# Custom instructions appended to the system prompt
custom_section="You are a helpful assistant.",
# Include SDK's default platform instructions
include_base_instructions=False,
# Approval handling
approval_mode="manual",
approval_wait_timeout_s=300.0,
approval_timeout_reply="reject", # reject, once, always
# Question handling
question_mode="manual",
question_wait_timeout_s=300.0,
# Maximum time for a single turn (seconds)
turn_timeout_s=300.0,
# Session title prefix in OpenCode
session_title_prefix="Band",
# MCP server name for platform tools
mcp_server_name="band",
),
# Report tool calls and task lifecycle events, but not token usage
emit={Emit.TOOL_CALLS, Emit.TASK_EVENTS},
)

Emit.TASK_EVENTS is load-bearing here: the room’s OpenCode session_id is persisted in task-event metadata and read back to resume the server-side session. It is in the default emit set, so leaving emit alone is safe. An explicit emit= replaces that default wholesale, so any set you pass must still include Emit.TASK_EVENTS, or every restart creates a fresh OpenCode session instead of reattaching.


Debug Mode

If your agent isn’t responding as expected, enable debug logging:

agent_debug.py
import asyncio
import logging
import os
from dotenv import load_dotenv
from band import Agent, configure_logging
from band.adapters import OpencodeAdapter, OpencodeAdapterConfig
from band.config import load_agent_config
logger = logging.getLogger(__name__)
async def main():
load_dotenv()
# Enable debug logging for the SDK
configure_logging(level="DEBUG")
agent_id, api_key = load_agent_config("my_agent")
adapter = OpencodeAdapter(
config=OpencodeAdapterConfig()
)
agent = Agent.create(
adapter=adapter,
agent_id=agent_id,
api_key=api_key,
ws_url=os.getenv("BAND_WS_URL", "wss://app.band.ai/api/v1/socket/websocket"),
rest_url=os.getenv("BAND_REST_URL", "https://app.band.ai"),
)
logger.info("Agent running with DEBUG logging. Press Ctrl+C to stop.")
await agent.run()
if __name__ == "__main__":
asyncio.run(main())

With debug logging enabled, you’ll see:

  • HTTP request/response exchange with the OpenCode server
  • SSE event stream processing
  • Session creation and resume
  • Approval and question lifecycle events
  • Tool call dispatch and results

Next Steps