Skip to content
Draft
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
24 changes: 1 addition & 23 deletions docker/codex/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@
AGENT_CONFIG Path to the YAML config file (required)
AGENT_KEY Key to look up in keyed config (default: "agent")
CODEX_CWD Working directory for Codex (default: /workspace/repo)
CODEX_TRANSPORT Transport mode: stdio or ws (default: stdio)
CODEX_MODEL Model ID override
CODEX_ROLE Role name; loads prompt from {config_dir}/prompts/{role}.md
CODEX_SANDBOX Sandbox mode (default: external-sandbox)
CODEX_REASONING_EFFORT Reasoning effort level
CODEX_APPROVAL_MODE Approval mode: manual, auto_accept, auto_decline
CODEX_TURN_TASK_MARKERS Emit turn task markers: true/false (default: false)
BAND_WS_URL Platform WebSocket URL
BAND_REST_URL Platform REST URL
OPENAI_API_KEY OpenAI API key
Expand Down Expand Up @@ -53,11 +51,9 @@
)
logger = logging.getLogger(__name__)

CodexTransport = Literal["stdio", "ws"]
CodexApprovalMode = Literal["manual", "auto_accept", "auto_decline"]
CodexReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh"]

_TRANSPORTS: dict[str, CodexTransport] = {"stdio": "stdio", "ws": "ws"}
_APPROVAL_MODES: dict[str, CodexApprovalMode] = {
"manual": "manual",
"auto_accept": "auto_accept",
Expand Down Expand Up @@ -126,15 +122,6 @@ def _optional_str(value: Any) -> str | None:
return text or None


def _parse_transport(value: str) -> CodexTransport:
parsed = _TRANSPORTS.get(value.lower())
if parsed is None:
raise ValueError(
f"CODEX_TRANSPORT must be one of {', '.join(sorted(_TRANSPORTS))}; got: {value}"
)
return parsed


def _parse_approval_mode(value: str) -> CodexApprovalMode:
parsed = _APPROVAL_MODES.get(value.lower())
if parsed is None:
Expand Down Expand Up @@ -216,9 +203,6 @@ async def main() -> None:
or repo_init.repo_path
or "/workspace/repo"
)
codex_transport = _parse_transport(
_optional_str(os.environ.get("CODEX_TRANSPORT")) or "stdio"
)
codex_model = _optional_str(os.environ.get("CODEX_MODEL")) or _optional_str(
config.get("model")
)
Expand Down Expand Up @@ -254,25 +238,20 @@ async def main() -> None:
or _optional_str(config.get("approval_mode"))
or "manual"
)
codex_turn_markers = _env_bool("CODEX_TURN_TASK_MARKERS", default=False)

