@ellie-ai/server
v0.2.0
Published
SSE server for Ellie runtime - stream state updates to thin clients
Downloads
298
Readme
@ellie-ai/server
SSE server for Ellie runtime - stream state updates to thin clients.
Installation
bun add @ellie-ai/serverQuick Start
import { createRuntime } from "@ellie-ai/runtime";
import { agentPlugin } from "@ellie-ai/agent-plugin";
import { anthropic } from "@ellie-ai/model-providers";
import { createServer, createServerPlugin } from "@ellie-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:
- Read state via SSE subscriptions and REST endpoints
- 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/eli-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 @ellie-ai/hitl-plugin, mount the approval routes:
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:
// 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)
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: ".ellie", // Directory for runs.jsonl, state.jsonl, logs/
},
});createServer(config)
import { RuntimeContract } from "@ellie-ai/runtime";
import { AgentContract } from "@ellie-ai/agent-plugin";
import { HITLContract } from "@ellie-ai/hitl-plugin";
const server = createServer({
runtime, // Required: Ellie 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
// 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/eli-manifest.json) provides self-description:
{
"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:
{
"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:
.ellie/
├── runs.jsonl # Run metadata with tamper-evident digest
├── state.jsonl # State snapshots at execution completion
└── logs/
└── {executionId}.jsonl # Per-run action logsEach 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:
# 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/cancelResponse includes:
executionId,startTime,endTime,statusactionCount- Number of actions in the rundigest- SHA-256 hash for verification (when persistence enabled)actions- Full action log (in detail endpoint)
Example
See 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
