Skip to content

Architecture ​

kayak-lab is built as a three-layer architecture: Core, Capabilities, and Projections.

Core Layer ​

The foundation of the platform. Handles event storage, session lifecycle, and agent execution.

EventStream ​

Immutable, append-only sequence of events. Events are strictly ordered by sequence number within a session. Different sessions are completely isolated.

typescript
const stream = new EventStream();

// Append an event
const event = stream.append({
  session_id: "abc-123",
  event_type: "session.created",
  payload: { state: "active" },
  metadata: { source: "test" },
});

// Read events for a session
const events = stream.getEvents("abc-123");

SessionManager ​

Manages session lifecycle with a state machine. All transitions emit typed events.

typescript
const manager = new SessionManager(eventStream);

// Create a session
const session = await manager.createSession({ description: "My task" });

// Pause and resume
manager.pauseSession(session.id);
manager.resumeSession(session.id);

// Complete
manager.completeSession(session.id);

AgentRuntime ​

The agent execution loop: input → model → tool cycle. Manages context windows and tool invocations.

typescript
const runtime = new AgentRuntime(eventStream, sessionManager, modelManager, toolRegistry);

// Or with the new structured tool calling protocol
const runtime = new AgentRuntime(eventStream, sessionManager, modelManager, legacyToolRegistry, newToolRegistry);

// Start and process input
await runtime.start();
const response = await runtime.processInput("Run ls -la");

Dual-protocol dispatch: AgentRuntime checks newToolRegistry first for structured tools (JSON Schema validated), then falls back to the legacy toolRegistry. This enables incremental migration from legacy tools to the structured protocol.

Tool Calling Module ​

Structured tool execution protocol with JSON Schema validation, tool registry, authoring, and self-improvement.

The module lives in src/tools/ and provides:

ComponentFilePurpose
ToolDefinitiontool-definition.tsJSON Schema parameter validation for tool invocations
ToolCallingEnginecalling-engine.tsExecutes tools with structured input/output and timeout handling
ToolRegistryregistry.tsEnable/disable lifecycle, discovery, and lookup of tools
ToolAuthoringauthoring.tsProposal/review/accept/reject flow for creating new tools
ToolSelfImprovementself-improvement.tsUsage tracking, suggestion generation, and auto-improvement
Typestypes.tsShared interfaces (ToolHandlerContext, ToolDefinition, etc.)
typescript
import { ToolCallingEngine, ToolRegistry } from "./src/tools/mod.ts";

// Register tools with definitions
const registry = new ToolRegistry();
registry.enable("shell", { description: "Execute shell commands", parameters: { /* JSON Schema */ } });

// Execute a tool call
const engine = new ToolCallingEngine(registry);
const result = await engine.invoke({
  tool_name: "shell",
  parameters: { command: "ls -la" },
  tool_call_id: "call-123",
});

Tool authoring flow: Propose → Review → Accept/Reject → Register. Tools go through a review lifecycle before being added to the registry. Self-improvement tracks usage patterns and suggests optimizations.

MCP Module ​

Model Context Protocol (MCP) integration for connecting to external MCP servers and exposing harness capabilities as an MCP server. The module provides client, server, registry, and search components with a transport abstraction layer.

ComponentFilePurpose
MCPClientclient.tsConnects to external MCP servers, discovers tools, invokes them
MCPServerserver.tsExposes harness tools (exposable only) to external MCP clients
MCPRegistryregistry.tsManages MCP tools from external servers with state control
MCPSearchsearch.tsSearches MCP tools by name, capability, or category
Transporttransport.tsStdio, HTTP, WebSocket transports with factory function
Eventsevents.tsMCP event types and type guards
Event Wiringevent-emitter.tsWires MCP events to the event stream
typescript
import { MCPClient, MCPServer, MCPRegistry, MCPSearch } from "./src/mcp/mod.ts";

// Connect to an external MCP server
const client = new MCPClient({
  name: "my-mcp-server",
  transport: { type: "stdio", command: "mcp-server", args: ["--port", "3000"] },
});
await client.connect();
const tools = await client.discover();

