feat: refresh docs and agent schema

This commit is contained in:
Nathan Flurry 2026-01-25 03:04:12 -08:00
parent a49ea094f3
commit 0fbf6272b1
39 changed files with 3127 additions and 1806 deletions

View file

@ -0,0 +1,26 @@
---
title: "Agent Compatibility"
description: "Supported agents, install methods, and streaming formats."
---
## Compatibility matrix
| Agent | Provider | Binary | Install method | Session ID | Streaming format |
|-------|----------|--------|----------------|------------|------------------|
| Claude Code | Anthropic | `claude` | curl raw binary from GCS | `session_id` | JSONL via stdout |
| Codex | OpenAI | `codex` | curl tarball from GitHub releases | `thread_id` | JSONL via stdout |
| OpenCode | Multi-provider | `opencode` | curl tarball from GitHub releases | `session_id` | SSE or JSONL |
| Amp | Sourcegraph | `amp` | curl raw binary from GCS | `session_id` | JSONL via stdout |
## Agent modes
- **OpenCode**: discovered via the server API.
- **Claude Code / Codex / Amp**: hardcoded modes (typically `build`, `plan`, or `custom`).
## Capability notes
- **Questions / permissions**: OpenCode natively supports these workflows. Claude plan approval is normalized into a question event.
- **Streaming**: all agents stream events; OpenCode uses SSE while others use JSONL.
- **Files and images**: normalized via `UniversalMessagePart` with `File` and `Image` parts.
See [Universal API](/universal-api) for feature coverage details.

45
docs/architecture.mdx Normal file
View file

@ -0,0 +1,45 @@
---
title: "Architecture"
description: "How the daemon, schemas, and agents fit together."
---
Sandbox Agent SDK is built around a single daemon that runs inside the sandbox and exposes a universal HTTP API. Clients use the API (or the TypeScript SDK / CLI) to create sessions, send messages, and stream events.
## Components
- **Daemon**: Rust HTTP server that manages agent processes and streaming.
- **Universal schema**: Shared input/output types for messages and events.
- **SDKs & CLI**: Convenience wrappers around the HTTP API.
## Session model
- **Session ID**: Client-provided primary session identifier.
- **Agent session ID**: Underlying ID from the agent (thread/session). This is surfaced in events but is not the primary key.
## Event streaming
- Events are stored in memory per session and assigned a monotonically increasing `id`.
- `/events` returns a slice of events by offset/limit.
- `/events/sse` streams new events from the same offset semantics.
## Agent integration strategies
### Subprocess per session
Claude Code, Codex, and Amp run as subprocesses. The daemon reads JSONL output from stdout and converts each event into a UniversalEvent.
### Shared server (OpenCode)
OpenCode runs as a shared server. The daemon connects via HTTP and SSE, then converts OpenCode events to UniversalEvents.
## Human-in-the-loop
Questions and permission prompts are normalized into the universal schema:
- Question events surface as `questionAsked` with selectable options.
- Permission events surface as `permissionAsked` with `reply: once | always | reject`.
- Claude plan approval is normalized into a question event (approve/reject).
## Authentication
The daemon uses a **global token** configured at startup. All HTTP and CLI operations reuse the same token and are validated against the `Authorization` header (`Bearer` or `Token`) or `x-sandbox-token`.

111
docs/cli.mdx Normal file
View file