adapter = CodexAdapter(
config=CodexAdapterConfig(
transport=codex_transport,
cwd=codex_cwd,
model=codex_model,
personality="pragmatic",
approval_policy="never",
approval_mode=codex_approval,
sandbox=codex_sandbox,
reasoning_effort=codex_reasoning,
codex_ws_url=os.environ.get("CODEX_WS_URL", "ws://127.0.0.1:8765"),
custom_section="\n\n".join(
part for part in (custom_section, repo_init.context_bundle) if part
),
include_base_instructions=True,
enable_task_events=True,
emit_turn_task_markers=codex_turn_markers,
enable_execution_reporting=False,
emit_thought_events=False,
fallback_send_agent_text=True,
Expand All @@ -290,8 +269,7 @@ async def main() -> None:

logger.info("Starting Codex agent: %s", agent_id)
logger.info(
"Codex config: transport=%s, model=%s, cwd=%s, sandbox=%s, role=%s",
codex_transport,
"Codex config: model=%s, cwd=%s, sandbox=%s, role=%s",
codex_model or "auto",
codex_cwd,
codex_sandbox,
Expand Down
50 changes: 15 additions & 35 deletions docs/adapters/codex.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# 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 the OpenAI Codex Python SDK runtime to Band rooms so it can take part in conversations as a coding collaborator.

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.
Use this adapter when you want an OpenAI-powered coding agent with configurable sandboxing, approval commands, command/file-change telemetry, and reasoning visibility. 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.

## Install

Expand All @@ -12,25 +12,10 @@ uv add "band-sdk[codex]"

## Prerequisites

The adapter starts or connects to a Codex process. Install and authenticate the Codex CLI before running your Band agent:

```bash
npm install -g @openai/codex
codex login
```

Requires Node.js 18+.

You need two credentials or auth contexts:
The adapter uses the `openai-codex` Python package, which bundles the Codex runtime through `openai-codex-cli-bin`. 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
```
- Codex authentication for the Codex runtime. Reuse an existing Codex login, run `codex login` if the CLI is available, or set `OPENAI_API_KEY` if that is how your Codex environment is configured.

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

Expand Down Expand Up @@ -65,7 +50,7 @@ asyncio.run(agent.run())

Codex has three setup layers:

- `CodexAdapterConfig(...)` configures the Codex runtime: transport, model, working directory, sandbox, approval behavior, prompts, context injection, and streaming/telemetry detail.
- `CodexAdapterConfig(...)` configures the Codex runtime: 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.
- `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.

Expand All @@ -88,9 +73,9 @@ Common `Agent.create(...)` parameters:

## How It Works

Each Band room maps to one Codex thread. Through Band collaboration tools, Codex can send messages, look up peers, add participants, and create new chats. On startup, the adapter tries to resume the previous Codex thread from task-event metadata. If resume fails and `inject_history_on_resume_failure=True`, the adapter injects recent room history as text context.
Each Band room maps to one Codex thread. Through Band collaboration tools, Codex can send messages, look up peers, add participants, and create new chats. On startup, the adapter tries to resume the previous Codex thread from thread-mapping metadata. If resume fails and `inject_history_on_resume_failure=True`, the adapter injects recent room history as text context.

The adapter handles Codex approval requests in the room, persists thread metadata through task events, and sends optional telemetry such as tool calls, reasoning, diffs, token usage, and lifecycle events.
The adapter handles Codex approval requests in the room, persists thread metadata through non-task telemetry, and sends optional telemetry such as tool calls, reasoning, diffs, and token usage.

## Configuration Reference

Expand All @@ -115,13 +100,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`. |
| `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 @@ -162,15 +145,14 @@ Pass these to `CodexAdapterConfig(...)`:
| `max_history_messages` | `int` | `50` | Maximum messages to inject after a resume failure. |
| `fallback_send_agent_text` | `bool` | `True` | Send Codex final text as a room message if Codex did not call the send-message tool. |

### Transport and Advanced Settings
### Runtime and Advanced Settings

Pass these to `CodexAdapterConfig(...)`:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `codex_command` | `tuple[str, ...] \| None` | `None` | Custom command used to launch Codex for stdio transport. |
| `codex_command` | `tuple[str, ...] \| None` | `None` | Custom command used to launch Codex instead of the bundled `openai-codex-cli-bin` runtime. |
| `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 @@ -195,15 +177,15 @@ Pass these directly to `CodexAdapter(...)`:
- `capabilities` exposes optional Band tool categories to the model.
- `emit` controls telemetry events the adapter sends back to Band.

If `features` is omitted, Codex defaults to `Emit.TASK_EVENTS` because task events are used for lifecycle data and thread resume metadata. Optional capabilities are still off by default. If you pass `features=...`, your value is authoritative; include `Emit.TASK_EVENTS` if you want thread resume across reconnects.
If `features` is omitted, Codex defaults to `Emit.TASK_EVENTS` so the adapter can persist thread mapping metadata for reconnects. Optional capabilities are still off by default. If you pass `features=...`, your value is authoritative; include `Emit.TASK_EVENTS` if you want thread resume across reconnects.

| Feature | Supported | What it does |
|---------|-----------|--------------|
| `Capability.CONTACTS` | Yes | Exposes contact-management tools to Codex. Incoming contact request handling is configured separately with `ContactEventConfig` on `Agent.create(...)`. |
| `Capability.MEMORY` | Yes | Exposes memory tools, if memory is enabled for your Band workspace. |
| `Emit.EXECUTION` | Yes | Sends events for command execution, file changes, MCP tools, web search, image viewing, and collaboration-agent tool calls. |
| `Emit.THOUGHTS` | Yes | Sends completed reasoning, plan, and review-mode events as `thought` events. |
| `Emit.TASK_EVENTS` | Yes | Sends lifecycle, thread-resume, approval, diff-summary, token-usage, and error task events. Required for persisted Codex thread mapping. |
| `Emit.TASK_EVENTS` | Yes | Persists Codex thread-mapping metadata for reconnects. Model-authored task updates still come from Band tools. |

Example:

Expand All @@ -228,36 +210,34 @@ Use `AdapterFeatures.emit` for the broad telemetry categories:
|------|----------|--------------|
| `Emit.EXECUTION` | Auditing commands, tools, and file activity. | `tool_call` and `tool_result` pairs. |
| `Emit.THOUGHTS` | Debugging reasoning and planning UX. | Completed `thought` events. |
| `Emit.TASK_EVENTS` | Lifecycle, resume, approvals, and usage tracking. | Task events with metadata. |
| `Emit.TASK_EVENTS` | Reconnect continuity. | Non-task thread-mapping metadata. |

Codex also has streaming flags in `CodexAdapterConfig(...)`. These send incremental updates as Codex produces them and are independent of `Emit.THOUGHTS`, which gates completed items.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `stream_reasoning_events` | `bool` | `False` | Stream reasoning chunks as thought events. |
| `stream_plan_events` | `bool` | `False` | Stream plan chunks as thought events and plan updates as task-style events. |
| `stream_plan_events` | `bool` | `False` | Stream plan chunks as thought events and plan updates as execution telemetry. |
| `stream_commentary_events` | `bool` | `False` | Stream commentary chunks as thought events. |

These `CodexAdapterConfig(...)` flags add more telemetry detail:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `enable_task_events` | `bool` | `True` | When `features` is omitted, include `Emit.TASK_EVENTS` by default. Ignored when you pass explicit `features=` to `CodexAdapter(...)`. |
| `emit_turn_task_markers` | `bool` | `False` | Emit simple "Codex turn" task markers on turn completion. |
| `emit_turn_lifecycle_events` | `bool` | `False` | Emit enriched turn lifecycle events at turn start and completion. |
| `emit_diff_events` | `bool` | `False` | Include file diffs in event metadata, capped at 64 KB. |
| `emit_token_usage_events` | `bool` | `False` | Track and emit token usage per session. |
| `structured_errors` | `bool` | `True` | Emit structured error events instead of plain text errors. |

Enabling both `emit_turn_task_markers` and `emit_turn_lifecycle_events` produces two task events per completed turn. Pick one; lifecycle events contain richer metadata.
Codex runtime lifecycle events are not mirrored as Band `task` events. Band tasks are reserved for explicit todo/task-list style updates sent through Band tools.

## Chat Commands

Type `/help` in the room to see the command list. Common commands:

| Command | Description |
|---------|-------------|
| `/status` | Show transport, model, room/thread mapping, sandbox, approval state, reasoning settings, and token usage. |
| `/status` | Show model, room/thread mapping, sandbox, approval state, reasoning settings, and token usage. |
| `/model` or `/models` | Show the current model. |
| `/model list` or `/models list` | List available Codex models. |
| `/model <id>` | Use a model for subsequent turns. |
Expand Down
3 changes: 1 addition & 2 deletions examples/codex/.env.plan-review.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
CODEX_PLANNER_AGENT_KEY=codex_planner
CODEX_REVIEWER_AGENT_KEY=codex_reviewer

# Provider transport and sandbox
CODEX_TRANSPORT=stdio
# Sandbox
CODEX_PLANNER_SANDBOX=external-sandbox
CODEX_REVIEWER_SANDBOX=external-sandbox
GIT_SSH_STRICT_HOST_KEY_CHECKING=true
Expand Down
52 changes: 4 additions & 48 deletions examples/codex/01_basic_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,27 @@
"""
Basic Codex adapter agent example.

Runs a Band agent backed by Codex app-server.
Runs a Band agent backed by the OpenAI Codex Python SDK runtime.

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
1. Band agent credentials in `agent_config.yaml`.
2. Codex authentication through `codex login`, `OPENAI_API_KEY`, or the Codex process environment.

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_ROLE=coding|planner|reviewer
CODEX_MODEL=gpt-5.5
CODEX_APPROVAL_MODE=manual|auto_accept|auto_decline
CODEX_TURN_TASK_MARKERS=true|false
"""

from __future__ import annotations

import asyncio
import logging
import os
import shutil
import subprocess
import sys
from pathlib import Path

from dotenv import load_dotenv
Expand Down Expand Up @@ -68,38 +59,7 @@ async def main() -> None:
if not rest_url:
raise ValueError("BAND_REST_URL environment variable is required")

codex_bin = shutil.which("codex")
if codex_bin is None:
logger.error(
"Codex CLI not found on PATH. Install it: npm install -g @openai/codex"
)
sys.exit(1)

login_check = subprocess.run(
[codex_bin, "login", "status"],
capture_output=True,
text=True,
)
if login_check.returncode != 0:
print("Codex is not logged in.")
try:
answer = input("Run 'codex login' now? [Y/n] ").strip().lower()
except EOFError:
print("Non-interactive shell. Run 'codex login' manually, then retry.")
sys.exit(1)
if answer in ("", "y", "yes"):
result = subprocess.run([codex_bin, "login"], check=False)
if result.returncode != 0:
print("Login failed. Check the output above and retry.")
sys.exit(1)
else:
print("Exiting. Run 'codex login' manually, then retry.")
sys.exit(1)

agent_key = os.getenv("AGENT_KEY", "darter")
codex_transport = os.getenv("CODEX_TRANSPORT", "stdio")
if codex_transport not in {"stdio", "ws"}:
raise ValueError("CODEX_TRANSPORT must be 'stdio' or 'ws'")

# Load role prompt from file if CODEX_ROLE is set
codex_role = os.getenv("CODEX_ROLE")
Expand All @@ -116,8 +76,6 @@ async def main() -> None:

adapter = CodexAdapter(
config=CodexAdapterConfig(
transport=codex_transport, # type: ignore[arg-type] # str from env, validated at runtime
codex_ws_url=os.getenv("CODEX_WS_URL", "ws://127.0.0.1:8765"),
model=os.getenv("CODEX_MODEL") or None,
cwd=os.getenv("CODEX_CWD", os.getcwd()),
approval_policy=os.getenv("CODEX_APPROVAL_POLICY", "never"),
Expand All @@ -126,7 +84,6 @@ async def main() -> None:
custom_section=custom_section,
include_base_instructions=True,
enable_task_events=True,
emit_turn_task_markers=_env_bool("CODEX_TURN_TASK_MARKERS", False),
fallback_send_agent_text=True,
)
)
Expand All @@ -139,9 +96,8 @@ async def main() -> None:
)

logger.info(
"Starting Codex agent: agent_key=%s transport=%s role=%s",
"Starting Codex agent: agent_key=%s role=%s",
agent_key,
codex_transport,
codex_role or "none",
)
await agent.run()
Expand Down
Loading
Loading