// Register discovered tools
const registry = new MCPRegistry();
for (const tool of tools) {
  registry.register({ tool, serverName: "my-mcp-server", enabled: true, capabilities: [] });
}

// Search for tools
const search = new MCPSearch(registry, new Map([["my-mcp-server", client]]));
const results = search.search({ name: "file" });

// Expose harness tools as MCP server
const server = new MCPServer({
  transport: { type: "http", url: "http://localhost:8080" },
  toolRegistry: {
    getExposableTools: () => toolRegistry.list().filter((t) => t.exposable),
    getTool: (name) => toolRegistry.get(name),
    invokeTool: (name, params) => toolRegistry.invoke(name, params),
  },
});
await server.start();

Transport abstraction: The createTransport factory creates the appropriate transport based on configuration. Each transport implements IMCPTransport with connect, disconnect, send, and notify methods.

Event integration: MCP operations emit events to the event stream via wireClientEvents, wireServerEvents, wireRegistryEvents, and wireSearchEvents helpers. Events include mcp.connected, mcp.disconnected, mcp.tool.invocation, mcp.tool.result, mcp.server.started, mcp.server.stopped, and mcp.error.

Memory ​

Persistent memory subsystem for agent learning and context retention. Stores and retrieves memories across sessions, enabling agents to accumulate knowledge over time.

Memory types:

TypePurposeRetention
EpisodicEvent logs — what happened in a sessionShort-term, event-sourced
SemanticLearned facts — knowledge acquired over timeLong-term, persistent
ProceduralLearned patterns — how to do thingsLong-term, persistent
WorkingActive context — current session focusSession-scoped, volatile

Key modules:

ComponentFilePurpose
MemoryProviderprovider.tsAbstraction layer over storage backends
MemoryStoragestorage.tsBackend storage interface
MemoryRetrievalretrieval.tsQuery and ranking of stored memories
MemorySearchsearch.tsFull-text search across memory stores
SharedMemoryshared.tsSub-agent context snapshots
MemoryConfigconfig.tsMemory configuration and backend selection
MemoryUpdateupdate.tsAtomic state transitions with event sourcing
MemoryEmitteremitter.tsMemory event emission for observability
typescript
import { MemoryProvider } from "./src/memory/provider.ts";

const memory = new MemoryProvider(config);
await memory.initialize();

// Store episodic memory
await memory.store({ type: "episodic", content: "User prefers dark mode" });

// Retrieve relevant memories
const results = await memory.search({ query: "user preferences", type: "semantic" });

Provider abstraction: The MemoryProvider decouples storage logic from consumers. Swap between in-memory, file-based, or database-backed storage without changing application code. SharedMemory enables sub-agents to share context snapshots without coupling to parent state.

Provenance Module ​

Provenance-aware context management for intelligent token reduction. Extends ContextManager with provenance-weighted pruning, tool result compression, and hook-driven context assembly.

ComponentFilePurpose
ProvenanceContextManagerprovenance-context.tsProvenance-weighted pruning and context assembly
MessageClassifiermessage-classifier.tsPure message classification and scoring
Typesprovenance-context-types.tsEnums, config, and defaults
MemoryRetrievalretrieval.tsOn-demand retrieval with provenance scoring
typescript
import { ProvenanceContextManager } from "./src/memory/provenance-context.ts";

const ctx = new ProvenanceContextManager({ maxTokens: 8000 });
ctx.setProvenanceGraph(graph); // ProvenanceGraphData from provenance module

// Provenance-weighted pruning happens automatically on add()
ctx.add(message);

// Tool result compression — references only lines used downstream
const compressed = ctx.compressToolResult(result, toolCallId, eventId);

// Hook-driven context assembly
const hook = ctx.createBeforeModelCallHook();

Token reduction: When a provenance graph is available, messages are scored by role priority and provenance outcome (success/failure/dead-end). The lowest-scoring messages are pruned first, reducing token usage by 50–80% while preserving goal, commitment, and verification context. Falls back to positional pruning when the graph is unavailable or has fewer than 3 nodes.