@ -0,0 +1,111 @@
---
title: "CLI"
description: "CLI reference and server flags."
---
The `sandbox-agent` CLI mirrors the HTTP API so you can script everything without writing client code.
## Server flags
```bash
sandbox-agent --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 8787
```
- `--token`: global token for all requests.
- `--no-token`: disable auth (local dev only).
- `--host`, `--port`: bind address.
- `--cors-allow-origin`, `--cors-allow-method`, `--cors-allow-header`, `--cors-allow-credentials`: configure CORS.
## Agent commands
<details>
<summary><strong>agents list</strong></summary>
```bash
sandbox-agent agents list --endpoint http://127.0.0.1:8787
```
</details>
<details>
<summary><strong>agents install</strong></summary>
```bash
sandbox-agent agents install claude --reinstall --endpoint http://127.0.0.1:8787
```
</details>
<details>
<summary><strong>agents modes</strong></summary>
```bash
sandbox-agent agents modes claude --endpoint http://127.0.0.1:8787
```
</details>
## Session commands
<details>
<summary><strong>sessions create</strong></summary>
```bash
sandbox-agent sessions create my-session \
--agent claude \
--agent-mode build \
--permission-mode default \
--endpoint http://127.0.0.1:8787
```
</details>
<details>
<summary><strong>sessions send-message</strong></summary>
```bash
sandbox-agent sessions send-message my-session \
--message "Summarize the repository" \
--endpoint http://127.0.0.1:8787
```
</details>
<details>
<summary><strong>sessions events</strong></summary>
```bash
sandbox-agent sessions events my-session --offset 0 --limit 50 --endpoint http://127.0.0.1:8787
```
</details>
<details>
<summary><strong>sessions events-sse</strong></summary>
```bash
sandbox-agent sessions events-sse my-session --offset 0 --endpoint http://127.0.0.1:8787
```
</details>
<details>
<summary><strong>sessions reply-question</strong></summary>
```bash
sandbox-agent sessions reply-question my-session QUESTION_ID \
--answers "yes" \
--endpoint http://127.0.0.1:8787
```
</details>
<details>
<summary><strong>sessions reject-question</strong></summary>
```bash
sandbox-agent sessions reject-question my-session QUESTION_ID --endpoint http://127.0.0.1:8787
```
</details>
<details>
<summary><strong>sessions reply-permission</strong></summary>
```bash
sandbox-agent sessions reply-permission my-session PERMISSION_ID \
--reply once \
--endpoint http://127.0.0.1:8787
```
</details>

View file

@ -0,0 +1,21 @@
---
title: "Cloudflare Sandboxes"
description: "Deploy the daemon in Cloudflare Sandboxes."
---
## Steps
1. Create a Cloudflare Sandbox with a Linux runtime.
2. Install the agent binaries and the sandbox-agent daemon.
3. Start the daemon and expose the HTTP port.
```bash
export SANDBOX_TOKEN="..."
cargo run -p sandbox-agent -- \
--token "$SANDBOX_TOKEN" \
--host 0.0.0.0 \
--port 8787
```
4. Connect your client to the sandbox endpoint.

View file

@ -0,0 +1,21 @@
---
title: "Daytona"
description: "Run the daemon in a Daytona workspace."
---
## Steps
1. Create a Daytona workspace with Rust and curl available.
2. Install or build the sandbox-agent binary.
3. Start the daemon and expose port `8787` (or your preferred port).
```bash
export SANDBOX_TOKEN="..."
cargo run -p sandbox-agent -- \
--token "$SANDBOX_TOKEN" \
--host 0.0.0.0 \
--port 8787
```
4. Use your Daytona port forwarding to reach the daemon from your client.

View file

@ -0,0 +1,27 @@
---
title: "Docker (dev)"
description: "Build and run the daemon in a Docker container."
---
## Build the binary
Use the release Dockerfile to build a static binary:
```bash
docker build -f docker/release/linux-x86_64.Dockerfile -t sandbox-agent-build .
docker run --rm -v "$PWD/artifacts:/artifacts" sandbox-agent-build
```
The binary will be written to `./artifacts/sandbox-agent-x86_64-unknown-linux-musl`.
## Run the daemon
```bash
docker run --rm -p 8787:8787 \
-v "$PWD/artifacts:/artifacts" \
debian:bookworm-slim \
/artifacts/sandbox-agent-x86_64-unknown-linux-musl --token "$SANDBOX_TOKEN" --host 0.0.0.0 --port 8787
```
You can now access the API at `http://localhost:8787`.

25
docs/deployments/e2b.mdx Normal file
View file

@ -0,0 +1,25 @@
---
title: "E2B"
description: "Deploy the daemon inside an E2B sandbox."
---
## Steps
1. Start an E2B sandbox with network access.
2. Install the agent binaries you need (Claude, Codex, OpenCode, Amp).
3. Run the daemon and expose its port.
Example startup script:
```bash
export SANDBOX_TOKEN="..."
# Install sandbox-agent binary (or build from source)
# TODO: replace with release download once published
cargo run -p sandbox-agent -- \
--token "$SANDBOX_TOKEN" \
--host 0.0.0.0 \
--port 8787
```
4. Configure your client to connect to the sandbox endpoint.

