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
1 change: 1 addition & 0 deletions examples/blueprint/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
FROM runloop:runloop/starter-x86_64

RUN npm install -g @zed-industries/codex-acp
RUN npm install -g @earendil-works/pi-coding-agent@0.82.1

USER user
WORKDIR /home/user
Expand Down
3 changes: 2 additions & 1 deletion examples/blueprint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> **Alpha — subject to change.** This example uses an SDK in early development. APIs and behavior may change without notice between versions.

Builds the shared `axon-agents` Runloop [blueprint](https://docs.runloop.ai/guides/blueprints) used by examples that demonstrate pre-baked agent images. The blueprint bakes the agent binaries (Claude Code, OpenCode, Codex ACP) into a devbox image so subsequent devboxes start quickly and reproducibly.
Builds the shared `axon-agents` Runloop [blueprint](https://docs.runloop.ai/guides/blueprints) used by examples that demonstrate pre-baked agent images. The blueprint bakes the agent binaries (Claude Code, OpenCode, Codex ACP, Codex CLI, Pi) into a devbox image so subsequent devboxes start quickly and reproducibly.

**You must run this once before any other example will work.** The other examples create devboxes with `blueprint_name: "axon-agents"` — if that blueprint does not exist on your Runloop account, devbox creation will fail.

Expand Down Expand Up @@ -44,6 +44,7 @@ See [`Dockerfile`](Dockerfile) for the exact contents. At the time of writing it
- [OpenCode](https://opencode.ai) — for ACP examples using OpenCode
- [Codex ACP](https://www.npmjs.com/package/@zed-industries/codex-acp) — for ACP examples using Codex
- [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)
- [Pi](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) — for native Pi module examples (pinned to 0.82.1, matching the wire types hand-written in `sdk/src/pi/protocol/`)

## Alternatives

Expand Down
2 changes: 2 additions & 0 deletions examples/combined-app/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
RUNLOOP_API_KEY=your_runloop_api_key
ANTHROPIC_API_KEY=your_anthropic_api_key
OPENAI_API_KEY=your_openai_api_key
NEBIUS_API_KEY=your_nebius_api_key
NEBIUS_BASE_URL=your_nebius_endpoint_url
18 changes: 14 additions & 4 deletions examples/combined-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

> **Alpha — subject to change.** This example uses an SDK in early development. APIs and behavior may change without notice between versions.

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.
A full-stack demo that supports ACP, Claude Code, Codex, and Pi 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.

## Prerequisites

- Node.js 22+
- A [Runloop](https://runloop.ai) API key
- An [Anthropic](https://anthropic.com) API key (required for Claude agents)
- An [OpenAI](https://platform.openai.com) API key (required for Codex agents)
- A Nebius API key and base URL (required for Pi agents, which run GLM-5.2 on Runloop's dedicated Nebius endpoint)
- The `@runloop/remote-agents-sdk` SDK built locally (`cd ../../sdk && bun run build`)

## Setup
Expand All @@ -29,8 +30,15 @@ Add your keys to `.env`:
RUNLOOP_API_KEY=your_runloop_api_key
ANTHROPIC_API_KEY=your_anthropic_api_key
OPENAI_API_KEY=your_openai_api_key
NEBIUS_API_KEY=your_nebius_api_key
NEBIUS_BASE_URL=your_nebius_endpoint_url
```

Pi agents read `NEBIUS_API_KEY` and `NEBIUS_BASE_URL` from the devbox
environment. At launch the server writes `~/.pi/agent/models.json` declaring a
`nebius` provider whose `apiKey` is the literal `"$NEBIUS_API_KEY"`, which Pi
interpolates from the environment — the key itself is never written to disk.

Codex agents authenticate via a `~/.codex/auth.json` written into the devbox at
launch. By default it's generated in api-key mode from `OPENAI_API_KEY`. To use
ChatGPT-plan auth instead, set `CODEX_AUTH_JSON` to the full contents of your
Expand All @@ -42,7 +50,7 @@ CODEX_AUTH_JSON="$(cat ~/.codex/auth.json)"

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

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.
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), and [`src/server/pi-manager.ts`](src/server/pi-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 and Pi agents require a blueprint built after their CLIs were added to the [`Dockerfile`](../blueprint/Dockerfile) — re-run the command below if your `axon-agents` image predates them.

From the monorepo root:

Expand All @@ -66,8 +74,8 @@ Open http://localhost:5176. The Vite dev server proxies `/api/*` and `/ws` to th

## How It Works

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`).
2. **Send a prompt** — `POST /api/prompt` dispatches to the active connection's `prompt()` (ACP) or `send()` (Claude/Codex) and returns immediately.
1. **Start an agent** — the setup card lets you choose ACP, Claude, Codex, or Pi, 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`, `CodexAxonConnection`, or `PiAxonConnection`).
2. **Send a prompt** — `POST /api/prompt` dispatches to the active connection's `prompt()` (ACP) or `send()` (Claude/Codex/Pi) and returns immediately. For Pi, `send()` resolves once the prompt is *accepted*; the turn ends only when `agent_settled` arrives.
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.
4. **Render blocks** — the React client filters events by `agentId`, builds incremental turn blocks (`useBlockManager`), and renders them through `AssistantTurn` / `TurnBlocks`.

Expand All @@ -84,6 +92,7 @@ src/
│ ├── acp-client.ts ACP Client implementation (permissions, elicitation)
│ ├── claude-manager.ts Claude connection lifecycle
│ ├── codex-manager.ts Codex connection lifecycle (threads, approvals)
│ ├── pi-manager.ts Pi connection lifecycle (sessions, steer/follow-up)
│ └── agent-registry.ts Multi-agent bookkeeping
└── client/
├── main.tsx React entry point
Expand All @@ -94,6 +103,7 @@ src/
│ ├── useACPAgent.ts ACP event handling and state
│ ├── useClaudeAgent.ts Claude event handling and state
│ ├── useCodexAgent.ts Codex event handling and state (items, approvals)
│ ├── usePiAgent.ts Pi event handling and state (streaming deltas, tools)
│ ├── useBlockManager.ts Turn block accumulation
│ ├── useAgentList.ts Agent list polling
│ ├── useAttachments.ts File/image attachment handling
Expand Down
1 change: 1 addition & 0 deletions examples/combined-app/src/client/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -1875,6 +1875,7 @@ html, body, #root {
.tl-kind-acp { background: rgba(63, 185, 80, 0.15); color: var(--success); }
.tl-kind-claude { background: rgba(136, 87, 255, 0.15); color: #a78bfa; }
.tl-kind-codex { background: rgba(240, 246, 252, 0.12); color: #e6edf3; }
.tl-kind-pi { background: rgba(249, 115, 22, 0.15); color: #fb923c; }
.tl-kind-unknown { background: rgba(139, 148, 158, 0.15); color: var(--text-secondary); }
.tl-kind-custom { background: rgba(56, 189, 248, 0.15); color: #38bdf8; }

Expand Down
32 changes: 24 additions & 8 deletions examples/combined-app/src/client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,12 +243,18 @@ export default function App() {
autoApprovePermissions: startAutoApprove,
...sharedConfig,
}
: {
blueprintName: blueprintName || undefined,
model: model || undefined,
dangerouslySkipPermissions: startAutoApprove,
...sharedConfig,
};
: selectedAgentType === "pi"
? {
blueprintName: blueprintName || undefined,
model: model || undefined,
...sharedConfig,
}
: {
blueprintName: blueprintName || undefined,
model: model || undefined,
dangerouslySkipPermissions: startAutoApprove,
...sharedConfig,
};

try {
const resp = await api<{ agentId: string; agentType: AgentType; [key: string]: unknown }>(
Expand All @@ -266,7 +272,9 @@ export default function App() {
? (blueprintName || "Claude Agent")
: selectedAgentType === "codex"
? (blueprintName || "Codex Agent")
: (agentBinary || "ACP Agent"),
: selectedAgentType === "pi"
? (blueprintName || "Pi Agent")
: (agentBinary || "ACP Agent"),
axonId: resp.axonId as string,
devboxId: resp.devboxId as string,
createdAt: Date.now(),
Expand Down Expand Up @@ -430,7 +438,9 @@ export default function App() {
? "Claude Code"
: agent.agentType === "codex"
? "Codex"
: "ACP Agent";
: agent.agentType === "pi"
? "Pi"
: "ACP Agent";

// Derive devbox status from the last devbox_lifecycle system event in messages
const lastDevboxEvent = [...agent.messages].reverse().find(
Expand Down Expand Up @@ -510,6 +520,12 @@ export default function App() {
</div>
)}

{agent.agentType === "pi" && agent.sessionId && (
<div className="controls-bar">
<span className="config-label">Session: {agent.sessionId}</span>
</div>
)}

{agent.agentType === "codex" && agent.threadId && (
<div className="controls-bar">
<span className="config-label">Thread: {agent.threadId}</span>
Expand Down
8 changes: 7 additions & 1 deletion examples/combined-app/src/client/components/AgentSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ export function AgentSidebar({
>
<div className="agent-sidebar-item-top">
<span className="agent-sidebar-type-badge">
{agent.agentType === "claude" ? "C" : agent.agentType === "codex" ? "X" : "A"}
{agent.agentType === "claude"
? "C"
: agent.agentType === "codex"
? "X"
: agent.agentType === "pi"
? "P"
: "A"}
</span>
<span className="agent-sidebar-item-name">{agent.name}</span>
<button
Expand Down
29 changes: 24 additions & 5 deletions examples/combined-app/src/client/components/SetupCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function SetupCard({
<div className="setup-header">
<h2>Combined App</h2>
<p className="setup-subtitle">
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.
Launch a <strong>Claude Code</strong>, <strong>Codex</strong>, <strong>Pi</strong>, or <strong>ACP</strong> agent in a secure cloud sandbox and interact through a unified interface.
</p>
</div>

Expand Down Expand Up @@ -86,6 +86,13 @@ export function SetupCard({
>
Codex
</button>
<button
className={`agent-type-btn ${agentType === "pi" ? "active" : ""}`}
onClick={() => setAgentType("pi")}
disabled={connecting}
>
Pi
</button>
<button
className={`agent-type-btn ${agentType === "acp" ? "active" : ""}`}
onClick={() => setAgentType("acp")}
Expand All @@ -111,7 +118,7 @@ export function SetupCard({
</>
)}

{(agentType === "claude" || agentType === "codex") && (
{(agentType === "claude" || agentType === "codex" || agentType === "pi") && (
<>
<div className="form-group">
<label>Blueprint Name</label>
Expand All @@ -121,12 +128,22 @@ export function SetupCard({
<div className="form-group">
<label>Model</label>
<div className="form-hint">
{agentType === "claude" ? "Claude model to use. Leave empty for default." : "Codex model to use. Leave empty for the default (gpt-5.6-sol)."}
{agentType === "claude"
? "Claude model to use. Leave empty for default."
: agentType === "pi"
? "Pi model, as provider/id. Leave empty for the default (nebius/glm-5.2)."
: "Codex model to use. Leave empty for the default (gpt-5.6-sol)."}
</div>
<input
value={model}
onChange={(e) => setModel(e.target.value)}
placeholder={agentType === "claude" ? "claude-sonnet-4-20250514" : "gpt-5.6-sol"}
placeholder={
agentType === "claude"
? "claude-sonnet-4-20250514"
: agentType === "pi"
? "nebius/glm-5.2"
: "gpt-5.6-sol"
}
disabled={connecting}
/>
</div>
Expand Down Expand Up @@ -160,7 +177,9 @@ export function SetupCard({
? "Skip permissions (--dangerously-skip-permissions)"
: agentType === "codex"
? "Auto-approve approvals"
: "Auto-approve permissions"}
: agentType === "pi"
? "Pi runs tools unattended (no approval protocol)"
: "Auto-approve permissions"}
</span>
</label>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,19 @@ import type {
SDKSystemMessage,
} from "@runloop/remote-agents-sdk/claude";
import type { CodexProtocolTimelineEvent } from "@runloop/remote-agents-sdk/codex";
import type { PiProtocolTimelineEvent } from "@runloop/remote-agents-sdk/pi";
import type { AgentStartedPayload, TimelineEvent } from "../types.js";
import { PayloadTree, formatTime, originLabel, originBadgeClass } from "./shared.js";

const isAgentStartedEvent = createCustomEventGuard<AgentStartedPayload>("agent_started");

type TimelineKind = "system" | "acp_protocol" | "claude_protocol" | "codex_protocol" | "unknown";
type TimelineKind =
| "system"
| "acp_protocol"
| "claude_protocol"
| "codex_protocol"
| "pi_protocol"
| "unknown";

interface TimelineSummary {
icon: string;
Expand Down Expand Up @@ -172,6 +179,53 @@ function summarizeCodexProtocol(event: CodexProtocolTimelineEvent): TimelineSumm
}
}

function summarizePiProtocol(event: PiProtocolTimelineEvent): TimelineSummary {
switch (event.eventType) {
case "agent_start":
return { icon: "\u{1F195}", label: "agent_start", summary: "", kindClass: "kind-protocol" };
case "turn_start":
return { icon: "▶️", label: "turn_start", summary: "", kindClass: "kind-protocol" };
case "turn_end":
return { icon: "✅", label: "turn_end", summary: "", kindClass: "kind-protocol" };
case "message_start":
case "message_end":
return { icon: "\u{1F4E6}", label: event.eventType, summary: event.data.message.role, kindClass: "kind-protocol" };
case "message_update": {
const delta = event.data.assistantMessageEvent;
switch (delta.type) {
case "text_delta":
return { icon: "\u{1F916}", label: "text_delta", summary: delta.delta.slice(0, 40), kindClass: "kind-protocol" };
case "thinking_delta":
return { icon: "\u{1F4AD}", label: "thinking_delta", summary: "", kindClass: "kind-protocol" };
case "error":
return { icon: "⚠️", label: "message error", summary: delta.reason, kindClass: "kind-system" };
default:
return { icon: "\u{1F4E6}", label: delta.type, summary: "", kindClass: "kind-protocol" };
}
}
case "tool_execution_start":
case "tool_execution_update":
case "tool_execution_end":
return { icon: "\u{1F527}", label: event.eventType, summary: event.data.toolName, kindClass: "kind-protocol" };
case "agent_end":
// Not a turn boundary: `willRetry` means Pi keeps going.
return { icon: "⏸️", label: "agent_end", summary: event.data.willRetry ? "willRetry" : "", kindClass: "kind-protocol" };
case "agent_settled":
return { icon: "⏹️", label: "agent_settled", summary: "", kindClass: "kind-protocol" };
case "response": {
const { command, success, error } = event.data;
return {
icon: success ? "✅" : "⚠️",
label: `response ${command}`,
summary: success ? "" : (error ?? "failed"),
kindClass: success ? "kind-protocol" : "kind-system",
};
}
default:
return { icon: "\u{1F4E6}", label: (event as { eventType: string }).eventType, summary: "", kindClass: "kind-protocol" };
}
}

function summarizeTimelineEvent(event: TimelineEvent): TimelineSummary {
if (isTurnStartedEvent(event)) {
return { icon: "\u25B6\uFE0F", label: "turn.started", summary: "", kindClass: "kind-system" };
Expand Down Expand Up @@ -204,6 +258,8 @@ function summarizeTimelineEvent(event: TimelineEvent): TimelineSummary {
return summarizeClaudeProtocol(event);
case "codex_protocol":
return summarizeCodexProtocol(event);
case "pi_protocol":
return summarizePiProtocol(event);
case "unknown": {
if (isAgentStartedEvent(event)) {
return { icon: "\u2699\uFE0F", label: "Agent Started", summary: event.data.agentType ?? "", kindClass: "kind-custom" };
Expand All @@ -226,6 +282,7 @@ function kindBadgeLabel(kind: TimelineKind, custom: boolean): string {
case "acp_protocol": return "ACP";
case "claude_protocol": return "CLAUDE";
case "codex_protocol": return "CODEX";
case "pi_protocol": return "PI";
case "unknown": return "?";
}
}
Expand All @@ -237,6 +294,7 @@ function kindBadgeClass(kind: TimelineKind, custom: boolean): string {
case "acp_protocol": return "tl-kind-acp";
case "claude_protocol": return "tl-kind-claude";
case "codex_protocol": return "tl-kind-codex";
case "pi_protocol": return "tl-kind-pi";
case "unknown": return "tl-kind-unknown";
}
}
Expand Down
Loading
Loading