Skip to content
Open
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
29 changes: 12 additions & 17 deletions docs/acp.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ Two-layer pattern (mirrors A2A Gateway):
| Platform Bridge | `BandACPServerAdapter` | `ACPClientAdapter` |

**Server**: Editor -> ACP -> `ACPServer` -> `BandACPServerAdapter` -> Band REST/WS -> Peers
**Client**: Band room message -> `ACPClientAdapter` -> stdio subprocess **or** TCP connection (Codex, Claude Code, Cursor, GitHub Copilot, etc.)
**Client**: Band room message -> `ACPClientAdapter` -> its room-owned stdio subprocess (Codex, Claude Code, Cursor, GitHub Copilot, etc.)

## Key Files

| File | Purpose |
|------|---------|
| `src/band/integrations/acp/server.py` | `ACPServer` — handles ACP JSON-RPC methods, does not subclass `acp.Agent`; `run_acp_server` — runs it with `use_unstable_protocol` (required for `session/fork`, `session/resume`, `session/close`) |
| `src/band/integrations/acp/server_adapter.py` | `BandACPServerAdapter` — REST client, room/session mapping |
| `src/band/integrations/acp/client_adapter.py` | `ACPClientAdapter` — drives a remote ACP agent over stdio-spawn or TCP-connect |
| `src/band/integrations/acp/client_adapter.py` | `ACPClientAdapter` — drives a room-owned ACP agent over stdio |
| `src/band/integrations/acp/client_runtime.py` | `ACPRuntime` (transport-agnostic) + `ACPCollectingClient` (session_update parsing / coalescing / collapse / live sink), `tcp_spawn_process` (TCP connect seam) |
| `src/band/integrations/acp/room_emitter.py` | `RoomTurnEmitter` — posts a turn's chunks to the room in causal order; `turn_replied_in_room` (text-fallback suppression) |
| `src/band/adapters/copilot_acp.py` | `CopilotACPAdapter` — thin `ACPClientAdapter` for the GitHub Copilot CLI |
Expand Down Expand Up @@ -108,21 +108,16 @@ acp = ["agent-client-protocol"]

Install with: `pip install band-sdk[acp]` or `uv add band-sdk[acp]`

## Client transports (stdio / TCP)

`ACPClientAdapter` selects a transport at construction; both flow through `ACPRuntime`'s
injectable `spawn_process` seam, so the runtime and downstream code are transport-agnostic.

- **stdio** (default): pass `command=[...]` to spawn the agent as a subprocess
(`acp.spawn_agent_process`).
- **TCP**: pass `host=` + `port=` to connect to an already-running ACP server
(`tcp_spawn_process` → `asyncio.open_connection` → `acp.connect_to_agent`). Use for an
ACP agent in a remote/containerized environment.
- Exactly one of `{command, (host, port)}` is required (validated in `__init__`).
- Advanced: inject a custom `spawn_process` (e.g. `docker exec -i … copilot --acp`, ssh,
or a fake in tests). Tests inject a fake through this seam rather than patching module
globals (see `tests/integrations/acp/conftest.py::FakeSpawn` / the `make_acp_transport`
fixture).
## Client workspace isolation

`ACPClientAdapter` creates an isolated `./.band-workspaces/<room-id>` directory for
each Band room by default. Pass `workspace_for_room` only to select a different
absolute workspace policy. It lazily starts one stdio agent process per room and
stops that process when the room is cleaned up. TCP and custom transport injection are
rejected because they cannot prove that a remote process belongs to only one room.
The assigned working directory is not an operating-system sandbox; configure the agent's
sandbox policy separately when that boundary is required. A custom resolver must assign a
different workspace to every live room, and the adapter requires a non-empty stdio command.

## GitHub Copilot CLI backend

Expand Down
36 changes: 10 additions & 26 deletions docs/adapters/codex.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Codex Adapter

