Skip to content

Commit 4ee5230

Browse files
ross-rlclaudeReflexcodex
authored
docs(sdk): codex examples, compatibility matrix, and module docs (#137)
Third and final PR of the codex-native stack (PLAN Step 3), stacked on #135 (`feat/codex-connection`), which is stacked on #134. See `sdk/src/codex/PLAN.md`. ## Examples - **`feature-examples/src/agents.ts`**: adds a native `codex` agent (`broker_mount: { protocol: "codex", agent_binary: "codex" }`, `OPENAI_API_KEY` secret). `codex-acp` is kept — downstream consumers still use it. - **New use cases** demonstrating codex-native behavior that differs from Claude: - `approval-codex` — server-initiated command approval round-trip via `onApprovalRequest` (thread started with `sandbox: "read-only"` + `approvalPolicy: "on-request"` to force an approval; both the current and legacy approval methods are handled). - `thread-resume-codex` — client-driven `resumeThread()`: establish context on thread A, switch to a fresh thread B, resume A, and verify the codeword is still in context. - **Framework plumbing**: `types.ts`/`scaffold.ts`/`main.ts` extended with the `codex` protocol (connection setup with no initialize step, `--protocol codex` filter, Codex column in the generated matrix). - **Blueprint**: `examples/blueprint/Dockerfile` now installs `@openai/codex@0.144.1` (matches the SDK's vendored-protocol pin). ## Compatibility matrix `compatibility.md` (and the `llms.txt` use-case index) were **updated by hand, not machine-regenerated** — running `feature-compat` requires live `RUNLOOP_API_KEY`/`OPENAI_API_KEY` credentials plus broker-side codex support, none of which are available in this environment. The hand-edit matches the generator's output format exactly: codex cells show `pending` (the generator's no-results status), and the template/generator were updated so the next full run produces the Codex column automatically. `bun run feature-compat --validate` passes against the hand-edited files. ## Docs - **`sdk/README.md`**: Codex row in the module table, broker `protocol` option table (`acp` / `claude_json` / `codex`), Codex quickstart mirroring the Claude one, a full "Codex Module" section (connection options, methods, approval requests incl. the `approval_policy=never` launch-args alternative, timeline guards, transport), Codex timeline events, architecture comparison, and updated known limitations. - **`sdk/AGENTS.md`**: module table, codex quick start + key-methods table, `codex_protocol` timeline kind, constraints (auto-approve default, no initialize step, no extra dependency). ## Validation - `bun run check` / `bun run typecheck` / `bun run build` / `bun run test` (529 tests) — pass - `feature-examples`: `bun run typecheck` and `bun run feature-compat --validate` — pass - The new use cases could **not** be executed end-to-end here (no live credentials); they typecheck against the vendored protocol types. ## Notes for reviewers - The `codex` agent entry assumes a `codex` agent exists in the Runloop agent catalog (mirroring `codex-acp`); adjust `agentName` if the catalog entry differs. - `scaffold.ts` casts the broker-mount `protocol` because the published `@runloop/api-client` (≤1.25.0) doesn't include `"codex"` in its mount types yet — drop the cast once the client catches up. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Combined app (added in follow-up) `examples/combined-app` now supports Codex as a third agent type end-to-end: - **Server**: `codex-manager.ts` (provisions a `protocol: "codex"` broker mount pointing at `/home/user/.local/bin/codex`, injects `OPENAI_API_KEY`, starts a thread, maps `workingDir`/`model`/`systemPrompt` onto `threadStartParams`), `routes/codex.ts` (`POST /api/approval-response`), plus codex branches in lifecycle/prompt/cancel routes and the agent registry. With "Auto-approve approvals" off, the thread starts with `approvalPolicy: "on-request"` and command/file approvals are forwarded to the browser; with it on, the SDK's default auto-approve handlers answer. - **Client**: `useCodexAgent` hook renders app-server frames as turn blocks (agent-message deltas → text, reasoning deltas → thinking, commandExecution/fileChange/mcpToolCall/webSearch items → tool calls with streamed output), an `ApprovalPrompt` dialog for interactive approvals, a Codex button + fields in the setup card, timeline sidebar CODEX badges, and thread-id display. - Attachments: images are sent as data-URL `UserInput` image items; file attachments are flattened into text. - Validation: `tsc --noEmit` and `vite build` pass; the Express server boots with the new routes. Not exercised against a live devbox (no credentials here). --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Reflex <reflex@runloop.ai> Co-authored-by: OpenAI Codex <noreply@openai.com> Co-authored-by: OpenAI Codex <codex@openai.com>
1 parent 201da90 commit 4ee5230

40 files changed

Lines changed: 1867 additions & 495 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ examples/
295295
acp-cli/ → Interactive ACP REPL
296296
claude-hello-world/ → Minimal Claude single-prompt script
297297
claude-cli/ → Interactive Claude REPL
298-
combined-app/ → Full-stack combined demo (Claude + ACP, Express + React)
298+
combined-app/ → Full-stack combined demo (Claude + Codex + ACP, Express + React)
299299
feature-examples/ → Runnable SDK recipes (single-prompt, elicitation, etc.)
300300
```
301301

examples/blueprint/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ WORKDIR /home/user
77

88
RUN curl -fsSL https://claude.ai/install.sh | bash
99
RUN curl -fsSL https://opencode.ai/install | bash
10+
RUN curl -fsSL https://chatgpt.com/codex/install.sh | CODEX_NON_INTERACTIVE=true sh -s -- --release 0.144.1

examples/blueprint/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ See [`Dockerfile`](Dockerfile) for the exact contents. At the time of writing it
4343
- [Claude Code](https://docs.claude.com/en/docs/claude-code) — for the Claude module examples
4444
- [OpenCode](https://opencode.ai) — for ACP examples using OpenCode
4545
- [Codex ACP](https://www.npmjs.com/package/@zed-industries/codex-acp) — for ACP examples using Codex
46+
- [Codex CLI](https://developers.openai.com/codex/cli) — for native Codex module examples (pinned to 0.144.1, matching the SDK's vendored protocol types)
4647

4748
## Alternatives
4849

examples/combined-app/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
RUNLOOP_API_KEY=your_runloop_api_key
22
ANTHROPIC_API_KEY=your_anthropic_api_key
3+
OPENAI_API_KEY=your_openai_api_key

examples/combined-app/README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
> **Alpha — subject to change.** This example uses an SDK in early development. APIs and behavior may change without notice between versions.
44
5-
A full-stack demo that supports both ACP and Claude Code agents running in Runloop devboxes. An Express backend manages agent connections (one per protocol) and fans out SDK timeline events to a React frontend over a single WebSocket. Multiple agents can run concurrently.
5+
A full-stack demo that supports ACP, Claude Code, and Codex agents running in Runloop devboxes. An Express backend manages agent connections (one per protocol) and fans out SDK timeline events to a React frontend over a single WebSocket. Multiple agents can run concurrently.
66

77
## Prerequisites
88

99
- Node.js 22+
1010
- A [Runloop](https://runloop.ai) API key
1111
- An [Anthropic](https://anthropic.com) API key (required for Claude agents)
12+
- An [OpenAI](https://platform.openai.com) API key (required for Codex agents)
1213
- The `@runloop/remote-agents-sdk` SDK built locally (`cd ../../sdk && bun run build`)
1314

1415
## Setup
@@ -27,11 +28,12 @@ Add your keys to `.env`:
2728
```
2829
RUNLOOP_API_KEY=your_runloop_api_key
2930
ANTHROPIC_API_KEY=your_anthropic_api_key
31+
OPENAI_API_KEY=your_openai_api_key
3032
```
3133

3234
### Build the shared blueprint (one-time, required)
3335

34-
This example provisions devboxes with `blueprint_name: "axon-agents"` (see [`src/server/acp-manager.ts`](src/server/acp-manager.ts) and [`src/server/claude-manager.ts`](src/server/claude-manager.ts)). That blueprint must exist on your Runloop account before starting an agent from the UI — otherwise `POST /api/start` will fail when creating the devbox.
36+
This example provisions devboxes with `blueprint_name: "axon-agents"` (see [`src/server/acp-manager.ts`](src/server/acp-manager.ts), [`src/server/claude-manager.ts`](src/server/claude-manager.ts), and [`src/server/codex-manager.ts`](src/server/codex-manager.ts)). That blueprint must exist on your Runloop account before starting an agent from the UI — otherwise `POST /api/start` will fail when creating the devbox. Codex agents require a blueprint built after the Codex CLI was added to the [`Dockerfile`](../blueprint/Dockerfile) — re-run the command below if your `axon-agents` image predates it.
3537

3638
From the monorepo root:
3739

@@ -55,8 +57,8 @@ Open http://localhost:5176. The Vite dev server proxies `/api/*` and `/ws` to th
5557

5658
## How It Works
5759

58-
1. **Start an agent** — the setup card lets you choose ACP or Claude, configure the agent binary / blueprint, and optionally set a system prompt. `POST /api/start` provisions an Axon channel and devbox, then opens the appropriate SDK connection (`ACPAxonConnection` or `ClaudeAxonConnection`).
59-
2. **Send a prompt**`POST /api/prompt` dispatches to the active connection's `prompt()` (ACP) or `send()` (Claude) and returns immediately.
60+
1. **Start an agent** — the setup card lets you choose ACP, Claude, or Codex, configure the agent binary / blueprint, and optionally set a system prompt. `POST /api/start` provisions an Axon channel and devbox, then opens the appropriate SDK connection (`ACPAxonConnection`, `ClaudeAxonConnection`, or `CodexAxonConnection`).
61+
2. **Send a prompt**`POST /api/prompt` dispatches to the active connection's `prompt()` (ACP) or `send()` (Claude/Codex) and returns immediately.
6062
3. **Stream events** — the SDK's `onTimelineEvent` callback fires for every classified event (protocol messages, system turns, unknowns). The server broadcasts each event over WebSocket with an `agentId` tag.
6163
4. **Render blocks** — the React client filters events by `agentId`, builds incremental turn blocks (`useBlockManager`), and renders them through `AssistantTurn` / `TurnBlocks`.
6264

@@ -72,6 +74,7 @@ src/
7274
│ ├── acp-manager.ts ACP connection lifecycle
7375
│ ├── acp-client.ts ACP Client implementation (permissions, elicitation)
7476
│ ├── claude-manager.ts Claude connection lifecycle
77+
│ ├── codex-manager.ts Codex connection lifecycle (threads, approvals)
7578
│ └── agent-registry.ts Multi-agent bookkeeping
7679
└── client/
7780
├── main.tsx React entry point
@@ -81,6 +84,7 @@ src/
8184
│ ├── useAgent.ts Unified hook (delegates to protocol-specific hooks)
8285
│ ├── useACPAgent.ts ACP event handling and state
8386
│ ├── useClaudeAgent.ts Claude event handling and state
87+
│ ├── useCodexAgent.ts Codex event handling and state (items, approvals)
8488
│ ├── useBlockManager.ts Turn block accumulation
8589
│ ├── useAgentList.ts Agent list polling
8690
│ ├── useAttachments.ts File/image attachment handling

examples/combined-app/src/client/App.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1874,6 +1874,7 @@ html, body, #root {
18741874
.tl-kind-system { background: rgba(210, 153, 34, 0.15); color: var(--warning); }
18751875
.tl-kind-acp { background: rgba(63, 185, 80, 0.15); color: var(--success); }
18761876
.tl-kind-claude { background: rgba(136, 87, 255, 0.15); color: #a78bfa; }
1877+
.tl-kind-codex { background: rgba(240, 246, 252, 0.12); color: #e6edf3; }
18771878
.tl-kind-unknown { background: rgba(139, 148, 158, 0.15); color: var(--text-secondary); }
18781879
.tl-kind-custom { background: rgba(56, 189, 248, 0.15); color: #38bdf8; }
18791880

examples/combined-app/src/client/App.tsx

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { AgentSidebar } from "./components/AgentSidebar.js";
1515
import { ControlsBar } from "./components/ControlsBar.js";
1616
import { AssistantTurn } from "./components/AssistantTurn.js";
1717
import { AttachmentBar } from "./components/AttachmentBar.js";
18+
import { ApprovalPrompt } from "./components/ApprovalPrompt.js";
1819
import { ElicitationForm } from "./components/ElicitationForm.js";
1920
import { PermissionDialog } from "./components/PermissionDialog.js";
2021
import { ControlRequestPrompt } from "./components/ControlRequestPrompt.js";
@@ -234,12 +235,19 @@ export default function App() {
234235
autoApprovePermissions: startAutoApprove,
235236
...sharedConfig,
236237
}
237-
: {
238-
blueprintName: blueprintName || undefined,
239-
model: model || undefined,
240-
dangerouslySkipPermissions: startAutoApprove,
241-
...sharedConfig,
242-
};
238+
: selectedAgentType === "codex"
239+
? {
240+
blueprintName: blueprintName || undefined,
241+
model: model || undefined,
242+
autoApprovePermissions: startAutoApprove,
243+
...sharedConfig,
244+
}
245+
: {
246+
blueprintName: blueprintName || undefined,
247+
model: model || undefined,
248+
dangerouslySkipPermissions: startAutoApprove,
249+
...sharedConfig,
250+
};
243251

244252
try {
245253
const resp = await api<{ agentId: string; agentType: AgentType; [key: string]: unknown }>(
@@ -255,7 +263,9 @@ export default function App() {
255263
agentType: resp.agentType,
256264
name: selectedAgentType === "claude"
257265
? (blueprintName || "Claude Agent")
258-
: (agentBinary || "ACP Agent"),
266+
: selectedAgentType === "codex"
267+
? (blueprintName || "Codex Agent")
268+
: (agentBinary || "ACP Agent"),
259269
axonId: resp.axonId as string,
260270
devboxId: resp.devboxId as string,
261271
createdAt: Date.now(),
@@ -415,7 +425,11 @@ export default function App() {
415425

416426
const showChatView = selectedAgentId && !showSetup && selectedEntry;
417427

418-
const agentLabel = agent.agentType === "claude" ? "Claude Code" : "ACP Agent";
428+
const agentLabel = agent.agentType === "claude"
429+
? "Claude Code"
430+
: agent.agentType === "codex"
431+
? "Codex"
432+
: "ACP Agent";
419433

420434
// Derive devbox status from the last devbox_lifecycle system event in messages
421435
const lastDevboxEvent = [...agent.messages].reverse().find(
@@ -495,6 +509,12 @@ export default function App() {
495509
</div>
496510
)}
497511

512+
{agent.agentType === "codex" && agent.threadId && (
513+
<div className="controls-bar">
514+
<span className="config-label">Thread: {agent.threadId}</span>
515+
</div>
516+
)}
517+
498518
<div className="chat-area" ref={chatAreaRef}>
499519
{agent.messages.length === 0 && agent.currentTurnBlocks.length === 0 && !agent.isAgentTurn && agent.connectionPhase === "ready" && (
500520
<div className="empty-state">Send a message to start chatting</div>
@@ -542,6 +562,13 @@ export default function App() {
542562
/>
543563
)}
544564

565+
{agent.agentType === "codex" && agent.pendingApproval && (
566+
<ApprovalPrompt
567+
approval={agent.pendingApproval}
568+
onRespond={agent.respondToApproval}
569+
/>
570+
)}
571+
545572
{agent.agentType === "acp" && agent.pendingPermission && (
546573
<PermissionDialog
547574
permission={agent.pendingPermission}

examples/combined-app/src/client/components/AgentSidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function AgentSidebar({
4242
>
4343
<div className="agent-sidebar-item-top">
4444
<span className="agent-sidebar-type-badge">
45-
{agent.agentType === "claude" ? "C" : "A"}
45+
{agent.agentType === "claude" ? "C" : agent.agentType === "codex" ? "X" : "A"}
4646
</span>
4747
<span className="agent-sidebar-item-name">{agent.name}</span>
4848
<button
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { PendingApproval } from "../types.js";
2+
3+
export function ApprovalPrompt({
4+
approval,
5+
onRespond,
6+
}: {
7+
approval: PendingApproval;
8+
onRespond: (requestId: string, approve: boolean) => void;
9+
}) {
10+
return (
11+
<div className="elicitation-form">
12+
<div className="elicitation-message">
13+
Approval requested: <strong>{approval.summary}</strong>
14+
</div>
15+
<pre className="permission-raw-input">
16+
{JSON.stringify(approval.rawRequest.params, null, 2)}
17+
</pre>
18+
<div className="permission-actions">
19+
<button
20+
className="btn permission-btn permission-btn-allow"
21+
onClick={() => onRespond(approval.requestId, true)}
22+
>
23+
Approve
24+
</button>
25+
<button
26+
className="btn permission-btn permission-btn-reject"
27+
onClick={() => onRespond(approval.requestId, false)}
28+
>
29+
Decline
30+
</button>
31+
</div>
32+
</div>
33+
);
34+
}

examples/combined-app/src/client/components/SetupCard.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function SetupCard({
3737
<div className="setup-header">
3838
<h2>Combined App</h2>
3939
<p className="setup-subtitle">
40-
Launch a <strong>Claude Code</strong> or <strong>ACP</strong> agent in a secure cloud sandbox and interact through a unified interface.
40+
Launch a <strong>Claude Code</strong>, <strong>Codex</strong>, or <strong>ACP</strong> agent in a secure cloud sandbox and interact through a unified interface.
4141
</p>
4242
</div>
4343

@@ -79,6 +79,13 @@ export function SetupCard({
7979
>
8080
Claude Code
8181
</button>
82+
<button
83+
className={`agent-type-btn ${agentType === "codex" ? "active" : ""}`}
84+
onClick={() => setAgentType("codex")}
85+
disabled={connecting}
86+
>
87+
Codex
88+
</button>
8289
<button
8390
className={`agent-type-btn ${agentType === "acp" ? "active" : ""}`}
8491
onClick={() => setAgentType("acp")}
@@ -104,7 +111,7 @@ export function SetupCard({
104111
</>
105112
)}
106113

107-
{agentType === "claude" && (
114+
{(agentType === "claude" || agentType === "codex") && (
108115
<>
109116
<div className="form-group">
110117
<label>Blueprint Name</label>
@@ -113,8 +120,15 @@ export function SetupCard({
113120
</div>
114121
<div className="form-group">
115122
<label>Model</label>
116-
<div className="form-hint">Claude model to use. Leave empty for default.</div>
117-
<input value={model} onChange={(e) => setModel(e.target.value)} placeholder="claude-sonnet-4-20250514" disabled={connecting} />
123+
<div className="form-hint">
124+
{agentType === "claude" ? "Claude model to use. Leave empty for default." : "Codex model to use. Leave empty for default."}
125+
</div>
126+
<input
127+
value={model}
128+
onChange={(e) => setModel(e.target.value)}
129+
placeholder={agentType === "claude" ? "claude-sonnet-4-20250514" : "gpt-5.1-codex"}
130+
disabled={connecting}
131+
/>
118132
</div>
119133
</>
120134
)}
@@ -142,7 +156,11 @@ export function SetupCard({
142156
disabled={connecting}
143157
/>
144158
<span className="config-toggle-label">
145-
{agentType === "claude" ? "Skip permissions (--dangerously-skip-permissions)" : "Auto-approve permissions"}
159+
{agentType === "claude"
160+
? "Skip permissions (--dangerously-skip-permissions)"
161+
: agentType === "codex"
162+
? "Auto-approve approvals"
163+
: "Auto-approve permissions"}
146164
</span>
147165
</label>
148166
</div>

0 commit comments

Comments
 (0)