Tool result compression: Large tool outputs are compressed to only the lines referenced by downstream provenance nodes. The full output is preserved via an event ID reference for audit.

Context assembly: The createBeforeModelCallHook() returns a hook handler that assembles a token-budgeted context window from system prompt, goal, provenance summary, retrieved memories, and current input.

Message Classifier ​

Pure-function message classification and scoring for provenance-aware context management. No I/O, no side effects — all methods are pure.

typescript
import { MessageClassifier } from "./src/memory/message-classifier.ts";

const classifier = new MessageClassifier();
classifier.setProvenanceGraph(graph);

const score = classifier.scoreMessage(message); // base priority + outcome bonus
const priority = classifier.getBasePriority(message);

MessagePriority (base scoring by role/content heuristics):

PriorityValueHeuristic
SYSTEM1000System role messages
GOAL900User messages starting with goal:, task:, or request:
COMMITMENT800Tool results containing write/edit/create keywords
VERIFICATION700Tool results with exit codes, test output, pass/fail
EXPLORATION500Tool results from read-only operations
OTHER300Everything else

OutcomeScore (provenance graph bonus):

ScoreValueMeaning
SUCCESS200Linked to verification with exit code 0
FAILURE-100Linked to verification with non-zero exit code
DEAD_END-200No downstream commitments or verifications
NO_LINK0No provenance node linked, or graph unavailable

Memory Retrieval ​

On-demand memory retrieval with provenance scoring. Retrieves relevant memories from storage and ranks them by relevance.

typescript
import { MemoryRetrieval } from "./src/memory/retrieval.ts";

const retrieval = new MemoryRetrieval(storage, config);
const results = await retrieval.retrieve({
  query: "user preferences",
  max_results: 10,
  type: "semantic",
});
// Each result includes: memory, relevance_score, final_score

HookRegistry ​

Lifecycle hook system for AgentRuntime. Dispatches hooks at defined lifecycle points with timeout enforcement and error isolation.

typescript
import { hookRegistry, HookPoint } from "./src/runtime/hooks.ts";

// Register a global hook
const hookId = hookRegistry.register(HookPoint.BEFORE_MODEL_CALL, async (ctx) => {
  // Modify ctx.messages before model call
}, { timeoutMs: 3000 });

// Register a session-scoped hook
hookRegistry.register(HookPoint.AFTER_TOOL_EXECUTION, (ctx) => {
  console.log(`Tool ${ctx.toolName} completed: ${ctx.success}`);
}, { sessionId: "session-1" });

// Dispatch — called by AgentRuntime at each lifecycle point
await hookRegistry.dispatch(HookPoint.TURN_END, context, sessionId);

// Unregister
hookRegistry.unregister(hookId);

HookPoint enum:

Hook PointWhen FiredContext Type
BEFORE_MODEL_CALLBefore each LLM requestBeforeModelCallContext — messages, model, tools
AFTER_TOOL_EXECUTIONAfter each tool invocationAfterToolExecutionContext — tool name, params, result
TURN_ENDAfter each agent turnTurnEndContext — turn number, response, provenance nodes
SESSION_STARTWhen a session beginsSessionStartContext — session ID, config
SESSION_ENDWhen a session completesSessionEndContext — state, duration, attestation

Error isolation: Each hook is executed with a timeout (default 5s). If a hook throws or times out, the error is logged and execution continues to the next hook. Hooks never propagate failures to the caller.

Singleton: hookRegistry is the global instance. Session-scoped hooks are filtered by sessionId during dispatch.

AttestationService ​

Aggregates model usage events and emits session.attestation events on session completion. Tracks costs, token usage, and provenance summaries.

typescript
import { AttestationService } from "./src/session/attestation-service.ts";

const attestation = new AttestationService(eventStream, eventStore);

// Load pricing for cost calculation
attestation.loadPricing([
  { provider: "openai", model_name: "gpt-4", input_price_per_token: 0.00003, output_price_per_token: 0.00006, cache_read_price_per_token: 0.000015, cache_write_price_per_token: 0.00003 },
]);

