# Pi-Web API Routes and AgentSession Communication Architecture Explained

> Explore the agegr/pi-web architecture. Learn how its API routes and AgentSession communication leverage SSE for real-time streaming and connect React frontend to the Pi SDK.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: architecture
- Published: 2026-07-26

---

**Pi-Web implements a REST-like API layer that bridges React frontend commands to the Pi SDK through a global session registry and `AgentSessionWrapper` abstraction, using Server-Sent Events (SSE) for real-time streaming.**

The `agegr/pi-web` repository provides a Next.js-based interface for the Pi coding agent, requiring a robust architecture to manage long-running agent sessions. Understanding the pi-web API routes and AgentSession communication architecture reveals how HTTP requests transform into SDK commands and how the system maintains stateful connections across server hot-reloads. All routes ultimately delegate to [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts), which houses the session registry and wrapper logic.

## REST-Like API Route Structure

The frontend communicates with the agent through specific HTTP endpoints defined in the `app/api/` directory. Each route handles a distinct aspect of session management.

### Creating New Sessions via `POST /api/agent/new`

Located at [`app/api/agent/new/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/agent/new/route.ts), this endpoint initializes fresh agent instances. It calls `startRpcSession()` to spin up a new `AgentSessionWrapper` and optionally sends the first prompt immediately.

### Command Execution and State Retrieval via `/api/agent/[id]`

The dynamic route `app/api/agent/[id]/route.ts` handles two operations:

- **POST requests**: Look up existing sessions via `getRpcSession()`. If the session isn't alive, it resolves the session file and recreates the wrapper before forwarding the command via `session.send(body)`.
- **GET requests**: Retrieve the current session state by issuing a `"get_state"` command to the wrapper.

### Real-Time Event Streaming via `GET /api/agent/[id]/events`

The `app/api/agent/[id]/events/route.ts` route opens a Server-Sent Events (SSE) stream. It subscribes to the wrapper's `onEvent` emitter and forwards every SDK event (tool calls, streaming updates, compaction) to the client with proper heartbeat keepalives.

### Running Session Notifications via `GET /api/agent/running/events`

This endpoint powers the sidebar UI by calling `subscribeRunningSessions()` from [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts). It broadcasts updates whenever sessions start or stop running, ensuring the interface displays accurate status badges.

## Session Lifecycle Management in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts)

The core orchestration logic resides in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts), which manages session creation, persistence, and concurrency control.

### The `startRpcSession` Function

This async function initializes the connection between HTTP requests and the Pi SDK:

```typescript
// lib/rpc-manager.ts
export async function startRpcSession(
  sessionId: string,
  sessionFile: string,
  cwd: string,
  toolNames?: string[]
): Promise<{ session: AgentSessionWrapper; realSessionId: string }> {
  // 1️⃣ Resolve or create a SessionManager (opens existing .jsonl or creates new)
  const sessionManager = sessionFile
    ? SessionManager.open(sessionFile, undefined)
    : SessionManager.create(cwd, undefined);

  // 2️⃣ Build services & the inner AgentSession
  const services = await createAgentSessionServices({ cwd, agentDir });
  const { session: inner } = await createAgentSessionFromServices({
    services,
    sessionManager,
    ...(toolsOption !== undefined ? { tools: toolsOption } : {}),
  });

  // 3️⃣ Wrap it so the rest of Pi-Web can use a stable interface
  const wrapper = new AgentSessionWrapper(inner);
  wrapper.start();                     // subscribes to SDK events
  wrapper.beginExtensionBinding(...); // loads extensions (if any)

  // 4️⃣ Register the wrapper globally (survives Next.js hot-reload)
  const realSessionId = inner.sessionId as string;
  getRegistry().set(realSessionId, wrapper);
  return { session: wrapper, realSessionId };
}

```

### Global Registry and Concurrency Control

Two global mechanisms ensure session stability:

- **`globalThis.__piSessions`**: A `Map` storing `AgentSessionWrapper` objects keyed by real session ID. This registry persists across Next.js hot-reloads, allowing the same session to be accessed from any route handler after code changes.
- **`globalThis.__piStartLocks`**: A lock map preventing race conditions where concurrent requests might attempt to create duplicate sessions for the same temporary key.

## The AgentSessionWrapper Bridge

`AgentSessionWrapper` acts as a façade between the HTTP layer and the underlying Pi SDK (`@earendil-works/pi-coding-agent`). Defined in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts), it exposes a stable interface for route handlers.

### Command Dispatch via `send()`

The wrapper validates incoming commands, optionally waits for extensions to bind, and forwards to the appropriate SDK method:

```typescript
// lib/rpc-manager.ts
async send(command: Record<string, unknown>): Promise<unknown> {
  const type = command.type as string;
  if (this.shouldWaitForExtensions(type)) await this.waitForExtensionsBound();

  switch (type) {
    case "prompt":
      // forward to inner.prompt(...)
      break;
    case "fork":
      // creates a new session file and returns its ID
      break;
    case "get_state":
      // returns a snapshot of the inner.session state
      break;
    // … many other cases (bash, steer, follow_up, set_model, etc.)
  }
}

```

### Event Subscription with `onEvent()`

The wrapper exposes an `onEvent(listener)` method that registers callbacks for every SDK event. The SSE route utilizes this to capture tool calls, assistant messages, compaction events, and session terminations.

## Real-Time Event Streaming Architecture

The SSE implementation in `app/api/agent/[id]/events/route.ts` creates a persistent connection for live updates.

```typescript
// app/api/agent/[id]/events/route.ts
const unsubscribe = session.onEvent((event) => {
  encode(event);               // push `data: {...}\n\n` to the stream
});

```

The implementation includes a heartbeat mechanism sending colon (`:`) lines to prevent proxies from closing idle connections. When the client aborts the connection, the stream cleans up by calling `unsubscribe()`, clearing the heartbeat interval, and closing the controller.

## Running Session Broadcasting

A lightweight broadcaster keeps the UI sidebar synchronized with active sessions. The `notifyRunningChange()` function in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) collects IDs where `session.isRunning()` returns true and broadcasts updates only when the set changes.

The `/api/agent/running/events` route attaches listeners via `subscribeRunningSessions()` and forwards the session ID array as SSE data, enabling real-time badge updates without polling.

## Practical Implementation Examples

### Creating a Session and Sending the Initial Prompt

```typescript
// POST /api/agent/new
await fetch('/api/agent/new', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    cwd: '/home/user/project',
    type: 'prompt',
    message: 'Explain the repository layout.',
    // optional: provider, modelId, toolNames, thinkingLevel …
  })
})
  .then(r => r.json())
  .then(({ sessionId, data }) => {
    console.log('Session started:', sessionId);
    console.log('First answer:', data);
  });

