Files
hub/docs/architecture/agent-sessions.md
glm-5.1 2b63cda1c7 Setup repo: migrate architecture specs, code stubs, and tasks from alkhub_ts
Copy architecture docs, ADRs, storage domain specs, research, reviews,
and 56 storage architecture tasks from the alkhub_ts monorepo. Adapt for
standalone @alkdev/hub repo structure (src/ not packages/hub/).

Sanitize all sensitive information:
- Replace private IPs (10.0.0.1) with localhost defaults
- Remove internal server hostnames (dev1, ns528096)
- Replace /workspace/ private paths with npm package references
- Remove hardcoded credentials from examples
- Rewrite infrastructure.md without private network details

Add Deno project scaffolding: deno.json (pinned deps), .gitignore,
AGENTS.md, entry point. Migrate existing code stubs (crypto, config
types, logger) with updated import paths.
2026-05-25 10:56:32 +00:00

10 KiB

status, last_updated
status last_updated
draft 2026-04-16

Agent Sessions

Overview

The hub owns all agent sessions and messages. Every session — whether the LLM runs directly in the hub or in a remote opencode container — stores its data in the hub's Postgres. The hub is the source of truth; runners are execution environments.

Two execution paths, one storage model:

Path Where the LLM runs Session ownership Tool execution
Direct Hub process (AI SDK) Hub Postgres Hub operations registry
Runner Remote opencode container (spoke) Hub Postgres Opencode's built-in tools + hub MCP ops

Both paths produce UIMessage format. Both store in the same tables. Same session model, same message parts — just different execution environments.

Hub OpenAI Proxy

The hub runs an OpenAI-compatible proxy endpoint. No provider API keys leak to runners.

Runner(s) ──→ Hub proxy (/v1/chat/completions) ──→ Provider APIs
                    │
                    └── Key management, rate limiting, logging

All LLM calls — whether from direct agents in the hub or from opencode containers — go through this proxy. This means:

  • Provider keys stay on the hub
  • All LLM usage is observable and loggable (logtape drizzle adapter for query-level logging)
  • Rate limiting and routing happen in one place
  • Runners never need provider credentials

Built with Hono — an OpenAI-compatible proxy is straightforward: receive request, add API key from server-side config, forward to provider, stream response back.

Direct Agents

Agents that don't need opencode's dev tools run directly in the hub:

Role Tools Why no opencode
Architect read, write, webSearch No file editing needed
Decomposer read, taskgraph No bash needed
Code Reviewer read, grep, bash (read-only) Read-only access
Architecture Reviewer read Read-only access
Research Specialist webSearch, read No dev tools needed, processes external data (low trust, see agent-roles.md)

Implementation: AI SDK streamText / generateText with operations converted to AI SDK tools:

streamText({
  model: proxyProvider('anthropic/claude-opus-4-5-20251101'),
  messages: loadedFromPostgres,
  tools: operationRegistryToTools(registry, context),
  onFinish: ({ messages }) => saveToPostgres(sessionId, messages),
})

Operations → AI SDK tool mapping is direct because both use JSON Schema (TypeBox produces JSON Schema):

import { tool } from "ai";

function operationToTool(spec: OperationSpec) {
  return tool({
    description: spec.description,
    parameters: spec.inputSchema,
    execute: async (input) => registry.execute(`${spec.namespace}.${spec.name}`, input, context),
  });
}

Runner Agents (Opencode-backed)

Agents that need file editing, bash execution, and other dev tools run in opencode containers. Each container is a runner spoke connected to the hub.

Hub                                    Runner (opencode container)
  │                                        │
  │── WebSocket ──────────────────────────→│  (runner spoke connection)
  │                                        │
  │── hub.register ───────────────────────→│  (registers dev.* operations)
  │                                        │
  │── OpenAI proxy ◄── LLM calls ─────────│  (opencode calls hub for LLM)
  │                                        │
  │── hub.call/coord.* ◄── coord calls ──│  (opencode calls hub for coordination)
  │                                        │
  │── hub.search/schema ◄── MCP ──────────│  (discover hub ops via MCP endpoint)
  │                                        │
  │── hub.call/opencode.* ────────────────→│  (hub calls ops on the runner)
  │   └── opencode.sessionPromptAsync etc. │
  │                                        │
  └── Postgres                             │
      └── session writes via hub ops       │  (hub persists, runner is stateless)

The opencode instance uses:

  1. Hub's OpenAI proxy for LLM calls (never talks to providers directly)
  2. Hub's MCP endpoint for coordination ops (search/schema/call pattern)
  3. Hub's call protocol for session persistence — the runner calls hub operations that write to Postgres. The runner itself has no Postgres connection.

AI SDK provider for opencode (ai-sdk-provider-opencode-sdk, MIT): An AI SDK v3 provider that wraps opencode's SDK, making opencode look like a standard AI SDK language model. ~6000 lines of source, ~4600 lines of tests.

