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

213 lines
10 KiB
Markdown

---
status: draft
last_updated: 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](./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):
```ts
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)
```ts
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`.
```ts
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.
```ts
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. |