Carry durable input hooks and add a headless harness channel

This commit is contained in:
2026-11-07 01:03:17 +00:00
parent aaf6d9b54f
commit c8525c3acb
16 changed files with 21810 additions and 153 deletions
+162 -94
View File
@@ -30494,8 +30494,8 @@ var CachedStorage = class {
#submissions = new Lru(256);
/** Per record with a read in flight: the reads, and how many published commits wrote it since the first began. */
#reads = /* @__PURE__ */ new Map();
constructor(storage) {
this.#storage = storage;
constructor(storage2) {
this.#storage = storage2;
}
/** The backend's own scope for a publication (`Storage.publishing`), when it has one. */
get publishing() {
@@ -30686,10 +30686,10 @@ var CachedStorage = class {
return this.#reads.size;
}
};
function scopeOf(storage) {
if (storage.publishing === void 0)
function scopeOf(storage2) {
if (storage2.publishing === void 0)
return void 0;
return (publish2, context) => storage.publishing(publish2, context);
return (publish2, context) => storage2.publishing(publish2, context);
}
var Lru = class {
#map = /* @__PURE__ */ new Map();
@@ -30877,8 +30877,8 @@ function tableKeys(writes, keys) {
var GatedStorage = class {
#storage;
#touch;
constructor(storage, touch) {
this.#storage = storage;
constructor(storage2, touch) {
this.#storage = storage2;
this.#touch = touch;
}
commit(batches, receive, context) {
@@ -30954,24 +30954,24 @@ var ROOT_CONVERSATION_ID = 1;
// node_modules/@earendil-works/pi-durable/dist/session/forks.js
var SCAN_PAGE_SIZE = 256;
async function prepareForkDocumentCopies(storage, parentConversationId, at, childConversationId, context) {
const entry2 = await storage.entry(parentConversationId, at, context);
async function prepareForkDocumentCopies(storage2, parentConversationId, at, childConversationId, context) {
const entry2 = await storage2.entry(parentConversationId, at, context);
if (entry2 === void 0)
throw new Error(`Entry ${at} is not visible from conversation ${parentConversationId}`);
const copies = [];
const copiedAddresses = /* @__PURE__ */ new Set();
await collectCopies(storage, { kind: "conversation", conversationId: entry2.entry.conversationId }, entry2.commitSeq, "asOf", childConversationId, copies, copiedAddresses, context);
await collectCopies(storage, { kind: "conversation", conversationId: parentConversationId }, "current", "current", childConversationId, copies, copiedAddresses, context);
await collectCopies(storage2, { kind: "conversation", conversationId: entry2.entry.conversationId }, entry2.commitSeq, "asOf", childConversationId, copies, copiedAddresses, context);
await collectCopies(storage2, { kind: "conversation", conversationId: parentConversationId }, "current", "current", childConversationId, copies, copiedAddresses, context);
return copies;
}
async function collectCopies(storage, scope, at, policy, childConversationId, copies, copiedAddresses, context) {
async function collectCopies(storage2, scope, at, policy, childConversationId, copies, copiedAddresses, context) {
let cursor;
do {
const page2 = await storage.scanDocuments({ scope, at }, SCAN_PAGE_SIZE, cursor, context);
const page2 = await storage2.scanDocuments({ scope, at }, SCAN_PAGE_SIZE, cursor, context);
for (const source of page2.items) {
if (source.scope.kind !== "conversation" || source.fork !== policy)
continue;
const id = await storage.mintId();
const id = await storage2.mintId();
const identity = {
id,
kind: source.kind,
@@ -31140,6 +31140,9 @@ var Transaction = class {
this.#assertOpen();
return record;
}
beforeInput(conversationId, input) {
return this.#read("beforeInput", () => this.#host.beforeInput(this, conversationId, input, this.#context));
}
appendEntry(first, second, third) {
if (typeof first === "number")
return this.#appendEntry(first, second);
@@ -31612,7 +31615,7 @@ var Transaction = class {
return { observed: publications, stored: stored ?? publications };
}
async #assemble() {
const storage = this.#host.storage;
const storage2 = this.#host.storage;
const plans = this.#plans;
for (const document of this.#documents) {
const plan = planDocument(document);
@@ -31667,7 +31670,7 @@ var Transaction = class {
}
let cursor;
do {
const page2 = await storage.scanDocuments({ scope: { kind: "task", taskId }, at: "current" }, INTERNAL_SCAN_PAGE_SIZE, cursor, this.#context);
const page2 = await storage2.scanDocuments({ scope: { kind: "task", taskId }, at: "current" }, INTERNAL_SCAN_PAGE_SIZE, cursor, this.#context);
for (const record of page2.items)
retire(record);
cursor = page2.next;
@@ -31691,7 +31694,7 @@ var Transaction = class {
plan.conversationId = task.publicationConversationId;
}
for (const { id, change } of this.#submissionChanges) {
const current = this.#submissions.get(id) ?? await storage.submission(id, this.#context);
const current = this.#submissions.get(id) ?? await storage2.submission(id, this.#context);
if (current === void 0)
throw new Error(`Submission ${id} does not exist`);
const next = applySubmissionChange(current, change);
@@ -32262,12 +32265,12 @@ var SessionImpl = class {
get failed() {
return this.#failed.promise;
}
constructor(storage, now = Date.now, documentRetentionMs = () => void 0, setTimer, readOnly = false, persistWithinMs = () => void 0) {
constructor(storage2, now = Date.now, documentRetentionMs = () => void 0, setTimer, readOnly = false, persistWithinMs = () => void 0) {
this.#failed.promise.catch(() => {
});
this.#readOnly = readOnly;
this.#cached = readOnly ? void 0 : new CachedStorage(storage);
this.#storage = new GuardedStorage(this.#cached ?? storage, this);
this.#cached = readOnly ? void 0 : new CachedStorage(storage2);
this.#storage = new GuardedStorage(this.#cached ?? storage2, this);
this.#gated = new GatedStorage(this.#storage, (key) => this.touch(key));
this.#writer = new Writer({
commit: (batches, receive, context) => this.#storage.commit(batches, receive, context),
@@ -32322,6 +32325,7 @@ var SessionImpl = class {
if (this.#documents.get(id)?.record.id === recordId)
this.#dropDocument(id);
},
beforeInput: (tx, id, input, context) => this.beforeInput(tx, id, input, context),
conversationCreated: (tx, record) => this.conversationCreated(tx, record),
taskDocuments: (taskId) => this.#taskDocuments.get(taskId)?.values(),
latestEntryId: (conversationId, context) => this.latestEntryId(conversationId, context),
@@ -32646,6 +32650,9 @@ var SessionImpl = class {
*/
report(_error, _info = LISTENER, _context) {
}
async beforeInput(_tx, _id, _input, _context) {
return [];
}
/**
* Runs inside every transaction that creates or forks a conversation, after the conversation record is staged. A
* plain Session stages nothing; a Harness stages its built-in documents.
@@ -33561,8 +33568,8 @@ var GuardedStorage = class {
/** Backend calls underway, also those off the Session line, which close waits for before it closes the backend. */
#underway = /* @__PURE__ */ new Set();
#closing = false;
constructor(storage, latch) {
this.#storage = storage;
constructor(storage2, latch) {
this.#storage = storage2;
this.#latch = latch;
}
/** The backend's own scope for a publication (`Storage.publishing`), when it has one; not a call to guard. */
@@ -34072,21 +34079,21 @@ function defineTask(definition) {
var SCAN_PAGE_SIZE2 = 256;
var EXCLUDED_STOP_REASONS = /* @__PURE__ */ new Set(["aborted", "error", "deferred"]);
var MISSING_RESULT_TEXT = "Tool result unavailable: history ends before this call completed.";
async function captureContextBounds(session, storage, conversationId, context, at, visible = false) {
async function captureContextBounds(session, storage2, conversationId, context, at, visible = false) {
let tail;
if (at === void 0) {
tail = await session.latestEntryId(conversationId, context);
if (tail === void 0)
return void 0;
} else {
if (!visible && await storage.entry(conversationId, at, context) === void 0) {
if (!visible && await storage2.entry(conversationId, at, context) === void 0) {
throw new Error(`Entry ${at} is not visible from conversation ${conversationId}`);
}
tail = at;
}
return { head: await session.latestHeadMarker(conversationId, tail, context), tail };
}
async function readContextFrom(session, storage, conversationId, context, at, previous) {
async function readContextFrom(session, storage2, conversationId, context, at, previous) {
const visible = at !== void 0 && previous !== void 0 && rangeEntry(previous, at) !== void 0;
const bounds = await session.readOnLine((line) => captureContextBounds(session, line, conversationId, context, at, visible));
if (bounds === void 0)
@@ -34095,7 +34102,7 @@ async function readContextFrom(session, storage, conversationId, context, at, pr
const current = at === void 0;
let range;
if (previous === void 0 || previous.bounds.head?.id !== bounds.head?.id) {
const entries2 = freezeJson(await scanRange(storage, conversationId, rangeQuery(bounds), context));
const entries2 = freezeJson(await scanRange(storage2, conversationId, rangeQuery(bounds), context));
range = deriveRange(bounds, entries2, current);
} else if (bounds.tail === previous.bounds.tail) {
range = previous;
@@ -34103,7 +34110,7 @@ async function readContextFrom(session, storage, conversationId, context, at, pr
range = deriveRange(bounds, previous.entries.filter((entry2) => entry2.id <= bounds.tail), false);
} else {
const minEntryId = idFromNumber(previous.bounds.tail + 1);
const added = await scanRange(storage, conversationId, { minEntryId, maxEntryId: bounds.tail }, context);
const added = await scanRange(storage2, conversationId, { minEntryId, maxEntryId: bounds.tail }, context);
range = extendRange(previous, bounds, freezeJson(added), current);
}
if (range.current && session.knownLatestEntryId(conversationId) !== range.bounds.tail) {
@@ -34120,14 +34127,14 @@ async function readContextFrom(session, storage, conversationId, context, at, pr
range
};
}
async function readContext(session, storage, conversationId, context, at) {
async function readContext(session, storage2, conversationId, context, at) {
const bounds = await session.readOnLine((line) => captureContextBounds(session, line, conversationId, context, at));
return deriveContext(storage, conversationId, bounds, context);
return deriveContext(storage2, conversationId, bounds, context);
}
async function deriveContext(storage, conversationId, bounds, context) {
async function deriveContext(storage2, conversationId, bounds, context) {
if (bounds === void 0)
return emptyView();
return deriveRange(bounds, await scanRange(storage, conversationId, rangeQuery(bounds), context), false).view;
return deriveRange(bounds, await scanRange(storage2, conversationId, rangeQuery(bounds), context), false).view;
}
function emptyView() {
return { head: void 0, entries: [], contributions: [], messages: [] };
@@ -34245,11 +34252,11 @@ function rangeQuery(bounds) {
const head = bounds.head;
return head === void 0 ? { maxEntryId: bounds.tail } : { minEntryId: head.head, maxEntryId: bounds.tail };
}
async function scanRange(storage, conversationId, range, context) {
async function scanRange(storage2, conversationId, range, context) {
const entries2 = [];
let cursor;
do {
const page2 = await storage.scanEntries({ conversationId, ...range }, SCAN_PAGE_SIZE2, cursor, context);
const page2 = await storage2.scanEntries({ conversationId, ...range }, SCAN_PAGE_SIZE2, cursor, context);
entries2.push(...page2.items);
cursor = page2.next;
} while (cursor !== void 0);
@@ -34403,10 +34410,14 @@ var InboxDoc = defineDoc({
initial: () => ({ items: [] }),
checkpointWhen: (value2) => value2.items.length === 0
});
async function prepareBoundary(tx, conversationId, modes) {
async function prepareBoundary(tx, conversationId, modes, at, now, incoming) {
const head = (await tx.latestHeadMarker(conversationId))?.head;
const inbox = await tx.doc(InboxDoc, conversationId);
const first = inbox.items.find((item) => item.mode === "steer" || item.mode === "followUp");
const input = first === void 0 ? incoming : inputDraft(first.content, first.entry, now);
const entries2 = input !== void 0 && (at === "final" || hasQueuedReset(inbox.items)) ? await tx.beforeInput(conversationId, input) : [];
return {
entries: entries2,
conversationId,
inbox,
steeringMode: modes.steeringMode,
@@ -34442,6 +34453,9 @@ async function applyBoundary(tx, boundary, at, now) {
}
const placed = [];
for (const index of users) {
if (placed.length === 0)
for (const draft of boundary.entries)
await tx.appendEntry(conversationId, draft);
const item = items[index];
const entry2 = await appendInput(tx, conversationId, item.content, item.entry, now);
tx.placeSubmission(item.id, entry2.id);
@@ -34454,10 +34468,11 @@ async function applyBoundary(tx, boundary, at, now) {
return { users: placed, first, reset };
}
function appendInput(tx, conversationId, content, entry2, now) {
return tx.appendEntry(conversationId, inputDraft(content, entry2, now));
}
function inputDraft(content, entry2, now) {
const message = { role: "user", content, timestamp: now };
if (entry2 === void 0)
return tx.appendEntry(UserEntry, conversationId, { model: [message] });
return tx.appendEntry(conversationId, { ...pickInputEntry(entry2), model: [message] });
return { ...entry2 === void 0 ? { kind: UserEntry.kind } : pickInputEntry(entry2), input: true, model: [message] };
}
function pickInputEntry({ kind, data }) {
const picked = data === void 0 ? { kind } : { kind, data };
@@ -35177,6 +35192,7 @@ var TaskScheduler = class {
#unsubscribeRegistry = () => {
};
#enabled = false;
#held = false;
#readOnly;
#closing = false;
#dirty = false;
@@ -35205,6 +35221,14 @@ var TaskScheduler = class {
this.#context = options.context;
this.#readOnly = options.readOnly === true;
}
/** Start no run invocation until `release()`; running invocations and abort handlers go on. Overrides `resume()`. */
hold() {
this.#held = true;
}
release() {
this.#held = false;
this.#kick();
}
/**
* Load live tasks and change surviving `running` tasks back to `pending`, counting the restart. Dispatches nothing;
* a task cut off `settings.restartLimit` times ends when it is next reserved. Read-only, it only loads them: a
@@ -35394,13 +35418,13 @@ var TaskScheduler = class {
return marked.result;
}
async waitForTask(id, context) {
const found = await this.#session.readOnLine(async (storage) => {
const found = await this.#session.readOnLine(async (storage2) => {
if (this.#closing)
throw closedError(this.#session);
if (await this.#region(() => this.#liveRecord(id) !== void 0)) {
return { promise: this.#taskWaiters.add(id, context) };
}
const record = await storage.task(id, context);
const record = await storage2.task(id, context);
if (record === void 0)
throw new Error(`Task ${id} does not exist`);
return { promise: Promise.resolve(record) };
@@ -36659,6 +36683,8 @@ var TaskScheduler = class {
for (const record of plan.ready) {
const id = record.id;
const mode = record.abortRequested ? "abort" : "run";
if (mode === "run" && this.#held)
continue;
const cutOff = record.restarts !== void 0 && record.restarts >= this.#restartLimit();
if (cutOff && (mode === "abort" || !this.#limitsRestarts(record.kind))) {
const message = mode === "abort" ? `Abort handler of task ${id} (${record.kind}) was cut off ${record.restarts} times` : `Task ${id} (${record.kind}) was cut off ${record.restarts} times without progress`;
@@ -36844,7 +36870,7 @@ var TaskScheduler = class {
const tasks = [];
for (const record of this.#live.values())
tasks.push({ record, state: this.#inspectTask(record, snapshot) });
const scheduling = this.#closing ? "closing" : this.#enabled ? "running" : "paused";
const scheduling = this.#closing ? "closing" : this.#enabled && !this.#held ? "running" : "paused";
return { scheduling, tasks };
}
#inspectTask(record, snapshot) {
@@ -36999,6 +37025,10 @@ var TaskScheduler = class {
* invocation continues.
*/
#progress(tx, current, state2, extension, context) {
if (this.#held) {
tx.setTask(withState(current, { status: "pending", checkpoint: current.state.checkpoint }));
return false;
}
state2.snapshot = this.#registry.snapshot();
const next = state2.snapshot.task(current.kind);
if (next === state2.task)
@@ -37366,7 +37396,7 @@ var TaskScheduler = class {
watchDoc: ((...args) => this.#watchDoc(invocation, args)),
snapshot: ((...args) => this.#read(invocation, () => sessionMethod(this.#session, "snapshot")(...args))),
snapshotAsOf: ((...args) => this.#read(invocation, () => sessionMethod(this.#session, "snapshotAsOf")(...args))),
getTask: ((id, context) => this.#read(invocation, () => this.#session.readOnLine((storage) => storage.task(id, context)))),
getTask: ((id, context) => this.#read(invocation, () => this.#session.readOnLine((storage2) => storage2.task(id, context)))),
waitForTask: ((id, context) => this.#read(invocation, () => this.waitForTask(id, withAbortSignal(invocation.controller.signal, context)))),
ownedTasks: (context) => this.#read(invocation, () => this.#session.readOnLine(async () => {
if (invocation.ended)
@@ -37386,7 +37416,7 @@ var TaskScheduler = class {
})),
abortOwned: (id, context) => this.#read(invocation, async () => {
const bound = withAbortSignal(invocation.controller.signal, context);
const record = await this.#session.readOnLine((storage) => storage.task(id, bound));
const record = await this.#session.readOnLine((storage2) => storage2.task(id, bound));
if (record?.owner !== invocation.taskId) {
throw new Error(`Task ${id} is not owned by task ${invocation.taskId}`);
}
@@ -37395,10 +37425,10 @@ var TaskScheduler = class {
await this.abort(id, bound, true);
await this.waitForTask(id, bound);
}),
outcomes: ((ids, context) => this.#read(invocation, () => this.#session.readOnLine(async (storage) => {
outcomes: ((ids, context) => this.#read(invocation, () => this.#session.readOnLine(async (storage2) => {
const outcomes = [];
for (const id of ids) {
const state2 = (await storage.task(id, context))?.state;
const state2 = (await storage2.task(id, context))?.state;
if (state2?.status !== "terminal")
throw new Error(`Task ${id} is not terminal`);
outcomes.push(state2.outcome);
@@ -37416,11 +37446,11 @@ var TaskScheduler = class {
const [token, id, context] = args.length === 2 ? [void 0, args[0], args[1]] : [args[0], args[1], args[2]];
return this.#read(invocation, async () => {
const kept = this.#contexts.get(invocation.conversationId);
const entry2 = (kept === void 0 ? void 0 : rangeEntry(kept.range, id)) ?? (await this.#session.readOnLine((storage) => storage.entry(invocation.conversationId, id, context)))?.entry;
const entry2 = (kept === void 0 ? void 0 : rangeEntry(kept.range, id)) ?? (await this.#session.readOnLine((storage2) => storage2.entry(invocation.conversationId, id, context)))?.entry;
return token === void 0 || entry2?.kind === token.kind ? entry2 : void 0;
});
}),
entries: (query, limit2, cursor, context) => this.#read(invocation, () => this.#session.readOnLine((storage) => storage.scanEntries(conversationEntries(invocation.conversationId, query), limit2, cursor, context))),
entries: (query, limit2, cursor, context) => this.#read(invocation, () => this.#session.readOnLine((storage2) => storage2.scanEntries(conversationEntries(invocation.conversationId, query), limit2, cursor, context))),
context: (conversationId, context, options) => this.#read(invocation, async () => {
const read2 = await readContextFrom(this.#session, this.#storage, conversationId, context, options?.at, this.#contexts.get(conversationId)?.range);
const { view: view2 } = read2;
@@ -39732,7 +39762,7 @@ async function prepareFailure(tx, runtime) {
const policy = settings.afterFailedRun;
if (policy !== "continue")
return { policy };
return { policy, boundary: await prepareBoundary(tx, runtime.conversationId, settings) };
return { policy, boundary: await prepareBoundary(tx, runtime.conversationId, settings, "final", runtime.now()) };
}
async function endFailedRun(tx, runtime, live, failure, end) {
const conversationId = runtime.conversationId;
@@ -39900,7 +39930,7 @@ async function answer(runtime, message, context) {
const afterRun = await asksAfterRun(runtime, context);
const conversationId = runtime.conversationId;
await runtime.commit(async (tx) => {
const boundary = await prepareBoundary(tx, conversationId, runtime.settings);
const boundary = await prepareBoundary(tx, conversationId, runtime.settings, "final", runtime.now());
const live = await tx.doc(LiveDoc, conversationId);
const entry2 = await appendAssistant(tx, conversationId, message);
const result = { status: "terminal", outcome: { status: "completed", result: { entryId: entry2.id } } };
@@ -39973,7 +40003,7 @@ function prepareRunEndTasks(runtime, requests, prepared) {
async function endAnsweredRun(runtime, answer2, context) {
const conversationId = runtime.conversationId;
await runtime.commit(async (tx) => {
const boundary = await prepareBoundary(tx, conversationId, runtime.settings);
const boundary = await prepareBoundary(tx, conversationId, runtime.settings, "final", runtime.now());
const live = await tx.doc(LiveDoc, conversationId);
const { users, first } = await applyBoundary(tx, boundary, "final", runtime.now());
endRun(tx, live, runtime.taskId, { status: "done", answer: answer2 });
@@ -40046,7 +40076,7 @@ async function finishToolRound(runtime, assistant, tools, context) {
const maxTurns = (await runtime.agent(context)).maxTurns;
const afterRun = terminate && await asksAfterRun(runtime, context);
await runtime.commit(async (tx) => {
const boundary = await prepareBoundary(tx, conversationId, runtime.settings);
const boundary = await prepareBoundary(tx, conversationId, runtime.settings, terminate || handoff !== void 0 ? "final" : "postTools", runtime.now());
if (added.length > 0)
await addTools(tx, conversationId, added);
const live = await tx.doc(LiveDoc, conversationId);
@@ -40354,19 +40384,19 @@ var Submissions = class {
}
/** Handle for an existing submission, or `undefined`. */
async get(id, context) {
const record = await this.#session.readOnLine((storage) => storage.submission(id, context));
const record = await this.#session.readOnLine((storage2) => storage2.submission(id, context));
return record === void 0 ? void 0 : new SubmissionHandle(record.id, this);
}
async status(id, context) {
const record = await this.#session.readOnLine((storage) => storage.submission(id, context));
const record = await this.#session.readOnLine((storage2) => storage2.submission(id, context));
if (record === void 0)
throw new Error(`Submission ${id} does not exist`);
return record;
}
async wait(id, context) {
this.#resume();
const found = await this.#session.readOnLine(async (storage) => {
const record = await storage.submission(id, context);
const found = await this.#session.readOnLine(async (storage2) => {
const record = await storage2.submission(id, context);
if (record === void 0)
throw new Error(`Submission ${id} does not exist`);
if (isSettled(record))
@@ -40463,13 +40493,13 @@ async function admitSubmission(tx, conversationId, draft, now, queueModes, span)
if (draft.type === "continue") {
if (busy)
throw new ConversationBusy(conversationId);
const boundary2 = await prepareBoundary(tx, conversationId, queueModes);
const boundary2 = await prepareBoundary(tx, conversationId, queueModes, "final", now);
const { id: id2 } = await tx.createSubmission({ ...fields, type: "continue", status: "placed" });
const { users, first } = await applyBoundary(tx, boundary2, "final", now);
await startRun(tx, conversationId, live, { inputs: [...users, id2], first: live.runFailure?.first ?? first });
return { id: id2, status: "placed" };
}
const boundary = busy ? void 0 : await prepareBoundary(tx, conversationId, queueModes);
const boundary = busy ? void 0 : await prepareBoundary(tx, conversationId, queueModes, "final", now, draft.type === "input" ? inputDraft(draft.content, draft.entry, now) : void 0);
if (boundary === void 0 || boundary.inbox.items.length > 0) {
const { id: id2 } = await tx.createSubmission({
...fields,
@@ -40507,6 +40537,8 @@ async function admitSubmission(tx, conversationId, draft, now, queueModes, span)
const write = { ...fields, type: "write", status: "done", entry: entry3.id };
return { id: (await tx.createSubmission(write)).id, status: "done" };
}
for (const before of boundary.entries)
await tx.appendEntry(conversationId, before);
const entry2 = await appendInput(tx, conversationId, draft.content, draft.entry, now);
const input = { ...fields, type: "input", status: "placed", entry: entry2.id };
const { id } = await tx.createSubmission(input);
@@ -41037,9 +41069,9 @@ var TaskGraphView = class {
}
/** Register an observer created from the current revision, atomically on the Session line. */
#attach(create, context) {
return this.#session.readOnLine(async (storage) => {
return this.#session.readOnLine(async (storage2) => {
const mount = this.#mount ?? {
...await this.#build(storage, context),
...await this.#build(storage2, context),
observers: /* @__PURE__ */ new Set()
};
const detach = () => {
@@ -41076,16 +41108,16 @@ var TaskGraphView = class {
});
}
/** Read the graph on the line, through the line's `storage`. */
async #build(storage, context) {
async #build(storage2, context) {
const tasks = {};
const live = /* @__PURE__ */ new Map();
const records = [];
for (const status of LIVE_STATUSES2) {
records.push(...await scanAll((cursor) => storage.scanTasks({ status }, SCAN_PAGE_SIZE4, cursor, context)));
records.push(...await scanAll((cursor) => storage2.scanTasks({ status }, SCAN_PAGE_SIZE4, cursor, context)));
}
records.sort((a, b) => a.id - b.id);
for (const record of records) {
const owned = await scanAll((cursor) => storage.scanConversations({ ownerTaskId: record.id }, SCAN_PAGE_SIZE4, cursor, context));
const owned = await scanAll((cursor) => storage2.scanConversations({ ownerTaskId: record.id }, SCAN_PAGE_SIZE4, cursor, context));
const conversations = owned.map((conversation) => conversation.id).sort((a, b) => a - b);
const subject = blockedSubject(record);
tasks[String(record.id)] = nodeOf2(record, conversations, this.#blocking.blocked(subject));
@@ -41262,9 +41294,9 @@ var ConversationViews = class {
*/
async attach(id, window2, create, context) {
const bounds = resolveWindow(window2);
return this.#session.readOnLine(async (storage) => {
const mount = this.#mounts.get(id) ?? await this.#build(storage, id, context);
const { entries: entries2, before } = await loadWindow(storage, id, bounds, context);
return this.#session.readOnLine(async (storage2) => {
const mount = this.#mounts.get(id) ?? await this.#build(storage2, id, context);
const { entries: entries2, before } = await loadWindow(storage2, id, bounds, context);
const value2 = view(mount.conversation, entries2, before, mount.docs);
let windowed;
const detach = () => {
@@ -41284,8 +41316,8 @@ var ConversationViews = class {
});
}
/** Hydrate a mount on the line, through the line's `storage`. */
async #build(storage, id, context) {
const conversation = await storage.conversation(id, context);
async #build(storage2, id, context) {
const conversation = await storage2.conversation(id, context);
if (conversation === void 0)
throw new Error(`Conversation ${id} does not exist`);
const docs = {};
@@ -41303,25 +41335,25 @@ var ConversationViews = class {
function view(conversation, entries2, before, docs) {
return before === void 0 ? { conversation, entries: entries2, docs } : { conversation, entries: entries2, before, docs };
}
async function loadWindow(storage, conversationId, bounds, context) {
async function loadWindow(storage2, conversationId, bounds, context) {
const { from, min, max } = bounds;
if (from !== void 0) {
const since = await scanUpTo(storage, { conversationId, minEntryId: from, order: "ascending" }, max + 1, context);
const since = await scanUpTo(storage2, { conversationId, minEntryId: from, order: "ascending" }, max + 1, context);
if (since.length <= max) {
const query = { conversationId, maxEntryId: idFromNumber(from - 1) };
const previous = (await storage.scanEntries(query, 1, void 0, context)).items[0];
const previous = (await storage2.scanEntries(query, 1, void 0, context)).items[0];
return { entries: since, before: previous?.id };
}
}
const newest = await scanUpTo(storage, { conversationId }, min + 1, context);
const newest = await scanUpTo(storage2, { conversationId }, min + 1, context);
const before = newest.length > min ? newest.pop().id : void 0;
return { entries: newest.reverse(), before };
}
async function scanUpTo(storage, query, limit2, context) {
async function scanUpTo(storage2, query, limit2, context) {
const entries2 = [];
let cursor;
do {
const page2 = await storage.scanEntries(query, Math.min(SCAN_PAGE_SIZE5, limit2 - entries2.length), cursor, context);
const page2 = await storage2.scanEntries(query, Math.min(SCAN_PAGE_SIZE5, limit2 - entries2.length), cursor, context);
entries2.push(...page2.items);
cursor = page2.next;
} while (cursor !== void 0 && entries2.length < limit2);
@@ -41441,7 +41473,7 @@ var ConversationImpl = class {
}
entries(query, limit2, cursor, context) {
const scan = conversationEntries(this.id, query);
return this.#host.harness.readOnLine((storage) => storage.scanEntries(scan, limit2, cursor, context));
return this.#host.harness.readOnLine((storage2) => storage2.scanEntries(scan, limit2, cursor, context));
}
fork(at, options, context) {
return this.#host.create({ kind: "fork", parentId: this.id, at, ownership: options.ownership }, options, context);
@@ -41482,7 +41514,7 @@ var HarnessImpl = class extends SessionImpl {
const now = safeNow(options.now, report2);
const hostSettings = currentSettings(options.settings);
super(rawStorage, now, () => hostSettings()?.documentRetentionMs, options.setTimer, options.readOnly === true, () => hostSettings()?.progress?.persistWithinMs);
const storage = this.storage;
const storage2 = this.storage;
this.#options = options;
this.#report = report2;
this.#hostSettings = hostSettings;
@@ -41490,7 +41522,7 @@ var HarnessImpl = class extends SessionImpl {
this.#tasks = new TaskScheduler({
telemetry: options.telemetry,
session: this,
storage,
storage: storage2,
registry: options.registry,
models: currentModels(options.models, options.identity),
agent: (id, snapshot, callContext) => this.resolveAgent(id, snapshot, callContext),
@@ -41518,7 +41550,7 @@ var HarnessImpl = class extends SessionImpl {
});
this.#host = {
harness: this,
storage,
storage: storage2,
tasks: this.#tasks,
submissions: this.#submissions,
views: new ConversationViews(this),
@@ -41526,6 +41558,15 @@ var HarnessImpl = class extends SessionImpl {
create: (target, createOptions, context2) => this.#create(target, createOptions, context2)
};
}
async beforeInput(tx, conversationId, input, context) {
const agent = resolveAgent(await tx.doc(AgentDoc, conversationId), this.#options.registry.snapshot(), resolveSettings(this.#hostSettings()), (error, extension) => this.#report(error, { kind: "extension_error", conversationId, extension }, context));
const entries2 = [];
for (const { handlers } of agentHooks(agent, "pi.generation")) {
const result = await handlers.beforeInput?.(input, { conversationId, tx }, context);
entries2.push(...result?.entries ?? []);
}
return entries2;
}
/** Resolve a conversation's committed `pi.agent` against `snapshot`, or the current one, and the current settings. */
async resolveAgent(id, snapshot, context) {
const registry2 = snapshot ?? this.#options.registry.snapshot();
@@ -41548,12 +41589,18 @@ var HarnessImpl = class extends SessionImpl {
this.assertUsable();
this.#tasks.resume();
}
hold() {
this.#tasks.hold();
}
release() {
this.#tasks.release();
}
wake() {
this.assertUsable();
return this.#tasks.wake();
}
getTask(id, context) {
return this.readOnLine((storage) => storage.task(id, context));
return this.readOnLine((storage2) => storage2.task(id, context));
}
/** Internal, for tests: sizes of the scheduler's ownership indexes. */
get [SCHEDULER_INDEX_SIZES]() {
@@ -41564,9 +41611,9 @@ var HarnessImpl = class extends SessionImpl {
return this.#tasks.idleState;
}
inspect(context) {
return this.readOnLine(async (storage) => {
return this.readOnLine(async (storage2) => {
const { scheduling, tasks } = await this.#tasks.inspect(this.#options.registry.snapshot());
const scan = (status) => scanAll((cursor) => storage.scanSubmissions({ status }, SCAN_PAGE_SIZE6, cursor, context));
const scan = (status) => scanAll((cursor) => storage2.scanSubmissions({ status }, SCAN_PAGE_SIZE6, cursor, context));
const submissions = [...await scan("queued"), ...await scan("placed")].sort((a, b) => a.id - b.id);
return { scheduling, tasks, submissions };
});
@@ -41590,7 +41637,7 @@ var HarnessImpl = class extends SessionImpl {
}
/** Sum every conversation's committed `pi.usage`. Each document is read at its own point; totals only grow. */
async usage(context) {
const conversations = await this.readOnLine((storage) => scanAll((cursor) => storage.scanConversations({}, SCAN_PAGE_SIZE6, cursor, context)));
const conversations = await this.readOnLine((storage2) => scanAll((cursor) => storage2.scanConversations({}, SCAN_PAGE_SIZE6, cursor, context)));
const total = UsageDoc.definition.initial();
for (const { id } of conversations) {
const state2 = await this.snapshot(UsageDoc, id, context);
@@ -41615,7 +41662,7 @@ var HarnessImpl = class extends SessionImpl {
}
async conversation(id, context) {
this.assertUsable();
const record = await this.readOnLine((storage) => storage.conversation(id, context));
const record = await this.readOnLine((storage2) => storage2.conversation(id, context));
return record === void 0 ? void 0 : new ConversationImpl(record.id, this.#host);
}
createConversation(options, context) {
@@ -41746,7 +41793,7 @@ function boundConversation(id, binding, submissions, tasks) {
}
var Harness = {
/** Open a Harness over storage. The registry may keep changing while the Harness runs. */
async open(storage, options, context) {
async open(storage2, options, context) {
context.abortSignal?.throwIfAborted();
const snapshot = options.registry.snapshot();
const missing = BUILTIN_TASKS.filter((task) => snapshot.task(task.definition.name) === void 0);
@@ -41754,7 +41801,7 @@ var Harness = {
const names2 = missing.map((task) => task.definition.name).join(", ");
throw new Error(`Registry lacks built-in tasks ${names2}; create it with createRegistry()`);
}
const harness2 = new HarnessImpl(storage, options, context);
const harness2 = new HarnessImpl(storage2, options, context);
try {
await harness2.openTasks(context);
} catch (error) {
@@ -44482,6 +44529,29 @@ async function startRequests(harness2, context) {
};
}
// input.ts
async function serve(harness2, context, afterSubmit) {
while (true) {
const input = await Deno.core.ops.op_input();
if (input === null || input.type === "stop") {
await harness2.waitForIdle(context);
return;
}
const conversation = await harness2.conversation(input.conversation, context);
if (!conversation) throw new Error(`Unknown conversation: ${input.conversation}`);
const submission2 = await conversation.submit({
type: "input",
content: input.content,
requestId: input.requestId
}, context);
if (afterSubmit) {
const result = await submission2.wait(context);
await afterSubmit();
if (result.status !== "done") throw new Error(`Run ended: ${JSON.stringify(result)}`);
}
}
}
// main.ts
var proxy = Deno.core.ops.op_model_proxy();
if (!proxy?.base_url || !proxy?.api_key) throw new Error("The llm-proxy credential is required");
@@ -44516,9 +44586,11 @@ models.setProvider({
var registry = createRegistry();
registry.install(CodingTools);
registry.install(CredentialRequests);
var harness = await Harness.open(await openStorage(globalThis.storagePath), {
var storage = await openStorage(globalThis.storagePath);
var harness = await Harness.open(storage, {
models,
registry,
settings: { followUpMode: "all" },
env: (target) => environment(target.cwd ?? "/", target.conversationId)
}, BACKGROUND_CONTEXT);
var root = await harness.root(BACKGROUND_CONTEXT, { agent: { model: { provider: "openai", modelId: model2.id }, thinkingLevel: "low", cwd: "/", extensions: [CodingTools, CredentialRequests] } });
@@ -44543,25 +44615,21 @@ var render = (view2) => {
for (const entry2 of watch.value.entries) {
if (entry2.kind === "pi.assistant") emitted.set(entry2.id, text(entry2.model?.[0]));
}
watch.start((event) => {
if (globalThis.consoleEnabled) watch.start((event) => {
if (event.kind === "value") render(event.value);
});
var stopRequests = await startRequests(harness, BACKGROUND_CONTEXT);
harness.resume();
await harness.waitForIdle(BACKGROUND_CONTEXT);
while (true) {
const line = await Deno.core.ops.op_input();
if (line === null) break;
if (!line.trim()) continue;
const submission2 = await root.submit({ type: "input", content: line, requestId: crypto.randomUUID() }, BACKGROUND_CONTEXT);
const result = await submission2.wait(BACKGROUND_CONTEXT);
await serve(harness, BACKGROUND_CONTEXT, globalThis.consoleEnabled ? async () => {
await harness.waitForIdle(BACKGROUND_CONTEXT);
const final = await root.watch(BACKGROUND_CONTEXT);
render(final.value);
await final.stop();
Deno.core.ops.op_output("\n");
if (result.status !== "done") throw new Error(`Run ended: ${JSON.stringify(result)}`);
}
} : void 0);
await stopRequests();
await watch.stop();
await harness.close(BACKGROUND_CONTEXT);
export {
storage
};