[OpenAI Codex](https://openai.com/codex) is a coding agent runtime that can inspect files, edit files, run commands, and manage approval workflows. The Band Codex adapter connects a Codex process to Band rooms over stdio or WebSocket so it can take part in conversations as a coding collaborator.
[OpenAI Codex](https://openai.com/codex) is a coding agent runtime that can inspect files, edit files, run commands, and manage approval workflows. The Band Codex adapter connects one room-owned Codex process to each Band room over stdio.

Use this adapter when you want an OpenAI-powered coding agent with configurable sandboxing, approval commands, command/file-change telemetry, reasoning visibility, and task lifecycle events. Use the [Claude SDK adapter](claude_sdk.md) for Claude Code based coding agents, the [Anthropic adapter](anthropic.md) for direct Claude API chat/tool agents, or the [LangGraph adapter](langgraph.md) for custom graph workflows.

Expand All @@ -26,47 +26,31 @@ You need two credentials or auth contexts:
- A Band platform API key for `Agent.create(api_key=...)`.
- Codex authentication for the Codex process. Use `codex login`, or set `OPENAI_API_KEY` if that is how your Codex environment is configured.

For `transport="ws"`, start the Codex app server separately:

```bash
codex app-server --listen ws://127.0.0.1:8765
```

Credentials for Band can also be loaded from `agent_config.yaml` with `Agent.from_config("my_agent", adapter=adapter)`.

## Quick Start

```python
import asyncio
import os

from band import Agent
from band.adapters.codex import CodexAdapter, CodexAdapterConfig

adapter = CodexAdapter(
config=CodexAdapterConfig(
cwd=os.getcwd(),
model="gpt-5.5",
),
config=CodexAdapterConfig(model="gpt-5.5"),
)

agent = Agent.create(
adapter=adapter,
agent_id="your-agent-uuid",
api_key="your-band-api-key",
ws_url="wss://app.band.ai/api/v1/socket/websocket",
rest_url="https://app.band.ai",
)

asyncio.run(agent.run())
assert adapter.config.model == "gpt-5.5"
```

## Where Parameters Go

Codex has three setup layers:

- `CodexAdapterConfig(...)` configures the Codex runtime: transport, model, working directory, sandbox, approval behavior, prompts, context injection, and streaming/telemetry detail.
- `CodexAdapter(...)` wraps that runtime config for Band and adds adapter-level settings: feature flags, custom tools, history conversion, and advanced client injection.
- `CodexAdapterConfig(...)` configures the Codex runtime: a room workspace resolver, model, sandbox, approval behavior, prompts, context injection, and streaming/telemetry detail.
- `CodexAdapter(...)` wraps that runtime config for Band and adds adapter-level settings: feature flags, custom tools, and history conversion.
- `Agent.create(...)` connects the configured adapter to Band. Use it for the Band agent identity, Band API key, platform URLs, session settings, contact-event handling, callbacks, and preprocessing.

Codex authentication is handled by `codex login`, `OPENAI_API_KEY`, or the Codex process environment. `Agent.create(api_key=...)` is only the Band platform key.
Expand Down Expand Up @@ -104,7 +88,10 @@ from band.core.types import Emit
from band.adapters.codex import CodexAdapter, CodexAdapterConfig

adapter = CodexAdapter(
config=CodexAdapterConfig(cwd="/repo", sandbox="workspace-write"),
config=CodexAdapterConfig(
workspace_for_room=lambda room_id: f"/workspaces/{room_id}",
sandbox="workspace-write",
),
emit=Emit.TOOL_CALLS | Emit.TASK_EVENTS,
)
```
Expand All @@ -115,12 +102,11 @@ Pass these to `CodexAdapterConfig(...)`:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `transport` | `"stdio" \| "ws"` | `"stdio"` | How the adapter connects to Codex. Use `"stdio"` to spawn a process, or `"ws"` to connect to `codex app-server`. |
| `workspace_for_room` | `Callable[[str], str] | None` | `None` | Optional override for a room workspace. By default, the adapter creates `./.band-workspaces/<room-id>`. |
| `model` | `str \| None` | `None` | Model to use. When unset, the adapter asks Codex for visible models and uses the first visible model, or the adapter default if discovery fails or returns no usable model. |
| `reasoning_effort` | `"none" \| "minimal" \| "low" \| "medium" \| "high" \| "xhigh" \| None` | `None` | Reasoning effort for models that support it. |
| `reasoning_summary` | `"auto" \| "concise" \| "detailed" \| "none" \| None` | `None` | How Codex summarizes reasoning in responses. |
| `personality` | `"friendly" \| "pragmatic" \| "none"` | `"pragmatic"` | Codex response style. |
| `cwd` | `str \| None` | `None` | Working directory for Codex sessions. |
| `turn_timeout_s` | `float` | `180.0` | Maximum seconds to wait for one Codex turn. |

### Safety, Sandbox, and Approvals
Expand Down Expand Up @@ -170,7 +156,6 @@ Pass these to `CodexAdapterConfig(...)`:
|-----------|------|---------|-------------|
| `codex_command` | `tuple[str, ...] \| None` | `None` | Custom command used to launch Codex for stdio transport. |
| `codex_env` | `dict[str, str] \| None` | `None` | Extra environment variables for the Codex process. |
| `codex_ws_url` | `str` | `"ws://127.0.0.1:8765"` | WebSocket URL for `transport="ws"`. |
| `experimental_api` | `bool` | `True` | Use experimental Codex API features. |
| `enable_self_config_tools` | `bool` | `False` | Expose tools that let Codex change its own model and reasoning settings. Use only in trusted rooms. |
| `additional_dynamic_tools` | `list[dict]` | `[]` | Extra dynamic tool schemas registered with the Codex client. |
Expand All @@ -187,7 +172,6 @@ Pass these directly to `CodexAdapter(...)`:
| `capabilities` | `Capability \| Iterable[Capability] \| None` | none | Optional Band tool categories exposed to the model. Opt-in: omitted, defaults to empty. |
| `additional_tools` | `list[CustomToolDef] \| None` | `None` | Custom tools as `(PydanticModel, callable)` tuples. |
| `history_converter` | `CodexHistoryConverter \| None` | auto | Advanced escape hatch for replacing the default history/thread-metadata converter. |
| `client_factory` | callable | `None` | Test/advanced injection point for a custom Codex client. |

## Feature flags: Capabilities and Emit

Expand Down
7 changes: 2 additions & 5 deletions examples/acp/clients/bridge_architecture.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@

from band import Agent, configure_logging
from band.adapters import ACPClientAdapter
from band.integrations.acp.client_profiles import CursorACPClientProfile
from band.integrations.acp.client_profiles import resolve_acp_client_profile

configure_logging(
level=logging.INFO,
Expand Down Expand Up @@ -83,15 +83,12 @@ async def main() -> None:
settings = Settings()

command = shlex.split(settings.acp_agent_command)
cwd = settings.acp_agent_cwd
auth_method = settings.acp_auth_method or None
inject_band_tools = settings.acp_inject_band_tools
profile_name = settings.acp_client_profile.strip().lower()
profile = CursorACPClientProfile() if profile_name == "cursor" else None
profile = resolve_acp_client_profile(settings.acp_client_profile)

adapter = ACPClientAdapter(
command=command,
cwd=cwd,
inject_band_tools=inject_band_tools,
auth_method=auth_method,
profile=profile,
Expand Down
13 changes: 0 additions & 13 deletions examples/acp/clients/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@
import os

from dotenv import load_dotenv
from pydantic_settings import BaseSettings, SettingsConfigDict

from band import Agent, configure_logging
from band.adapters import ACPClientAdapter
from band.integrations.acp.client_profiles import CursorACPClientProfile
Expand All @@ -70,18 +68,8 @@
logger = logging.getLogger(__name__)


class Settings(BaseSettings):
model_config = SettingsConfigDict(
extra="ignore", case_sensitive=False, env_ignore_empty=True
)

acp_agent_cwd: str = "."


async def main() -> None:
load_dotenv()
settings = Settings()
cwd = settings.acp_agent_cwd

# Cursor authentication environment — passed to the subprocess, so left as
# a direct os.getenv pair rather than a Settings field.
Expand All @@ -98,7 +86,6 @@ async def main() -> None:
# - Band tools are injected through a local localhost-only MCP server
adapter = ACPClientAdapter(
command=[os.path.expanduser("~/.local/bin/agent"), "acp"],
cwd=cwd,
env=cursor_env or None,
inject_band_tools=True,
auth_method="cursor_login",
Expand Down
4 changes: 0 additions & 4 deletions examples/acp/clients/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,9 @@ async def main() -> None:
# Command to spawn the remote ACP agent
acp_command = shlex.split(settings.acp_agent_command)

# Working directory for ACP sessions
acp_cwd = settings.acp_agent_cwd

# Create adapter pointing to remote ACP agent
adapter = ACPClientAdapter(
command=acp_command,
cwd=acp_cwd,
)

logger.info(
Expand Down
4 changes: 0 additions & 4 deletions examples/acp/clients/rich_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,9 @@ async def main() -> None:
# Command to spawn the remote ACP agent
acp_command = shlex.split(settings.acp_agent_command)

# Working directory for ACP sessions
acp_cwd = settings.acp_agent_cwd

# Create adapter pointing to remote ACP agent
adapter = ACPClientAdapter(
command=acp_command,
cwd=acp_cwd,
)

logger.info("Starting ACP client bridge with rich streaming...")
Expand Down
9 changes: 3 additions & 6 deletions examples/codex/01_basic_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,14 @@
Prerequisites:
1. OAuth login:
codex login
2. For stdio mode (default), no extra process is needed.
3. For ws mode, start app-server separately:
codex app-server --listen ws://127.0.0.1:8765
2. The adapter starts one local stdio process for each Band room.

Run:
uv run examples/codex/01_basic_agent.py

Optional env overrides:
AGENT_KEY=darter
CODEX_TRANSPORT=stdio|ws
CODEX_WS_URL=ws://127.0.0.1:8765
CODEX_WORKSPACE_ROOT=.band-workspaces
CODEX_ROLE=coding|planner|reviewer
CODEX_MODEL=gpt-5.5
CODEX_APPROVAL_MODE=manual|auto_accept|auto_decline
Expand Down Expand Up @@ -84,7 +81,7 @@ async def main() -> None:
"Role '%s' specified but no prompt file at %s", codex_role, prompt_file
)

# transport/codex_ws_url/model/cwd/approval_policy/approval_mode/
# model/approval_policy/approval_mode/
# emit_turn_task_markers all self-source from CODEX_* env vars (see module
# docstring) when omitted here.
adapter = CodexAdapter(
Expand Down
2 changes: 1 addition & 1 deletion examples/codex/02_tom_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
async def main() -> None:
load_dotenv()

# cwd/model self-source from CODEX_CWD/CODEX_MODEL when omitted here.
# model self-sources from CODEX_MODEL when omitted here.
adapter = CodexAdapter(
config=CodexAdapterConfig(
transport="stdio",
Expand Down
2 changes: 1 addition & 1 deletion examples/codex/03_jerry_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
async def main() -> None:
load_dotenv()

# cwd/model self-source from CODEX_CWD/CODEX_MODEL when omitted here.
# model self-sources from CODEX_MODEL when omitted here.
adapter = CodexAdapter(
config=CodexAdapterConfig(
transport="stdio",
Expand Down
4 changes: 3 additions & 1 deletion examples/docker_demo/agents/dev/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ async def main() -> None:
config = DevConfig()
adapter = CodexAdapter(
config=CodexAdapterConfig(
model=config.model, approval_policy="never", custom_section=build_persona()
model=config.model,
approval_policy="never",
custom_section=build_persona(),
),
# Emit tool_call/tool_result and reasoning to the room, keeping the default
# per-turn task markers but excluding usage events. Codex's Band tools
Expand Down
21 changes: 6 additions & 15 deletions examples/run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@
uv run python examples/run_agent.py --example crewai
uv run python examples/run_agent.py --example crewai --streaming # Show tool calls
uv run python examples/run_agent.py --example codex
uv run python examples/run_agent.py --example codex --agent darter --codex-transport stdio
uv run python examples/run_agent.py --example codex --agent darter --codex-transport ws --codex-ws-url ws://127.0.0.1:8765
uv run python examples/run_agent.py --example codex --agent darter
uv run python examples/run_agent.py --example a2a --a2a-url http://localhost:10000 # A2A bridge
uv run python examples/run_agent.py --example a2a_gateway # A2A Gateway (exposes peers)
uv run python examples/run_agent.py --example a2a_gateway --gateway-port 8080 # Custom port
Expand Down Expand Up @@ -446,7 +445,6 @@ async def run_codex_agent(
api_key: str,
custom_section: str,
codex_transport: str,
codex_ws_url: str,
codex_model: str | None,
codex_personality: str,
codex_approval_policy: str,
Expand All @@ -458,20 +456,20 @@ async def run_codex_agent(
logger: logging.Logger,
) -> None:
"""Run the Codex app-server adapter."""
from band import create_room_workspace_resolver # noqa: PLC0415 -- only load the adapters extra when this example is the one selected to run
from band.adapters import CodexAdapter # noqa: PLC0415 -- only load the adapters extra when this example is the one selected to run
from band.adapters.codex import CodexAdapterConfig # noqa: PLC0415 -- only load the codex extra when this example is the one selected to run

adapter = CodexAdapter(
config=CodexAdapterConfig(
transport=codex_transport, # type: ignore[arg-type] # str from CLI args, validated at runtime
cwd=codex_cwd,
workspace_for_room=create_room_workspace_resolver(codex_cwd),
model=codex_model,
personality=codex_personality, # type: ignore[arg-type] # str from CLI args, validated at runtime
approval_policy=codex_approval_policy,
approval_mode=codex_approval_mode, # type: ignore[arg-type] # str from CLI args, validated at runtime
sandbox=codex_sandbox,
reasoning_effort=codex_reasoning_effort, # type: ignore[arg-type] # str from CLI args, validated at runtime
codex_ws_url=codex_ws_url,
custom_section=custom_section,
include_base_instructions=True,
emit_turn_task_markers=codex_turn_task_markers,
Expand All @@ -482,7 +480,7 @@ async def run_codex_agent(
)

logger.info(
"Starting Codex agent (transport=%s, model=%s, cwd=%s)",
"Starting Codex agent (transport=%s, model=%s, workspace_root=%s)",
codex_transport,
codex_model or "auto",
codex_cwd,
Expand Down Expand Up @@ -775,7 +773,6 @@ async def main() -> None:
uv run python examples/run_agent.py --example codex # Codex app-server adapter
uv run python examples/run_agent.py --example codex --agent darter # Run Codex as darter agent
uv run python examples/run_agent.py --example codex --codex-transport stdio
uv run python examples/run_agent.py --example codex --codex-transport ws --codex-ws-url ws://127.0.0.1:8765
uv run python examples/run_agent.py --example a2a # A2A bridge (default: localhost:10000)
uv run python examples/run_agent.py --example a2a --debug # A2A with debug logging (context_id tracing)
uv run python examples/run_agent.py --example a2a --a2a-url http://remote:8080 # A2A with custom URL
Expand Down Expand Up @@ -859,15 +856,10 @@ async def main() -> None:
)
parser.add_argument(
"--codex-transport",
choices=["stdio", "ws"],
choices=["stdio"],
default="stdio",
help="Codex transport mode (default: stdio)",
)
parser.add_argument(
"--codex-ws-url",
default=os.getenv("CODEX_WS_URL", "ws://127.0.0.1:8765"),
help="Codex WebSocket URL when --codex-transport=ws",
)
parser.add_argument(
"--codex-role",
default=None,
Expand All @@ -887,7 +879,7 @@ async def main() -> None:
parser.add_argument(
"--codex-cwd",
default=os.getcwd(),
help="Working directory given to Codex app-server (default: current directory)",
help="Root directory for per-room Codex workspaces (default: current directory)",
)
parser.add_argument(
"--codex-reasoning-effort",
Expand Down Expand Up @@ -1105,7 +1097,6 @@ async def main() -> None:
api_key=api_key,
custom_section=codex_custom,
codex_transport=args.codex_transport,
codex_ws_url=args.codex_ws_url,
codex_model=args.codex_model,
codex_personality=args.codex_personality,
codex_approval_policy=args.codex_approval_policy,
Expand Down
Loading
Loading