home / skills / hhopkins95 / ai-systems / backend-setup

This skill helps you bootstrap a robust backend runtime for agent services by configuring persistence, REST/WebSocket servers, and Modal integration.

npx playbooks add skill hhopkins95/ai-systems --skill backend-setup

Review the files below or copy the command above to add this skill to your agents.

Files (2)
SKILL.md
6.6 KB
---
name: Backend Setup
description: This skill should be used when the user asks to "create an agent runtime server", "set up agent runtime backend", "configure Modal sandbox", "implement PersistenceAdapter", "start WebSocket server", "create REST API for agents", or needs to build a Node.js backend using @hhopkins/agent-runtime.
---

# Backend Setup

## Overview

Setting up an agent runtime backend involves:
1. Configuring environment variables
2. Implementing a PersistenceAdapter
3. Creating the runtime with configuration
4. Starting REST and WebSocket servers

## Environment Variables

Required environment variables:

```bash
# Modal credentials (for sandbox creation)
MODAL_TOKEN_ID=your_modal_token_id
MODAL_TOKEN_SECRET=your_modal_token_secret

# Anthropic API key (for Claude agents)
ANTHROPIC_API_KEY=your_anthropic_api_key
```

Obtain Modal credentials from [modal.com](https://modal.com).

## Minimal Server Example

```typescript
import { createServer } from "http";
import { createAgentRuntime, type PersistenceAdapter } from "@hhopkins/agent-runtime";

// 1. Implement PersistenceAdapter (see references/types.md for full interface)
const persistence: PersistenceAdapter = {
  // Session operations
  listAllSessions: async () => [],
  loadSession: async (sessionId) => null,
  createSessionRecord: async (session) => {},
  updateSessionRecord: async (sessionId, updates) => {},

  // Storage operations
  saveTranscript: async (sessionId, rawTranscript) => {},
  saveWorkspaceFile: async (sessionId, file) => {},
  deleteSessionFile: async (sessionId, path) => {},

  // Agent profile operations
  listAgentProfiles: async () => [{ id: "default", name: "Default Agent" }],
  loadAgentProfile: async (agentProfileId) => ({
    id: "default",
    name: "Default Agent",
    systemPrompt: "You are a helpful assistant.",
    tools: ["Read", "Write", "Edit", "Bash"],
  }),
};

async function main() {
  // 2. Create runtime
  const runtime = await createAgentRuntime({
    persistence,
    modal: {
      tokenId: process.env.MODAL_TOKEN_ID!,
      tokenSecret: process.env.MODAL_TOKEN_SECRET!,
      appName: "my-agent-app",
    },
    idleTimeoutMs: 15 * 60 * 1000, // 15 minutes
    syncIntervalMs: 30 * 1000,      // 30 seconds
  });

  // 3. Start runtime (loads sessions, starts background jobs)
  await runtime.start();

  // 4. Create REST API server
  const restApp = runtime.createRestServer({
    apiKey: "your-api-key",
  });

  // 5. Create HTTP server and attach REST routes
  const httpServer = createServer(async (req, res) => {
    const response = await restApp.fetch(
      new Request(`http://${req.headers.host}${req.url}`, {
        method: req.method,
        headers: req.headers as any,
        body: req.method !== "GET" && req.method !== "HEAD"
          ? await getRequestBody(req)
          : undefined,
      })
    );

    res.statusCode = response.status;
    response.headers.forEach((value, key) => res.setHeader(key, value));
    res.end(await response.text());
  });

  // 6. Create WebSocket server on same HTTP server
  const wsServer = runtime.createWebSocketServer(httpServer);

  // 7. Start listening
  httpServer.listen(3001, () => {
    console.log("Server running on http://localhost:3001");
  });

  // 8. Graceful shutdown
  process.on("SIGTERM", async () => {
    httpServer.close();
    wsServer.close();
    await runtime.shutdown();
    process.exit(0);
  });
}

function getRequestBody(req: any): Promise<string> {
  return new Promise((resolve, reject) => {
    let body = "";
    req.on("data", (chunk: any) => body += chunk.toString());
    req.on("end", () => resolve(body));
    req.on("error", reject);
  });
}

main();
```

## Runtime Configuration

```typescript
const runtime = await createAgentRuntime({
  // Required: persistence adapter implementation
  persistence: PersistenceAdapter,

  // Required: Modal credentials
  modal: {
    tokenId: string,
    tokenSecret: string,
    appName: string,  // Modal app name for sandboxes
  },

  // Optional: idle timeout before session cleanup (default: 15 min)
  idleTimeoutMs: number,

  // Optional: sync interval for persisting state (default: 30 sec)
  syncIntervalMs: number,
});
```

## REST API Endpoints

The runtime creates these REST endpoints:

| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/sessions/create` | Create new session |
| GET | `/sessions/:id` | Get session data |
| POST | `/sessions/:id/message` | Send message to agent |
| GET | `/sessions` | List all sessions |
| GET | `/agent-profiles` | List available agent profiles |
| GET | `/health` | Health check |

## Lazy Sandbox Pattern

Sandboxes are created lazily - not when a session is created, but when the first message is sent. This optimizes resource usage:

1. `POST /sessions/create` - Creates session record, no sandbox yet
2. `POST /sessions/:id/message` - First message triggers sandbox creation
3. Subsequent messages reuse the running sandbox
4. Idle timeout eventually terminates the sandbox

## SessionManager Access

Access the session manager for advanced operations:

```typescript
// Get all loaded sessions
const sessions = runtime.sessionManager.getLoadedSessions();

// Get specific session
const session = runtime.sessionManager.getSession(sessionId);

// Unload a session (terminates sandbox, syncs state)
await runtime.sessionManager.unloadSession(sessionId);

// Get session state
const state = session.getState();
```

## WebSocket Events

The WebSocket server emits these events to connected clients:

**Block streaming:**
- `session:block:start` - New block begins
- `session:block:delta` - Incremental text update
- `session:block:update` - Block metadata changes
- `session:block:complete` - Block finishes

**Session lifecycle:**
- `session:status` - Runtime state changes
- `session:metadata:update` - Token/cost updates

**Files:**
- `session:file:created` - New file in workspace
- `session:file:modified` - File changed
- `session:file:deleted` - File removed

**Subagents:**
- `session:subagent:discovered` - New subagent started
- `session:subagent:completed` - Subagent finished

**Errors:**
- `error` - Error occurred

## PersistenceAdapter

The PersistenceAdapter is the main integration point. Implement this interface to connect the runtime to your storage layer. See `references/types.md` for the full interface.

Common implementations:
- **In-memory** - For development/testing
- **SQLite** - For single-server deployments
- **PostgreSQL/MySQL** - For production
- **Convex/Supabase** - For serverless

## Related Skills

- **overview** - Understanding the runtime architecture
- **react-integration** - Building React frontends
- **agent-design** - Configuring agent profiles

Overview

This skill sets up a Node.js backend that runs agent runtimes using @hhopkins/agent-runtime. It covers environment configuration, implementing a PersistenceAdapter, creating the runtime, and exposing REST and WebSocket endpoints. Use it to host agent sandboxes, manage sessions, and stream agent outputs to clients.

How this skill works

You implement a PersistenceAdapter to connect sessions, transcripts, files, and agent profiles to your storage layer. The skill shows how to create and start the agent runtime with Modal credentials and optional timeouts, then expose a REST API and attach a WebSocket server to the same HTTP server. Sandboxes are created lazily on first message, and the runtime manages session lifecycle, syncing, and graceful shutdown.

When to use it

  • You need a production-ready backend to host conversational agents with session persistence.
  • You want a simple Node.js/TypeScript reference for integrating @hhopkins/agent-runtime.
  • You must implement storage integration (in-memory, SQLite, Postgres, Supabase, etc.).
  • You need REST endpoints for session lifecycle and WebSocket streaming for real-time updates.
  • You plan to use Modal sandboxes and require Modal credential configuration.

Best practices

  • Implement PersistenceAdapter to persist sessions, transcripts, workspace files, and agent profiles before production use.
  • Use lazy sandbox creation: create session records on create, only start sandboxes on first message to save resources.
  • Set sensible idleTimeoutMs and syncIntervalMs to balance resource usage and state durability.
  • Protect REST endpoints with an apiKey or other authentication and validate incoming requests.
  • Gracefully handle SIGTERM to close HTTP, WebSocket servers and shutdown the runtime to avoid state loss.

Example use cases

  • Small dev server with in-memory PersistenceAdapter for local testing and rapid iteration.
  • Single-server production using SQLite or Postgres PersistenceAdapter for durable sessions and files.
  • Serverless or managed DB backends using Supabase/Convex adapters with the same runtime API.
  • Real-time web client that consumes WebSocket block streaming events for UI updates.
  • Automated agents that spawn subagents; use sessionManager to inspect, unload, or retrieve session state.

FAQ

What environment variables are required?

Provide Modal credentials (MODAL_TOKEN_ID and MODAL_TOKEN_SECRET) and any AI provider keys such as ANTHROPIC_API_KEY when using Claude.

When is a sandbox created?

Sandboxes are created lazily: creating a session record does not start a sandbox. The first message to the session triggers sandbox creation.

How do I persist state reliably?

Implement a PersistenceAdapter backed by a durable store (Postgres, SQLite, Supabase). Configure syncIntervalMs and call runtime.sessionManager.unloadSession to ensure final sync before shutdown.