View file

@ -0,0 +1,21 @@
---
title: "Vercel Sandboxes"
description: "Run the daemon inside Vercel Sandboxes."
---
## Steps
1. Provision a Vercel Sandbox with network access and storage.
2. Install the agent binaries you need.
3. Run the daemon and expose the port.
```bash
export SANDBOX_TOKEN="..."
cargo run -p sandbox-agent -- \
--token "$SANDBOX_TOKEN" \
--host 0.0.0.0 \
--port 8787
```
4. Configure your client to use the sandbox URL.

82
docs/docs.json Normal file
View file

@ -0,0 +1,82 @@
{
"$schema": "https://mintlify.com/docs.json",
"theme": "dark",
"name": "Sandbox Agent SDK",
"colors": {
"primary": "#ff4f00",
"light": "#ff6a2a",
"dark": "#cc3f00",
"background": "#000000",
"card": "#1c1c1e",
"border": "#2c2c2e",
"inputBackground": "#2c2c2e",
"inputBorder": "#3a3a3c",
"text": "#ffffff",
"muted": "#8e8e93",
"success": "#30d158",
"warning": "#ff4f00",
"danger": "#ff3b30",
"purple": "#bf5af2"
},
"favicon": "/favicon.svg",
"logo": {
"light": "/logo/light.svg",
"dark": "/logo/dark.svg"
},
"navigation": {
"tabs": [
{
"tab": "Guides",
"groups": [
{
"group": "Getting started",
"pages": [
"index",
"quickstart",
"architecture",
"agent-compatibility",
"universal-api"
]
},
{
"group": "Operations",
"pages": [
"frontend"
]
}
]
},
{
"tab": "Reference",
"groups": [
{
"group": "Interfaces",
"pages": [
"cli",
"http-api",
"typescript-sdk"
]
}
]
},
{
"tab": "Deployments",
"groups": [
{
"group": "Examples",
"pages": [
"deployments/docker",
"deployments/e2b",
"deployments/daytona",
"deployments/vercel-sandboxes",
"deployments/cloudflare-sandboxes"
]
}
]
}
]
},
"styles": [
"/theme.css"
]
}

19
docs/favicon.svg Normal file
View file