This is optional infrastructure, not a required dependency. Two ways to interact with opencode:

  1. Operation registry (from_openapi): Import opencode's OpenAPI spec via from_openapi.ts. This generates typed operations (opencode.sessionCreate, opencode.sessionPromptAsync, etc.) that go through the call protocol. No additional dependency needed — the SSE handler fix in from_openapi.ts (converting SSE streams to async generators) makes this work for the streaming endpoints.

  2. AI SDK provider: Use createOpencode({ baseUrl }) to treat an opencode instance as an AI SDK language model. This is useful when the hub wants to programmatically drive an opencode session as if it were just another model call: streamText({ model: opencodeProvider('model') }).

Both paths write to the same Postgres tables. The operation registry path is the default — it's already in our toolkit and needs no new dependencies. The provider path is available for cases where you want tighter AI SDK integration.

Reference: ai-sdk-provider-opencode-sdk (npm package)

Note: The provider is a convenience, not a requirement. The hub can also interact with opencode containers via the operation registry (FromOpenAPI generates typed operations from opencode's REST spec) or via the call protocol over WebSocket. The provider is useful when you want the hub to treat a runner as an AI SDK model.

Session Model

Session (maps to sessions table)

type Session = {
  id: string;
  accountId: string;   // FK → accounts.id — the account that owns this session
  projectId: string;
  title: string;
  status: "idle" | "busy" | "retry" | "archived";
  roleName?: string; // which behavioral role (e.g., "architect", "implementation-specialist"). Maps from OpenCode's "agent" field. See ADR-012.
  parentId?: string; // for spawned sessions (coordinator relationship)
  provider?: string; // "direct" or "opencode" — which execution path
  createdAt: Date;
  updatedAt: Date;
};

Message (maps to messages table)

Message metadata is stored separately from part content. This follows the opencode pattern and enables streaming part updates, independent part queries, and SSE events for message.part.updated.

type Message = {
  id: string;
  sessionId: string;
  role: "user" | "assistant" | "system";
  // role-specific metadata in data column:
  // user: { format, summary, tools, model }
  // assistant: { model, provider, tokens, cost, finish, parentID }
  data: Record<string, unknown>;
  createdAt: Date;
  updatedAt: Date;
};

Part (maps to parts table)

Each message has multiple parts, stored in a separate table with their own IDs and timestamps. This is the same pattern opencode uses — it enables SSE streaming of individual part updates and querying parts independently.

type Part = {
  id: string;
  messageId: string;
  sessionId: string;
  type: "text" | "tool" | "reasoning" | "file" | "step-start" | "step-finish" | "snapshot" | "patch";
  // type-specific content in data column
  data: Record<string, unknown>;
  createdAt: Date;
  updatedAt: Date;
};

Part types and their data shapes are modeled after opencode's MessageV2.Part discriminated union (reference: opencode's message-v2 schema). Our part types will be a subset — we add the ones we need as we implement features.

AI SDK Compatibility

The AI SDK expects UIMessage format (role + parts array). Our API assembles messages + parts into UIMessage for consumption. Storage is normalized; the API presents the denormalized view. No format conversion needed — just a JOIN query.

No format conversion regardless of execution path. Direct agents and opencode runners both produce UIMessage. This is why importing opencode sessions works — same format, same tables, just potentially with additional opencode-specific tool parts.

Schema Research Needed

The message/part schema needs more iteration. Opencode's drizzle+sqlite schema (npm package) uses a message tree format with parent/child parts that we should reference. The AI SDK UIMessage part types and opencode's part types need to be reconciled. See storage/sessions.md for the session/message/part table schemas.

Per-Client Event Filtering

Clients subscribe to project/session-scoped events via Redis:

alk:events:session.status:{projectId}    — session status changes
alk:events:message.updated:{sessionId}   — message part updates
alk:events:runner.dispatch:{runnerId}    — spoke dispatch

No firehose. See pubsub-redis.md for the channel naming convention.

What This Replaces

Previous Now
Opencode's Effect SessionProcessor AI SDK streamText / generateText
Per-container MCP servers (websearch, etc.) Hub MCP endpoint + shared hub operations
Provider keys in each container Hub OpenAI proxy — one place for keys
In-memory session state Postgres — any process can serve any session
Single-process messaging Redis pub/sub for cross-process events

Reference Dependencies

Package Path Notes
ai-sdk-provider-opencode-sdk ai-sdk-provider-opencode-sdk (npm package) AI SDK v3 provider wrapping opencode SDK. ~6000 lines src, ~4600 tests. MIT.
AI SDK AI SDK (npm package) Core SDK. See AGENTS.md for version.
opencode opencode (application, not a dependency) Has drizzle+sqlite message schema for reference. MIT.