Carry durable input hooks and add a headless harness channel
This commit is contained in:
+162
-94
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user