// Create attestation for a completed session
const report = await attestation.createAttestation("session-1");
// report.total_cost_usd, report.models, report.provenance_summary

// Query attestations
const attestations = await attestation.getAttestations({
  sortBy: "cost",
  sortOrder: "desc",
  limit: 10,
});

ModelMetrics per model: input/output tokens, cache read/write tokens, estimated cost in USD.

ProvenanceSummary: total goals, explorations, commitments, verifications, and exploration-to-commitment ratio.

Causal Graph ​

Utility functions for building and traversing event causality graphs. Used by EventStore, PersistentEventStore, and the provenance system.

typescript
import { buildCausalGraph, findDownstream, findIndependentChains } from "./src/store/causal-graph.ts";

// Build adjacency list from events using causal_parents fields
const graph = buildCausalGraph(events); // Map<string, CausalGraphNode>

// Find all events downstream of a given event (transitive closure, BFS)
const downstream = findDownstream(eventId, events); // BaseEvent[]

// Find independent (causally disconnected) chains in a session
const chains = findIndependentChains(events); // string[][] (event IDs)

CausalGraphNode contains the event and its children (events that list it as a causal parent). Events without causal_parents are treated as graph roots.

Usage in EventStore: IEventStore exposes buildCausalGraph, findDownstream, and findIndependentChains as interface methods, implemented by both in-memory and persistent backends.

EventStore ​

In-memory event persistence with snapshot support. Stores events per session with range queries and replay capabilities. Can be used as a fast/ephemeral store, or swapped with the persistent store for durability.

typescript
import { EventStore } from "./src/store/event-store.ts";

const store = new EventStore();

// Store an event
store.store(event);

// Read events
const events = store.getEvents("session-1");
const recent = store.getEventsInRange("session-1", 5, 10);

// Snapshots for fast replay
const snapshot = store.createSnapshot("session-1", { lastEventId: "abc" });

PersistentEventStore ​

File-based event persistence with JSONL append-only logs, snapshot persistence, and startup recovery. Durably writes events to disk and reconstructs in-memory state on initialization, enabling sessions to survive process restarts and crashes.

typescript
import { PersistentEventStore } from "./src/store/persistence.ts";

// Default: uses ./data/events/ directory
const store = new PersistentEventStore({ dataDir: "./data/events" });

// Custom directory
const store = new PersistentEventStore({ dataDir: "/var/lib/kayak/events" });

// Custom backend (e.g., SQLite in the future)
const store = new PersistentEventStore({
  dataDir: "./data/events",
  backend: new SQLitePersistenceBackend("./data/kayak.db"),
});

// Store events — written synchronously to JSONL on disk
store.store(event);

// Read events — served from in-memory cache (rebuilt from disk on startup)
const events = store.getEvents("session-1");
const recent = store.getEventsInRange("session-1", 5, 10);

// Snapshots — persisted to disk as JSON files
const snapshot = store.createSnapshot("session-1", { lastEventId: "abc" });

// Explicit flush (no-op for synchronous writes, available for future buffered backends)
store.flush();

File layout:

FilePurpose
<session_id>.jsonlAppend-only event log (one JSON object per line)
<session_id>.snapshot.jsonLatest snapshot for fast recovery

Recovery behavior:

  • On startup, PersistentEventStore scans the data directory, loads snapshots, and replays events after the snapshot point.
  • Corrupted lines are logged and skipped; valid events continue loading.
  • Empty or missing data directory → clean start with no sessions.

Pluggable backends:

The IPersistenceBackend interface allows swapping storage engines without changing callers:

typescript
interface IPersistenceBackend {
  write(sessionId: string, line: string): void;
  readLines(sessionId: string): string[];
  writeSnapshot(sessionId: string, data: Snapshot): void;
  readSnapshot(sessionId: string): Snapshot | undefined;
  listSessions(): string[];
  exists(sessionId: string): boolean;
}

The default FilePersistenceBackend uses synchronous Deno file I/O for guaranteed durability per write. Implement this interface for SQLite, PostgreSQL, or other backends.

SQLite backend: The SQLitePersistenceBackend provides embedded SQL storage with WAL mode for concurrent reads and SQL-based analytics queries. It implements both IPersistenceBackend and IMemoryStorage interfaces.

typescript
import { SQLitePersistenceBackend } from "./src/store/sqlite-backend.ts";

const backend = new SQLitePersistenceBackend({ dbPath: "./kayak.db" });
const store = new PersistentEventStore({
  dataDir: "./data/events",
  backend,
});

SQLite query engine: The SQLiteQueryEngine replaces hand-rolled JavaScript aggregation with SQL-based queries on the SQLite database.

typescript
import { SQLiteQueryEngine } from "./src/store/sqlite-query-engine.ts";

const engine = new SQLiteQueryEngine(db);
const metrics = engine.getToolPerformance();
const summary = engine.getSessionSummary("session-1");

SQLite Storage ​

SQLite-backed persistence and analytics. The SQLitePersistenceBackend implements both IPersistenceBackend (event storage) and IMemoryStorage (memory storage) using a single embedded database file. WAL mode enables concurrent reads while a single writer serializes mutations.

ComponentFilePurpose
SQLitePersistenceBackendsqlite-backend.tsEvent + memory storage with WAL mode
SQLiteQueryEnginesqlite-query-engine.tsSQL-based analytics and aggregation

Schema: Three tables — events (event payloads with session/sequence indexing), memories (typed memory storage), and snapshots (session snapshot persistence).

Query capabilities via SQLiteQueryEngine:

MethodPurpose
getToolPerformance()Success rate, average duration per tool
getErrorPatterns()Error frequency by type and tool
getSessionSummary()Event counts, duration, tool/model invocations
getRecentSessions()Latest sessions with aggregate metrics
getEventTypeDistribution()Event type frequency and percentages
getAggregateToolUsage()Total invocations, unique tool count
getSessionDurationTrends()Average/min/max session durations
getTimeSeriesAggregation()Time-bucketed event and error counts
getSessionWithMemories()Cross-table join: sessions × memories
getToolUsageBySession()Pivot table: tool counts per session
getRollingErrorRate()Window function: rolling error rate over events
typescript
import { SQLitePersistenceBackend } from "./src/store/sqlite-backend.ts";
import { SQLiteQueryEngine } from "./src/store/sqlite-query-engine.ts";

const backend = new SQLitePersistenceBackend({ dbPath: "./data/kayak.db" });
const db = backend.getDatabase();
const engine = new SQLiteQueryEngine(db);

// Analytics
const performance = engine.getToolPerformance("shell");
const errors = engine.getErrorPatterns();
const trends = engine.getSessionDurationTrends();
const timeSeries = engine.getTimeSeriesAggregation("hour");

Schema Registry ​

Event schema versioning and migration. Registers schema versions per event type and migrates events on read.

typescript
const registry = new SchemaRegistry();

// Register schema with migration
registry.register("session.created", 2, schemaV2, (event) => migrateV1toV2(event));

// EventStore uses registry for automatic migration
const store = new EventStore(undefined, registry);

Health System ​

Component health reporting with parallel checks and Kubernetes-compatible endpoints.

typescript
import { HealthRegistry, createHealthHandler } from "./src/core/health.ts";

const registry = new HealthRegistry();
registry.register("event-store", () => checkEventStore(store));
registry.register("capabilities", () => checkCapabilities(registry));

// Run all checks in parallel (1s timeout each)
const result = await registry.check();
// result.status: "healthy" | "degraded" | "unhealthy"

// HTTP endpoints
const handler = createHealthHandler(registry);
// GET /health  → full status (200/503)
// GET /ready   → readiness (200/503)
// GET /alive   → liveness (200)

Configuration Management ​

YAML-based config loading with env var overrides and secret masking.

typescript
import { loadConfig, validateConfig, maskSecrets } from "./src/core/config.ts";

// Load from config directory (precedence: env > file > defaults)
const config = await loadConfig("./config");

// Validate raw config
const result = validateConfig(rawObj);
if (!result.valid) {
  console.error(result.errors);
}

