branch:
README.md
3159 bytesRaw
# Chat Rooms — Multiple Conversations via Sub-Agents

A chat app with rooms where each room is a **sub-agent** with its own isolated SQLite and conversation history. Create rooms, switch between them, and stream LLM responses — all under a single Durable Object.

## How It Works

```
OverseerAgent (extends Agent)
  ├── Room registry (own SQLite)
  ├── Per-connection routing via connection.setState({ activeRoomId })
  │
  ├── this.subAgent(ChatRoom, "room-abc")  →  own SQLite, own LLM calls
  ├── this.subAgent(ChatRoom, "room-def")  →  own SQLite, own LLM calls
  └── this.subAgent(ChatRoom, "room-ghi")  →  own SQLite, own LLM calls
```

- **OverseerAgent** manages the room list and routes chat messages to the active room's sub-agent
- **ChatRoom** stores messages and streams LLM responses via `toUIMessageStream()` — each room has a completely independent conversation
- Deleting a room calls `this.deleteSubAgent()` — the sub-agent and its storage are permanently removed
- No mixin needed — `Agent` has `subAgent()` / `deleteSubAgent()` built in

## Key Pattern

```typescript
import { Agent } from "agents";

export class ChatRoom extends Agent<Env> {
  onStart() {
    this.sql`CREATE TABLE IF NOT EXISTS messages (...)`;
  }

  async chatStream(
    userMessage: string,
    callback: { onEvent(json: string): void; onDone(msg: ChatMessage): void }
  ) {
    // Store message, load history, stream LLM response via callback
    const result = streamText({ model, messages: history });
    for await (const chunk of result.toUIMessageStream()) {
      await callback.onEvent(JSON.stringify(chunk));
    }
  }
}

export class OverseerAgent extends Agent<Env, RoomsState> {
  async sendMessage(connection: Connection, text: string) {
    const roomId = this._getActiveRoomId(connection);
    const room = await this.subAgent(ChatRoom, `room-${roomId}`);
    await room.chatStream(text, new StreamRelay(connection, requestId));
  }
}
```

The streaming protocol uses `stream-start`, `stream-event` (serialized `UIMessageChunk`), and `stream-done` messages. The client builds a custom `ChatTransport` for the AI SDK's `useChat` hook, with support for request ID correlation, cancel, and stream resumption on room switch.

## Quick Start

```bash
npm start
```

## Try It

1. Click **New** to create a room
2. Type a message — it streams from that room's sub-agent
3. Create another room, switch to it — empty conversation
4. Switch back — previous conversation is still there (persisted in the sub-agent's SQLite)
5. Switch rooms mid-stream — the server keeps generating, and switching back resumes the stream
6. **Clear** empties a room's messages, **Delete** removes the room and its sub-agent entirely

## Related

- [gadgets-subagents](../gadgets-subagents) — fan-out/fan-in with parallel sub-agents
- [gadgets-gatekeeper](../gadgets-gatekeeper) — gated database access via sub-agent boundary
- [gadgets-sandbox](../gadgets-sandbox) — isolated database sub-agent with dynamic Worker isolates
- [design/rfc-sub-agents.md](../../design/rfc-sub-agents.md) — RFC for the sub-agent API