/** * Storage ports (the hexagonal seams — vision §1 "storage stays swappable"). * * One port per ENTITY, with domain operations — never one generic * key/value interface. Core and the service layer that orbits it code * against these types only; concrete adapters live in src/store/ (fs * JSON/JSONL today, SQLite drop-in later) and are wired exactly once, in * the composition root (src/server/stores.ts). Dependencies point one way: * adapters may import core; core NEVER imports an adapter. */ import type { UIMessage } from "ai"; import type { ChatRecord, ChatSummary, GraphEdge, GraphNode, JobKind, JobRecord, LedgerEvent, NodeMapping, PlanTree, SessionCheckpoint, } from "./contracts.ts"; // ─── ChatStore — the session record's home (data/chats) ───────────────────── export interface ChatStore { /** Create a session record (persisted immediately). */ createChat(title: string): ChatRecord; /** The full record, or null if unknown id / corrupt file. */ getChat(id: string): ChatRecord | null; /** All sessions, newest activity first (the sidebar's list view). */ listChats(): ChatSummary[]; /** Persist the full message list (client state + new response) after a turn. */ saveMessages(id: string, messages: UIMessage[]): void; /** Persist the session plan tree (plan tools call this in execute). */ saveTree(id: string, tree: PlanTree): void; } // ─── GraphStore — the append-only evidence ledgers (data/graph) ────────────── // v1 surface = the five JSONL ledgers (nodes / edges / events / mappings / // sessions). Reads return the raw append-order lines; merging edge assertions // by (from, to, type) is DERIVED in core/graph.ts — the store never decides. // The extraction pipeline (next stage) becomes the sole writer; this read // surface stays. export interface GraphStore { /** Append one GraphNode (ids are unique — no merging needed). */ appendNode(node: GraphNode): void; /** Raw node lines in append order. */ readNodes(): GraphNode[]; /** Append one edge ASSERTION (a second session asserting the same * (from, to, type) appends another line; readers merge). */ appendEdge(assertion: GraphEdge): void; readEdges(): GraphEdge[]; /** Append one quiz outcome / understanding observation. */ appendEvent(event: LedgerEvent): void; readEvents(): LedgerEvent[]; /** Append one identity-map line (chat's tree node → graph node). */ appendMapping(mapping: NodeMapping): void; readMappings(): NodeMapping[]; /** Append one emission checkpoint. */ appendCheckpoint(checkpoint: SessionCheckpoint): void; readCheckpoints(): SessionCheckpoint[]; } // ─── JobStore — background sub-agent jobs (data/jobs) ───────────────────────── export interface CreateJobInit { kind: JobKind; chatId: string; toolCallId: string; /** First activity line (defaults to "starting"). */ activity?: string; } /** Mergeable fields — identity (id, kind, chatId, toolCallId, createdAt) is immutable. */ export type JobPatch = Partial>; export interface JobStore { /** Create a running job (keyed by toolCallId for spawn dedup). */ createJob(init: CreateJobInit): JobRecord; getJob(id: string): JobRecord | null; /** Merge a patch into the record; null if unknown id. */ updateJob(id: string, patch: JobPatch): JobRecord | null; /** All jobs for a chat, newest activity first. */ listJobs(chatId: string): JobRecord[]; /** The job fulfilling a given tool part, if any (spawn-hook dedup). */ findJobByToolCallId(chatId: string, toolCallId: string): JobRecord | null; }