// Mask secrets in output
console.log(maskSecrets(config));
// { persistence: { dataDir: "/data" }, capabilities: { github: { token: "***" } } }

Hot-reload: ConfigWatcher watches the config file for changes with debounced file watching to prevent reload storms. On change, the new config is validated before replacing the active one — if validation fails, the previous config is retained and the error is logged, enabling automatic rollback to a known-good state.

Env var overrides: KAYAK_PERSISTENCE_DATA_DIR → config.persistence.dataDir

Rate Limiting ​

Token bucket rate limiter for external API calls. Smooths burst traffic while enforcing average rate.

typescript
import { TokenBucket, RateLimiter } from "./src/core/rate-limiter.ts";

const bucket = new TokenBucket({
  capacity: 100,        // max burst
  refillRate: 10,       // tokens per interval
  refillIntervalMs: 1000,
});

const limiter = new RateLimiter(bucket);

// Wrap async function — throws on limit exceeded
const limitedFetch = limiter.wrap(fetch);
await limitedFetch("https://api.example.com");

// Or wait for tokens
const waitingFetch = limiter.wrapWithWait(fetch);
await waitingFetch("https://api.example.com"); // blocks until tokens available

Bounded Queue ​

Queue with configurable overflow policies for backpressure handling.

typescript
import { BoundedQueue } from "./src/core/bounded-queue.ts";

const queue = new BoundedQueue<Event>({
  maxSize: 1000,
  policy: "drop-oldest",  // or: drop-newest, block, reject
});

queue.push(event);
const next = queue.shift();

Overflow policies:

PolicyBehavior
drop-oldestRemove oldest item when full
drop-newestDiscard new item when full
blockWait until space available
rejectThrow error when full

Reliability Patterns ​

Circuit breaker, retry, and fallback for fault tolerance.

typescript
import { CircuitBreaker } from "./src/core/circuit-breaker.ts";
import { withRetry } from "./src/core/retry.ts";
import { executeWithFallback } from "./src/core/fallback.ts";

// Circuit breaker — opens after N failures
const breaker = new CircuitBreaker({ failureThreshold: 5, recoveryTimeMs: 30_000 });

// Retry with backoff
const result = await withRetry(fn, { maxRetries: 3, baseDelayMs: 100 });

// Fallback — try primary, fall back on failure
const result = await executeWithFallback(primaryFn, fallbackFn, breaker);

Error Taxonomy ​

Typed error hierarchy with error codes and retryability.

typescript
import { AppError, ValidationError, TimeoutError, RateLimitError } from "./src/core/errors.ts";

// All errors extend AppError
throw new ValidationError("Invalid input", { field: "name" });
throw new TimeoutError("Request timed out", { timeoutMs: 5000 });
throw new RateLimitError("Rate limited", { retryAfterMs: 60_000 });

// Error codes are unique strings
// AppError carries context, module, and operation

Capability Layer ​

Abstract interfaces for external systems. Capabilities are pluggable and independently testable.

CapabilityInterfaceImplementation
ShellIShellCapabilityReal — Deno.Command with safety constraints
SandboxISandboxRuntimeReal — Docker/gVisor with hardened flags
GitIGitCapabilityStubbed — simulated data
GitHubIGitHubCapabilityStubbed — simulated data
KubernetesIKubernetesCapabilityStubbed — simulated data

Capabilities follow a common pattern:

typescript
interface ICapability {
  readonly definition: CapabilityDefinition;
  initialize(context: CapabilityContext): Promise<void>;
  dispose(): Promise<void>;
}

interface CapabilityResult<T> {
  success: boolean;
  data?: T;
  error?: string;
}

Projection Layer ​

UI surfaces subscribe to the event stream and render events. Projections are independent — multiple can run simultaneously.

Projection Protocol ​

Subscription management with event filtering, pause/resume, and reconnection support.

typescript
const protocol = new ProjectionProtocol(eventStream);

// Subscribe to a session
const sub = protocol.subscribe("abc-123", (event) => {
  console.log(`[${event.event_type}]`, event.payload);
}, {
  filter: { event_types: ["tool.execution.started", "tool.execution.completed"] },
});

