npm package: @gizmo-ai/server
Description: SSE server for Gizmo runtime - stream state updates to thin clients
Latest version: 0.2.2

# @gizmo-ai/server

SSE server for Gizmo runtime - stream state updates to thin clients.

## Installation

```bash
bun add @gizmo-ai/server
```

## Quick Start

```typescript
import { createRuntime } from "@gizmo-ai/runtime";
import { agentPlugin } from "@gizmo-ai/agent-plugin";
import { anthropic } from "@gizmo-ai/model-providers";
import { createServer, createServerPlugin } from "@gizmo-ai/server";

// 1. Create server plugin (broadcasts state changes)
const serverPlugin = createServerPlugin({
  slices: ["execution", "agent"], // Which state slices to stream
});

// 2. Create runtime with both plugins
const runtime = createRuntime({
  plugins: [
    agentPlugin({ model: anthropic({ model: "claude-sonnet-4-20250514" }) }),
    serverPlugin.plugin,
  ],
});

// 3. Create and start server
const server = createServer({
  runtime,
  plugin: serverPlugin,
});

server.listen(3000);
```

## Thin Client Architecture

The server exposes the runtime over HTTP/SSE. Clients are **thin views** that:

1. **Read state** via SSE subscriptions and REST endpoints
2. **Dispatch commands** via POST endpoints (invoke, approve, cancel)

```
┌─────────────────────────────────────────────────────────────┐
│  Runtime (source of truth)                                  │
│  ├── State (execution, agent, hitl, ...)                    │
│  ├── Action dispatch                                        │
│  └── Plugin middleware                                      │
└──────────────────────────┬──────────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────────┐
│  Server (HTTP/SSE interface)                                │
│  ├── GET  /events        → Subscribe to state + actions     │
│  ├── GET  /state         → Read current state               │
│  ├── POST /invoke        → Dispatch user input              │
│  ├── POST /approvals/:id → Approve/reject HITL requests     │
│  └── POST /runs/:id/cancel → Cancel execution               │
└──────────────────────────┬──────────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────────┐
│  Thin Clients (views only - no business logic)              │
│  ├── CLI (Ink)                                              │
│  ├── Web (React)                                            │
│  ├── Mobile                                                 │
│  ├── Slack bot                                              │
│  └── Another agent                                          │
└─────────────────────────────────────────────────────────────┘
```

The server owns all logic. Clients just render state and send commands.

## Endpoints

### Discovery

| Method | Path | Description |
|--------|------|-------------|
| GET | `/.well-known/manifest.json` | Agent manifest with state schema |
| GET | `/contracts/actions.public.json` | Public action contracts from plugins |
| GET | `/health` | Health check |

### State & Streaming

| Method | Path | Description |
|--------|------|-------------|
| GET | `/events` | SSE combined stream (state + actions) |
| GET | `/stream/state` | SSE state updates only |
| GET | `/stream/actions` | SSE action events only |
| GET | `/state` | Full state snapshot |
| GET | `/state/:slice` | Specific state slice |

### Execution

| Method | Path | Description |
|--------|------|-------------|
| POST | `/invoke` | Invoke agent: `{ "input": "prompt" }` |
| POST | `/abort` | Abort current execution |
| POST | `/dispatch` | Dispatch action: `{ "action": { "type": "..." } }` |

### Run History & Control

| Method | Path | Description |
|--------|------|-------------|
| GET | `/runs` | List recent run history |
| GET | `/runs/:id` | Get specific run details with actions |
| POST | `/runs/:id/cancel` | Cancel a running execution |
| POST | `/runs/:id/continue` | Continue after intervention (e.g., HITL) |

### HITL Approvals

When using `@gizmo-ai/hitl-plugin`, mount the approval routes:

```typescript
server.app.route("/approvals", hitl.createRoutes());
```

| Method | Path | Description |
|--------|------|-------------|
| GET | `/approvals` | List pending approval requests |
| GET | `/approvals/:id` | Get approval details |
| POST | `/approvals/:id` | Approve or reject: `{ "decision": "approve" }` |
| GET | `/approvals/history` | Recent approval decisions |

## SSE Events

The `/events` endpoint streams these event types:

```typescript
// Initial state snapshot on connect
{ type: "state.initial", slices: {...}, timestamp: number }

// State slice updates (with JSON Patch)
{ type: "state.update", slice: "agent", patches: [...], value: {...}, timestamp: number }

// Execution lifecycle
{ type: "execution.status", status: "started" | "completed" | "failed" | "aborted", executionId: string }

// Keep-alive
{ type: "heartbeat", timestamp: number }
```

## Configuration

### createServerPlugin(config)

```typescript
const serverPlugin = createServerPlugin({
  slices: ["execution", "agent"], // Optional: limit which slices to broadcast
  historySize: 10,                // Optional: number of runs to keep in memory (default: 10)
  persistence: {                  // Optional: enable JSONL persistence
    enabled: true,
    baseDir: ".gizmo",            // Directory for runs.jsonl, state.jsonl, logs/
  },
});
```