```

### Sending Follow-Up Commands

```typescript
await fetch(`/api/agent/${sessionId}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ type: 'prompt', message: 'Now list the main components.' })
})
  .then(r => r.json())
  .then(res => console.log('Response:', res.data));

```

### Listening to Live Agent Events

```typescript
const ev = new EventSource(`/api/agent/${sessionId}/events`);
ev.addEventListener('message', e => {
  const evt = JSON.parse(e.data);
  console.log('Agent event:', evt);
});

```

### Subscribing to Running Session Badges

```typescript
const running = new EventSource('/api/agent/running/events');
running.addEventListener('message', e => {
  const ids = JSON.parse(e.data); // array of sessionIds that are currently running
  updateSidebarBadges(ids);
});

```

## Summary

- **Route delegation**: All API routes in `app/api/agent/` delegate to [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts), which houses the central session registry.
- **Global persistence**: `globalThis.__piSessions` ensures `AgentSessionWrapper` instances survive Next.js hot-reloads, maintaining long-running agent processes across code changes.
- **Concurrency safety**: `globalThis.__piStartLocks` prevents duplicate session creation when multiple simultaneous requests target the same temporary session key.
- **SDK abstraction**: `AgentSessionWrapper` provides a stable HTTP-friendly interface over the raw Pi SDK, handling command validation, extension loading, and event multiplexing.
- **Real-time communication**: Server-Sent Events (SSE) power both individual session event streams (`/api/agent/[id]/events`) and running-session notifications (`/api/agent/running/events`).

## Frequently Asked Questions

### How does pi-web handle session persistence during Next.js hot-reloads?

Pi-Web stores active `AgentSessionWrapper` instances in `globalThis.__piSessions`, a global Map that persists across Next.js hot-reloads. When the server restarts due to code changes, the registry retains the wrapper references, allowing route handlers to reconnect to existing agent processes rather than creating new ones.

### What prevents duplicate session creation when multiple requests arrive simultaneously?

The system uses `globalThis.__piStartLocks`, a global lock map that tracks in-progress session initializations. When `startRpcSession()` receives a request, it checks this map before proceeding, ensuring only one caller can create a session for a given temporary key while others wait for the existing promise to resolve.

### How does the AgentSessionWrapper differ from the raw Pi SDK AgentSession?

`AgentSessionWrapper` acts as a façade that adds HTTP-layer concerns missing from the raw SDK, including idle timeout management, extension binding coordination, event listener multiplexing for multiple SSE clients, and command validation. It translates generic JSON commands from HTTP requests into specific SDK method calls like `inner.prompt()` or `inner.navigateTree()`.

### Why does pi-web use Server-Sent Events instead of WebSockets for agent communication?

Server-Sent Events provide unidirectional server-to-client streaming over standard HTTP, which simplifies deployment behind proxies and load balancers compared to WebSockets. Since the communication pattern primarily requires the server to push agent updates to the browser (with commands sent via POST requests), SSE offers a more firewall-friendly solution that automatically handles reconnection and works seamlessly with Next.js App Router edge runtime constraints.