// Pause and resume
protocol.pause(sub.id);
protocol.resume(sub.id);

Terminal Projection ​

ANSI-colored event rendering for CLI surfaces.

typescript
const terminal = new TerminalProjection(protocol);
await terminal.start("abc-123");

WebSocket Projection ​

Real-time event delivery to connected clients via WebSocket. Supports subscription management, gap recovery, and backpressure.

typescript
import { WebSocketProjectionServer } from "./src/projection/websocket-server.ts";

const server = new WebSocketProjectionServer(eventStore, { port: 8080 });
await server.start();

// Clients connect via WebSocket, receive:
// - Welcome message with version and capabilities
// - Events matching their subscription filter
// - Gap recovery on reconnect

Heartbeat lifecycle: The WebSocket server maintains connection health via configurable heartbeat pings. heartbeatIntervalMs controls how often pings are sent; pongTimeoutMs determines how long to wait for a pong response before closing a stale connection.

Per-session event ordering: Events are inserted into each session's pendingEvents queue via binary search on sequence number, ensuring correct ordering even when events arrive out of order from concurrent sources.

Backpressure: Each client session uses a BoundedQueue for its send queue. When the queue fills, the configured overflow policy (drop-oldest, drop-newest, block, reject) applies, preventing slow consumers from blocking the event stream.

Client messages: subscribe, unsubscribe, reconnect, pongServer messages: welcome, event, error, ping

Review CLI ​

Standalone code review tool in review/. Runs custom checks and delegates to external CLI tools, producing prioritized findings. Not part of the agent runtime — executes directly via deno task review.

Check registry pattern: registry.ts scans review/checks/ and review/delegate/ at startup. Each file must export a default ReviewCheck object. No configuration or wiring required — add a file, it runs.

ComponentFilePurpose
Findingtypes.tsA single finding: title, body, priority (1–3), confidence, file path, line range
ReviewChecktypes.tsInterface: name, description, run(ctx) → Finding[]
ReviewContexttypes.tsPre-computed file metadata shared across all checks
buildContextcontext.tsScans src/ once; builds file map, test pairing, dependency graph
loadChecksregistry.tsAuto-discovers custom checks from review/checks/
loadDelegatesregistry.tsAuto-discovers tool delegates from review/delegate/
formatFindingsformatter.tsGroups findings by file, renders with priority colors and summary
maincli.tsParses --only/--skip/--fail-on, runs preflight + checks, outputs results

CLI flags:

FlagEffect
--only <name>Run only the named check(s)
--skip <name>Skip the named check(s) or preflight
--fail-on <priority>Exit 1 if any finding has priority ≤ threshold (CI gate)

Custom check example:

typescript
import type { Finding, ReviewCheck, ReviewContext } from "../types.ts";

const check: ReviewCheck = {
  name: "my-check",
  description: "What this check does",
  async run(ctx: ReviewContext): Promise<Finding[]> {
    const findings: Finding[] = [];
    for (const [, entry] of ctx.files) {
      // scan and produce findings
    }
    return findings;
  },
};

export default check;

Delegate example (wraps an external CLI tool):

typescript
import type { Finding, ReviewCheck } from "../types.ts";

const check: ReviewCheck = {
  name: "my-tool",
  description: "Runs an external tool",
  async run(): Promise<Finding[]> {
    const cmd = new Deno.Command("my-tool", { args: [...], stdout: "piped" });
    const output = await cmd.output();
    // parse output into Finding[]
    return findings;
  },
};

export default check;

Data Flow ​

Design Principles ​

  1. Events are the source of truth — All state is derivable from events. No hidden mutable state.
  2. UI independence — Agent runtime never knows about UI surfaces. Projections are disposable.
  3. Provider independence — Model providers are abstracted. Swap without code changes.
  4. Capability independence — External systems are abstracted. Test with mocks.
  5. Session isolation — Events from different sessions never mix. Concurrent sessions are safe.
  6. Recovery by replay — Any session state can be reconstructed from its event stream.

Released under the ISC License.