### createServer(config)

```typescript
import { RuntimeContract } from "@gizmo-ai/runtime";
import { AgentContract } from "@gizmo-ai/agent-plugin";
import { HITLContract } from "@gizmo-ai/hitl-plugin";

const server = createServer({
  runtime,                    // Required: Gizmo runtime instance
  plugin: serverPlugin,       // Required: Server plugin from createServerPlugin()
  heartbeatInterval: 30000,   // Optional: Heartbeat interval in ms (default: 30000)
  basePath: "/api",           // Optional: Base path for all routes (default: "")
  serializer: customFn,       // Optional: Custom JSON serializer
  contracts: [                // Optional: Action contracts for /contracts endpoint
    RuntimeContract,
    AgentContract,
    HITLContract,
  ],
  manifest: {                 // Optional: Agent identity for manifest
    identity: {
      agentId: "my-agent-v1",
      name: "My Agent",
      version: "1.0.0",
    },
  },
});
```

## Client Example

```typescript
// Connect to SSE stream
const events = new EventSource("http://localhost:3000/events");

events.addEventListener("state.initial", (e) => {
  const { slices } = JSON.parse(e.data);
  console.log("Initial state:", slices);
});

events.addEventListener("state.update", (e) => {
  const { slice, patches, value } = JSON.parse(e.data);
  console.log(`${slice} updated:`, patches || value);
});

events.addEventListener("execution.status", (e) => {
  const { status, executionId } = JSON.parse(e.data);
  console.log(`Execution ${executionId}: ${status}`);
});

// Invoke agent
await fetch("http://localhost:3000/invoke", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ input: "Hello!" }),
});

// Approve HITL request
await fetch("http://localhost:3000/approvals/abc-123", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ decision: "approve" }),
});

// Cancel execution
await fetch("http://localhost:3000/runs/exec-456/cancel", {
  method: "POST",
});
```

## Agent Manifest

The manifest endpoint (`/.well-known/manifest.json`) provides self-description:

```json
{
  "manifestVersion": "1.0.0",
  "identity": {
    "agentId": "my-agent-v1",
    "name": "My Agent",
    "version": "1.0.0"
  },
  "endpoints": {
    "invoke": "/invoke",
    "state": "/state",
    "runs": "/runs",
    "streams": {
      "state": "/stream/state",
      "actions": "/stream/actions",
      "events": "/events"
    },
    "control": {
      "cancel": "/runs/:id/cancel",
      "continue": "/runs/:id/continue"
    }
  },
  "state": {
    "slices": ["execution", "agent"],
    "schemaVersion": "1.0.0",
    "schema": { ... }
  },
  "actions": {
    "publicContractsRef": "/contracts/actions.public.json"
  },
  "capabilities": {
    "tools": ["read", "write", "bash"],
    "features": ["streaming", "history"]
  }
}
```

## Action Contracts

The `/contracts/actions.public.json` endpoint returns public action definitions from all plugins:

```json
{
  "schemaVersion": "1.0.0",
  "contracts": [
    {
      "pluginKey": "hitl",
      "stateKey": "hitl",
      "version": "0.1.0",
      "emits": [
        {
          "type": "HITL_APPROVAL_REQUESTED",
          "description": "Tool call blocked - awaiting human decision",
          "visibility": "public",
          "pattern": "request"
        }
      ],
      "listensTo": [{ "type": "AGENT_TOOL_CALL_REQUESTED" }],
      "accepts": ["HITL_APPROVAL_GRANTED", "HITL_APPROVAL_REJECTED"]
    }
  ]
}
```

Use this to build clients that understand what actions the server emits and accepts.

## Persistence

When persistence is enabled, the server stores:

```
.gizmo/
├── runs.jsonl      # Run metadata with tamper-evident digest
├── state.jsonl     # State snapshots at execution completion
└── logs/
    └── {executionId}.jsonl  # Per-run action logs
```

Each completed run includes a SHA-256 digest of the run bundle (actions + final state) for tamper evidence.

## Run History

Query run history with optional filters:

```bash
# List runs
curl "http://localhost:3000/runs?status=completed&limit=10"

# Get run details with all actions
curl http://localhost:3000/runs/exec-123

# Cancel a running execution
curl -X POST http://localhost:3000/runs/exec-123/cancel
```

Response includes:
- `executionId`, `startTime`, `endTime`, `status`
- `actionCount` - Number of actions in the run
- `digest` - SHA-256 hash for verification (when persistence enabled)
- `actions` - Full action log (in detail endpoint)

## Example

See [`examples/demo/`](../../examples/demo/) for a complete demo showcasing:
- Real-time state streaming with SSE
- Manifest discovery and state schema
- HITL approval workflow
- Run history and persistence
- Self-aware agent tools