@ -0,0 +1,19 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.06145 23.1079C5.26816 22.3769 -3.39077 20.6274 1.4173 5.06384C9.6344 6.09939 16.9728 14.0644 9.06145 23.1079Z" fill="url(#paint0_linear_17557_2021)"/>
<path d="M8.91928 23.0939C5.27642 21.2223 0.78371 4.20891 17.0071 0C20.7569 7.19341 19.6212 16.5452 8.91928 23.0939Z" fill="url(#paint1_linear_17557_2021)"/>
<path d="M8.91388 23.0788C8.73534 19.8817 10.1585 9.08525 23.5699 13.1107C23.1812 20.1229 18.984 26.4182 8.91388 23.0788Z" fill="url(#paint2_linear_17557_2021)"/>
<defs>
<linearGradient id="paint0_linear_17557_2021" x1="3.77557" y1="5.91571" x2="5.23185" y2="21.5589" gradientUnits="userSpaceOnUse">
<stop stop-color="#18E299"/>
<stop offset="1" stop-color="#15803D"/>
</linearGradient>
<linearGradient id="paint1_linear_17557_2021" x1="12.1711" y1="-0.718425" x2="10.1897" y2="22.9832" gradientUnits="userSpaceOnUse">
<stop stop-color="#16A34A"/>
<stop offset="1" stop-color="#4ADE80"/>
</linearGradient>
<linearGradient id="paint2_linear_17557_2021" x1="23.1327" y1="15.353" x2="9.33841" y2="18.5196" gradientUnits="userSpaceOnUse">
<stop stop-color="#4ADE80"/>
<stop offset="1" stop-color="#0D9373"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

20
docs/frontend.mdx Normal file
View file

@ -0,0 +1,20 @@
---
title: "Frontend Demo"
description: "Run the Vite + React UI for testing the daemon."
---
The demo frontend lives at `frontend/packages/web`.
## Run locally
```bash
pnpm install
pnpm --filter @sandbox-agent/web dev
```
The UI expects:
- Endpoint (e.g. `http://127.0.0.1:8787`)
- Optional token
If you see CORS errors, enable CORS on the daemon with `--cors-allow-origin` and related flags.

157
docs/http-api.mdx Normal file
View file

@ -0,0 +1,157 @@
---
title: "HTTP API"
description: "Endpoint reference for the sandbox agent daemon."
---
All endpoints are under `/v1`. Authentication uses the daemon-level token via `Authorization: Bearer <token>` or `x-sandbox-token`.
## Sessions
<details>
<summary><strong>POST /v1/sessions/{sessionId}</strong> - Create session</summary>
Request:
```json
{
"agent": "claude",
"agentMode": "build",
"permissionMode": "default",
"model": "claude-3-5-sonnet",
"variant": "high",
"agentVersion": "latest"
}
```
Response:
```json
{
"healthy": true,
"agentSessionId": "..."
}
```
</details>
<details>
<summary><strong>POST /v1/sessions/{sessionId}/messages</strong> - Send message</summary>
Request:
```json
{
"message": "Describe the repository."
}
```
</details>
<details>
<summary><strong>GET /v1/sessions/{sessionId}/events</strong> - Fetch events</summary>
Query params:
- `offset`: last-seen event id (exclusive)
- `limit`: max number of events
Response:
```json
{
"events": [
{
"id": 1,
"timestamp": "2026-01-25T10:00:00Z",
"sessionId": "my-session",
"agent": "claude",
"agentSessionId": "...",
"data": { "message": { "role": "assistant", "parts": [{ "type": "text", "text": "..." }] } }
}
],
"hasMore": false
}
```
</details>
<details>
<summary><strong>GET /v1/sessions/{sessionId}/events/sse</strong> - Stream events (SSE)</summary>
Query params:
- `offset`: last-seen event id (exclusive)
SSE payloads are `UniversalEvent` JSON.
</details>
<details>
<summary><strong>POST /v1/sessions/{sessionId}/questions/{questionId}/reply</strong></summary>
Request:
```json
{ "answers": [["Option A"], ["Option B", "Option C"]] }
```
</details>
<details>
<summary><strong>POST /v1/sessions/{sessionId}/questions/{questionId}/reject</strong></summary>
Request:
```json
{}
```
</details>
<details>
<summary><strong>POST /v1/sessions/{sessionId}/permissions/{permissionId}/reply</strong></summary>
Request:
```json
{ "reply": "once" }
```
</details>
## Agents
<details>
<summary><strong>GET /v1/agents</strong> - List agents</summary>
Response:
```json
{
"agents": [
{ "id": "claude", "installed": true, "version": "...", "path": "/usr/local/bin/claude" }
]
}
```
</details>
<details>
<summary><strong>POST /v1/agents/{agentId}/install</strong> - Install agent</summary>
Request:
```json
{ "reinstall": false }
```
</details>
<details>
<summary><strong>GET /v1/agents/{agentId}/modes</strong> - List modes</summary>
Response:
```json
{
"modes": [
{ "id": "build", "name": "Build", "description": "Default coding mode" }
]
}
```
</details>
## Error handling
All errors use RFC 7807 Problem Details and stable `type` strings (e.g. `urn:sandbox-agent:error:session_not_found`).

68
docs/index.mdx Normal file
View file

@ -0,0 +1,68 @@
---
title: "Overview"
description: "Universal API for running Claude Code, Codex, OpenCode, and Amp inside sandboxes."
---
Sandbox Agent SDK is a universal API and daemon for running coding agents inside sandboxes. It standardizes agent sessions, events, and human-in-the-loop workflows across Claude Code, Codex, OpenCode, and Amp.
## At a glance
- Universal HTTP API and TypeScript SDK
- Runs inside sandboxes with a lightweight Rust daemon
- Streams events in a shared UniversalEvent schema
- Supports questions and permission workflows
- Designed for multi-provider sandbox environments
## Quickstart
Run the daemon locally:
```bash
sandbox-agent --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 8787
```
Send a message:
```bash
curl -X POST "http://127.0.0.1:8787/v1/sessions/my-session" \
-H "Authorization: Bearer $SANDBOX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"agent":"claude"}'
curl -X POST "http://127.0.0.1:8787/v1/sessions/my-session/messages" \
-H "Authorization: Bearer $SANDBOX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message":"Explain the repo structure."}'
```
See the full quickstart in [Quickstart](/quickstart).
## What this project solves
- **Universal Coding Agent API**: standardize tool calls, messages, and events across agents.
- **Agents in sandboxes**: run a single HTTP daemon inside any sandbox provider.
- **Agent transcripts**: stream or persist a universal event log in your own storage.
## Project scope
**In scope**
- Agent session orchestration inside a sandbox
- Streaming events in a universal schema
- Human-in-the-loop questions and permissions
- TypeScript SDK and CLI wrappers
**Out of scope**
- Persistent storage of sessions on disk
- Building custom LLM agents (use Vercel AI SDK for that)
- Sandbox provider APIs (use provider SDKs or custom glue)
- Git repo management
## Next steps
- Read the [Architecture](/architecture) overview
- Review [Agent compatibility](/agent-compatibility)
- See the [HTTP API](/http-api) and [CLI](/cli)
- Run the [Frontend demo](/frontend)
- Use the [TypeScript SDK](/typescript-sdk)

21
docs/logo/dark.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

21
docs/logo/light.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

75
docs/quickstart.mdx Normal file
View file

@ -0,0 +1,75 @@
---
title: "Quickstart"
description: "Start the daemon and send your first message."
---
## 1. Run the daemon
Use the installed binary, or `cargo run` in development.
```bash
sandbox-agent --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 8787
```
If you want to run without auth (local dev only):
```bash
sandbox-agent --no-token --host 127.0.0.1 --port 8787
```
### CORS (frontend usage)
If you are calling the daemon from a browser, enable CORS explicitly:
```bash
sandbox-agent \
--token "$SANDBOX_TOKEN" \
--cors-allow-origin "http://localhost:5173" \
--cors-allow-method "GET" \
--cors-allow-method "POST" \
--cors-allow-header "Authorization" \
--cors-allow-header "Content-Type" \
--cors-allow-credentials
```
## 2. Create a session
```bash
curl -X POST "http://127.0.0.1:8787/v1/sessions/my-session" \
-H "Authorization: Bearer $SANDBOX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"agent":"claude","agentMode":"build","permissionMode":"default"}'
```
## 3. Send a message
```bash
curl -X POST "http://127.0.0.1:8787/v1/sessions/my-session/messages" \
-H "Authorization: Bearer $SANDBOX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message":"Summarize the repository and suggest next steps."}'
```
## 4. Read events
```bash
curl "http://127.0.0.1:8787/v1/sessions/my-session/events?offset=0&limit=50" \
-H "Authorization: Bearer $SANDBOX_TOKEN"
```
For streaming output, use SSE:
```bash
curl "http://127.0.0.1:8787/v1/sessions/my-session/events/sse?offset=0" \
-H "Authorization: Bearer $SANDBOX_TOKEN"
```
## 5. CLI shortcuts
The CLI mirrors the HTTP API:
```bash
sandbox-agent sessions create my-session --agent claude --endpoint http://127.0.0.1:8787 --token "$SANDBOX_TOKEN"
sandbox-agent sessions send-message my-session --message "Hello" --endpoint http://127.0.0.1:8787 --token "$SANDBOX_TOKEN"
```

72
docs/theme.css Normal file
View file

@ -0,0 +1,72 @@
:root {
color-scheme: dark;
--sa-primary: #ff4f00;
--sa-bg: #000000;
--sa-card: #1c1c1e;
--sa-border: #2c2c2e;
--sa-input-bg: #2c2c2e;
--sa-input-border: #3a3a3c;
--sa-text: #ffffff;
--sa-muted: #8e8e93;
--sa-success: #30d158;
--sa-warning: #ff4f00;
--sa-danger: #ff3b30;
--sa-purple: #bf5af2;
}
html,
body {
background-color: var(--sa-bg);
color: var(--sa-text);
}
a {
color: var(--sa-primary);
}
a:hover {
color: var(--sa-warning);
}
hr {
border-color: var(--sa-border);
}
input,
textarea,
select {
background-color: var(--sa-input-bg);
border: 1px solid var(--sa-input-border);
color: var(--sa-text);
}
code,
pre {
background-color: var(--sa-card);
border: 1px solid var(--sa-border);
color: var(--sa-text);
}
.card,
.mintlify-card,
.docs-card {
background-color: var(--sa-card);
border: 1px solid var(--sa-border);
}
.muted,
.text-muted {
color: var(--sa-muted);
}
.alert-success {
border-color: var(--sa-success);
}
.alert-warning {
border-color: var(--sa-warning);
}
.alert-danger {
border-color: var(--sa-danger);
}

100
docs/typescript-sdk.mdx Normal file
View file

@ -0,0 +1,100 @@
---
title: "TypeScript SDK"
description: "Generated types and a thin fetch-based client."
---
The TypeScript SDK is generated from the OpenAPI spec produced by the Rust server.
## Generate types
```bash
pnpm --filter @sandbox-agent/typescript-sdk generate
```
This runs:
- `cargo run -p sandbox-agent-openapi-gen` to emit OpenAPI JSON
- `openapi-typescript` to generate types
## Usage
```ts
import { SandboxDaemonClient } from "@sandbox-agent/typescript-sdk";
const client = new SandboxDaemonClient({
baseUrl: "http://127.0.0.1:8787",
token: process.env.SANDBOX_TOKEN,
});
await client.createSession("my-session", { agent: "claude" });
await client.postMessage("my-session", { message: "Hello" });
const events = await client.getEvents("my-session", { offset: 0, limit: 50 });
```
## Endpoint mapping
<details>
<summary><strong>client.listAgents()</strong></summary>
Maps to `GET /v1/agents`.
</details>
<details>
<summary><strong>client.installAgent(agentId, body)</strong></summary>
Maps to `POST /v1/agents/{agentId}/install`.
</details>
<details>
<summary><strong>client.getAgentModes(agentId)</strong></summary>
Maps to `GET /v1/agents/{agentId}/modes`.
</details>
<details>
<summary><strong>client.createSession(sessionId, body)</strong></summary>
Maps to `POST /v1/sessions/{sessionId}`.
</details>
<details>
<summary><strong>client.postMessage(sessionId, body)</strong></summary>
Maps to `POST /v1/sessions/{sessionId}/messages`.
</details>
<details>
<summary><strong>client.getEvents(sessionId, params)</strong></summary>
Maps to `GET /v1/sessions/{sessionId}/events`.
</details>
<details>
<summary><strong>client.getEventsSse(sessionId, params)</strong></summary>
Maps to `GET /v1/sessions/{sessionId}/events/sse` (raw SSE response).
</details>
<details>
<summary><strong>client.streamEvents(sessionId, params)</strong></summary>
Helper that parses SSE into `UniversalEvent` objects.
</details>
<details>
<summary><strong>client.replyQuestion(sessionId, questionId, body)</strong></summary>
Maps to `POST /v1/sessions/{sessionId}/questions/{questionId}/reply`.
</details>
<details>
<summary><strong>client.rejectQuestion(sessionId, questionId)</strong></summary>
Maps to `POST /v1/sessions/{sessionId}/questions/{questionId}/reject`.
</details>
<details>
<summary><strong>client.replyPermission(sessionId, permissionId, body)</strong></summary>
Maps to `POST /v1/sessions/{sessionId}/permissions/{permissionId}/reply`.
</details>

30
docs/universal-api.mdx Normal file
View file

@ -0,0 +1,30 @@
---
title: "Universal API"
description: "Feature checklist and normalization rules."
---
## Feature checklist
- [x] Session creation and lifecycle events
- [x] Message streaming (assistant and tool messages)
- [x] Tool call and tool result normalization
- [x] File and image parts
- [x] Human-in-the-loop questions
- [x] Permission prompts and replies
- [x] Plan approval normalization (Claude -> question)
- [x] Event streaming over SSE
- [ ] Persistent storage (out of scope)
## Normalization rules
- **Session ID** is always the client-provided ID.
- **Agent session ID** is surfaced in events but never replaces the primary session ID.
- **Tool calls** map to `UniversalMessagePart::ToolCall` and results to `ToolResult`.
- **File and image parts** map to `AttachmentSource` with `Path`, `Url`, or base64 `Data`.
## Agent mode vs permission mode
- **agentMode**: behavior or system prompt strategy (build/plan/custom).
- **permissionMode**: capability restrictions (default/plan/bypass).
These are separate concepts and must be configured independently.