mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-06 23:44:36 +00:00
feat(agent): separate sessions, branches, and lanes
This commit is contained in:
parent
cbaec01048
commit
423e20cb78
82 changed files with 3933 additions and 4299 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { access, mkdir, open as openFile, readdir, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { Context, Entry, ForkOptions, SessionCreateOptions, StoredValue } from "@earendil-works/pi-agent-core";
|
||||
import { createForkSnapshot, laneLeaf, StorageBackedSession } from "@earendil-works/pi-agent-core";
|
||||
import { branchTip, createForkSnapshot, StorageBackedSession } from "@earendil-works/pi-agent-core";
|
||||
import { uuidv7 } from "@earendil-works/pi-ai";
|
||||
import { applyInitialSchema } from "./migrations.ts";
|
||||
import { appendEntryToBranchIndex, scanBranchEntries } from "./session/branch-entries.ts";
|
||||
|
|
@ -15,7 +15,7 @@ import {
|
|||
readSessionRow,
|
||||
type SqliteSessionMetadata,
|
||||
} from "./session/session-row.ts";
|
||||
import { insertInitialMainLaneValues, readAllScalarValueRows, setScalarValueRow } from "./session/values.ts";
|
||||
import { readAllScalarValueRows, setScalarValueRow } from "./session/values.ts";
|
||||
import {
|
||||
claimWriterLease,
|
||||
readWriterLease,
|
||||
|
|
@ -33,7 +33,7 @@ export const SQLITE_SESSION_EXTENSION = ".sqlite";
|
|||
|
||||
const DEFAULT_WRITER_LEASE_MS = 30_000;
|
||||
const WRITER_LEASE_RENEW_INTERVAL_MS = DEFAULT_WRITER_LEASE_MS / 2;
|
||||
const FIRST_AVAILABLE_COMMIT_SEQ = 3;
|
||||
const FIRST_AVAILABLE_COMMIT_SEQ = 1;
|
||||
|
||||
export type SqliteSessionCreateOptions = SessionCreateOptions;
|
||||
|
||||
|
|
@ -98,12 +98,12 @@ function readForkSourceEntries(
|
|||
options: ForkOptions,
|
||||
): Entry[] {
|
||||
if (options.scope === "tree") return readSourceEntries(db, sessionId);
|
||||
const mainAddress = laneLeaf("main");
|
||||
const mainLeaf = scalarValues.find(
|
||||
const mainAddress = branchTip("main");
|
||||
const mainTip = scalarValues.find(
|
||||
(stored) => stored.address.namespace === mainAddress.namespace && stored.address.key === mainAddress.key,
|
||||
) as StoredValue<string | null> | undefined;
|
||||
if (mainLeaf === undefined) throw new Error("Source session is missing main lane");
|
||||
const requested = options.entryId ?? mainLeaf.value;
|
||||
if (mainTip === undefined) throw new Error("Source session is missing main branch");
|
||||
const requested = options.entryId ?? mainTip.value;
|
||||
return requested === null ? [] : scanBranchEntries(db, sessionId, { start: requested, order: "oldestFirst" });
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +188,6 @@ export class SqliteSessionRepo {
|
|||
lease = activeDb.transaction(() => {
|
||||
if (hasSessionRow(activeDb, id)) throw new Error(`SQLite session already exists: ${id}`);
|
||||
insertSessionRow(activeDb, metadata, SQLITE_STORAGE_VERSION, FIRST_AVAILABLE_COMMIT_SEQ);
|
||||
insertInitialMainLaneValues(activeDb, id);
|
||||
return claimWriterLease(activeDb, id, uuidv7(this.now()), this.now(), DEFAULT_WRITER_LEASE_MS);
|
||||
});
|
||||
initialized = true;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,14 @@
|
|||
import type {
|
||||
AgentMessage,
|
||||
BranchScan,
|
||||
Branch,
|
||||
Context,
|
||||
Entry,
|
||||
EntryQuery,
|
||||
JsonValue,
|
||||
LaneConfiguration,
|
||||
ListElement,
|
||||
ListReadOptions,
|
||||
Session,
|
||||
SessionMutation,
|
||||
SessionMutator,
|
||||
SessionMutationCallback,
|
||||
SessionStats,
|
||||
SessionTree,
|
||||
StorageBranchScan,
|
||||
StoredValue,
|
||||
Value,
|
||||
|
|
@ -60,22 +56,65 @@ export class SqliteOpenSession implements Session<SqliteSessionMetadata> {
|
|||
this.renewalTimer.unref?.();
|
||||
}
|
||||
|
||||
beginMutation(lane: string, context: Context): Promise<SessionMutation> {
|
||||
return this.admit(() => this.session.beginMutation(lane, context));
|
||||
async beginMutation(context: Context): Promise<SessionMutation> {
|
||||
let resolveFinished!: () => void;
|
||||
const finished = new Promise<void>((resolve) => {
|
||||
resolveFinished = resolve;
|
||||
});
|
||||
this.admitted.add(finished);
|
||||
let source: SessionMutation;
|
||||
try {
|
||||
source = await this.admit(() => this.session.beginMutation(context));
|
||||
} catch (error) {
|
||||
this.admitted.delete(finished);
|
||||
resolveFinished();
|
||||
throw error;
|
||||
}
|
||||
if (this.state !== "open") {
|
||||
await source.end(context);
|
||||
this.admitted.delete(finished);
|
||||
resolveFinished();
|
||||
throw this.closedError;
|
||||
}
|
||||
let ended = false;
|
||||
return {
|
||||
commit: (writes, commitContext) => source.commit(writes, commitContext),
|
||||
end: async (endContext) => {
|
||||
try {
|
||||
await source.end(endContext);
|
||||
} finally {
|
||||
if (!ended) {
|
||||
ended = true;
|
||||
this.admitted.delete(finished);
|
||||
resolveFinished();
|
||||
}
|
||||
}
|
||||
},
|
||||
getEntries: (ids, readContext) => source.getEntries(ids, readContext),
|
||||
getValue: (address, readContext) => source.getValue(address, readContext),
|
||||
scanValues: (prefix, readContext) => source.scanValues(prefix, readContext),
|
||||
readList: (address, options, readContext) => source.readList(address, options, readContext),
|
||||
scanBranch: (query, readContext) => source.scanBranch(query, readContext),
|
||||
};
|
||||
}
|
||||
|
||||
mutate<T>(
|
||||
lane: string,
|
||||
mutation: (mutator: SessionMutator, context: Context) => T | Promise<T>,
|
||||
context: Context,
|
||||
): Promise<T> {
|
||||
return this.admit(() => this.session.mutate(lane, mutation, context));
|
||||
mutate<T>(mutation: SessionMutationCallback<T>, context: Context): Promise<T> {
|
||||
return this.admit(() =>
|
||||
this.session.mutate((mutator, mutationContext) => {
|
||||
if (this.state !== "open") throw this.closedError;
|
||||
return mutation(mutator, mutationContext);
|
||||
}, context),
|
||||
);
|
||||
}
|
||||
|
||||
getEntries(ids: string[], context: Context): Promise<Map<string, Entry>> {
|
||||
return this.admit(() => this.session.getEntries(ids, context));
|
||||
}
|
||||
|
||||
getEntry(id: string, context: Context): Promise<Entry | undefined> {
|
||||
return this.admit(() => this.session.getEntry(id, context));
|
||||
}
|
||||
|
||||
getValue<T>(address: Value<T>, context: Context): Promise<StoredValue<T> | undefined> {
|
||||
return this.admit(() => this.session.getValue(address, context));
|
||||
}
|
||||
|
|
@ -96,58 +135,35 @@ export class SqliteOpenSession implements Session<SqliteSessionMetadata> {
|
|||
return this.admit(() => this.session.scanBranch(query, context));
|
||||
}
|
||||
|
||||
view(lane: string): SessionTree {
|
||||
const view = this.session.view(lane);
|
||||
return {
|
||||
getLeafId: (context) => this.admit(() => view.getLeafId(context)),
|
||||
getEntry: (id, context) => this.admit(() => view.getEntry(id, context)),
|
||||
getStats: (context) => this.admit(() => view.getStats(context)),
|
||||
getValue: (address, context) => this.admit(() => view.getValue(address, context)),
|
||||
scanValues: (prefix, context) => this.admit(() => view.scanValues(prefix, context)),
|
||||
readList: (address, options, context) => this.admit(() => view.readList(address, options, context)),
|
||||
setValue: (address, next, context) => this.admit(() => view.setValue(address, next, context)),
|
||||
deleteValue: (address, context) => this.admit(() => view.deleteValue(address, context)),
|
||||
appendList: (address, element, context) => this.admit(() => view.appendList(address, element, context)),
|
||||
deleteList: (address, context) => this.admit(() => view.deleteList(address, context)),
|
||||
getName: (context) => this.admit(() => view.getName(context)),
|
||||
setName: (name, context) => this.admit(() => view.setName(name, context)),
|
||||
getLabel: (targetId, context) => this.admit(() => view.getLabel(targetId, context)),
|
||||
setLabel: (targetId, label, context) => this.admit(() => view.setLabel(targetId, label, context)),
|
||||
findEntries: (query, context) => this.admit(() => view.findEntries(query, context)),
|
||||
findEntry: (query, context) => this.admit(() => view.findEntry(query, context)),
|
||||
findEntriesOnBranch: (query, context) => this.admit(() => view.findEntriesOnBranch(query, context)),
|
||||
findEntryOnBranch: (query, context) => this.admit(() => view.findEntryOnBranch(query, context)),
|
||||
appendMessage: (message, context) => this.admit(() => view.appendMessage(message, context)),
|
||||
appendCustomEntry: (customType, data, context) =>
|
||||
this.admit(() => view.appendCustomEntry(customType, data, context)),
|
||||
};
|
||||
}
|
||||
|
||||
createLane(
|
||||
name: string,
|
||||
at: string | null,
|
||||
configuration: LaneConfiguration,
|
||||
onCommitted: ((context: Context) => void | Promise<void>) | undefined,
|
||||
context: Context,
|
||||
): Promise<SessionTree> {
|
||||
return this.admit(async () => {
|
||||
await this.session.createLane(name, at, configuration, onCommitted, context);
|
||||
return this.view(name);
|
||||
});
|
||||
}
|
||||
|
||||
getLeafId(context: Context): Promise<string | null> {
|
||||
return this.admit(() => this.session.getLeafId(context));
|
||||
}
|
||||
|
||||
getEntry(id: string, context: Context): Promise<Entry | undefined> {
|
||||
return this.admit(() => this.session.getEntry(id, context));
|
||||
}
|
||||
|
||||
getStats(context: Context): Promise<SessionStats> {
|
||||
return this.admit(() => this.session.getStats(context));
|
||||
}
|
||||
|
||||
getName(context: Context): Promise<string | undefined> {
|
||||
return this.admit(() => this.session.getName(context));
|
||||
}
|
||||
|
||||
getLabel(targetId: string, context: Context): Promise<string | undefined> {
|
||||
return this.admit(() => this.session.getLabel(targetId, context));
|
||||
}
|
||||
|
||||
findEntries(query: EntryQuery | undefined, context: Context): Promise<Entry[]> {
|
||||
return this.admit(() => this.session.findEntries(query, context));
|
||||
}
|
||||
|
||||
findEntry(query: EntryQuery | undefined, context: Context): Promise<Entry | undefined> {
|
||||
return this.admit(() => this.session.findEntry(query, context));
|
||||
}
|
||||
|
||||
async branch(name: string, context: Context): Promise<Branch | undefined> {
|
||||
const branch = await this.admit(() => this.session.branch(name, context));
|
||||
return branch === undefined ? undefined : this.wrapBranch(branch);
|
||||
}
|
||||
|
||||
async createBranch(name: string, at: string | null, context: Context): Promise<Branch> {
|
||||
return this.wrapBranch(await this.admit(() => this.session.createBranch(name, at, context)));
|
||||
}
|
||||
|
||||
setValue<T>(address: Value<T>, next: NoInfer<T>, context: Context): Promise<void> {
|
||||
return this.admit(() => this.session.setValue(address, next, context));
|
||||
}
|
||||
|
|
@ -164,46 +180,14 @@ export class SqliteOpenSession implements Session<SqliteSessionMetadata> {
|
|||
return this.admit(() => this.session.deleteList(address, context));
|
||||
}
|
||||
|
||||
getName(context: Context): Promise<string | undefined> {
|
||||
return this.admit(() => this.session.getName(context));
|
||||
}
|
||||
|
||||
setName(name: string | undefined, context: Context): Promise<void> {
|
||||
return this.admit(() => this.session.setName(name, context));
|
||||
}
|
||||
|
||||
getLabel(targetId: string, context: Context): Promise<string | undefined> {
|
||||
return this.admit(() => this.session.getLabel(targetId, context));
|
||||
}
|
||||
|
||||
setLabel(targetId: string, label: string | undefined, context: Context): Promise<void> {
|
||||
return this.admit(() => this.session.setLabel(targetId, label, context));
|
||||
}
|
||||
|
||||
findEntries(query: EntryQuery | undefined, context: Context): Promise<Entry[]> {
|
||||
return this.admit(() => this.session.findEntries(query, context));
|
||||
}
|
||||
|
||||
findEntry(query: EntryQuery | undefined, context: Context): Promise<Entry | undefined> {
|
||||
return this.admit(() => this.session.findEntry(query, context));
|
||||
}
|
||||
|
||||
findEntriesOnBranch(query: BranchScan | undefined, context: Context): Promise<Entry[]> {
|
||||
return this.admit(() => this.session.findEntriesOnBranch(query, context));
|
||||
}
|
||||
|
||||
findEntryOnBranch(query: BranchScan | undefined, context: Context): Promise<Entry | undefined> {
|
||||
return this.admit(() => this.session.findEntryOnBranch(query, context));
|
||||
}
|
||||
|
||||
appendMessage(message: AgentMessage, context: Context): Promise<string> {
|
||||
return this.admit(() => this.session.appendMessage(message, context));
|
||||
}
|
||||
|
||||
appendCustomEntry(customType: string, data: JsonValue | undefined, context: Context): Promise<string> {
|
||||
return this.admit(() => this.session.appendCustomEntry(customType, data, context));
|
||||
}
|
||||
|
||||
close(context: Context): Promise<void> {
|
||||
if (this.closePromise !== undefined) return this.closePromise;
|
||||
this.state = "closing";
|
||||
|
|
@ -221,6 +205,18 @@ export class SqliteOpenSession implements Session<SqliteSessionMetadata> {
|
|||
return this.closePromise;
|
||||
}
|
||||
|
||||
private wrapBranch(branch: Branch): Branch {
|
||||
return {
|
||||
name: branch.name,
|
||||
getTipId: (context) => this.admit(() => branch.getTipId(context)),
|
||||
findEntries: (query, context) => this.admit(() => branch.findEntries(query, context)),
|
||||
findEntry: (query, context) => this.admit(() => branch.findEntry(query, context)),
|
||||
appendMessage: (message, context) => this.admit(() => branch.appendMessage(message, context)),
|
||||
appendCustomEntry: (customType, data, context) =>
|
||||
this.admit(() => branch.appendCustomEntry(customType, data, context)),
|
||||
};
|
||||
}
|
||||
|
||||
private admit<T>(operation: () => Promise<T>): Promise<T> {
|
||||
if (this.state !== "open") return Promise.reject(this.closedError);
|
||||
if (this.leaseError !== undefined) return Promise.reject(this.leaseError);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import {
|
||||
type ListElement,
|
||||
type ListReadOptions,
|
||||
laneLeaf,
|
||||
laneState,
|
||||
resolveListReadOptions,
|
||||
type StoredValue,
|
||||
setValue,
|
||||
type Value,
|
||||
type ValueList,
|
||||
value,
|
||||
|
|
@ -25,17 +22,6 @@ export interface ListValueRow {
|
|||
value: string;
|
||||
}
|
||||
|
||||
export function insertInitialMainLaneValues(db: SqliteDatabase, sessionId: string): void {
|
||||
const writes = [
|
||||
setValue(laneLeaf("main"), null),
|
||||
setValue(laneState("main"), { currentOperationId: null, pendingNextRun: [] }),
|
||||
];
|
||||
for (const [index, write] of writes.entries()) {
|
||||
if (write.op !== "set") throw new Error("Expected initial scalar value write");
|
||||
setScalarValueRow(db, sessionId, write.namespace, write.key, index + 1, write.value);
|
||||
}
|
||||
}
|
||||
|
||||
export function setScalarValueRow(
|
||||
db: SqliteDatabase,
|
||||
sessionId: string,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import type {
|
|||
ValueList,
|
||||
Write,
|
||||
} from "@earendil-works/pi-agent-core";
|
||||
import { laneLeaf, prepareStorageCommit } from "@earendil-works/pi-agent-core";
|
||||
import { branchTip, prepareStorageCommit } from "@earendil-works/pi-agent-core";
|
||||
import { appendEntryToBranchIndex, scanBranchEntries, scanBranchEntryStructures } from "./session/branch-entries.ts";
|
||||
import { decodeEntryRow, EntryRowWriter, readAllEntryRows, readEntryRows, scanEntryRows } from "./session/entries.ts";
|
||||
import { advanceNextSeq, readNextSeq } from "./session/session-sequences.ts";
|
||||
|
|
@ -154,12 +154,12 @@ export class SqliteStorage implements Storage {
|
|||
|
||||
private readSnapshotEntries(options: ForkOptions, scalarValues: readonly StoredValue<unknown>[]): Entry[] {
|
||||
if (options.scope === "tree") return readAllEntryRows(this.db, this.sessionId).map(decodeEntryRow);
|
||||
const mainAddress = laneLeaf("main");
|
||||
const mainLeaf = scalarValues.find(
|
||||
const mainAddress = branchTip("main");
|
||||
const mainTip = scalarValues.find(
|
||||
(stored) => stored.address.namespace === mainAddress.namespace && stored.address.key === mainAddress.key,
|
||||
) as StoredValue<string | null> | undefined;
|
||||
if (mainLeaf === undefined) throw new Error("Source session is missing main lane");
|
||||
const requested = options.entryId ?? mainLeaf.value;
|
||||
if (mainTip === undefined) throw new Error("Source session is missing main branch");
|
||||
const requested = options.entryId ?? mainTip.value;
|
||||
return requested === null
|
||||
? []
|
||||
: scanBranchEntries(this.db, this.sessionId, { start: requested, order: "oldestFirst" });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue