388 lines
24 KiB
Diff
388 lines
24 KiB
Diff
diff --git a/dist/harness/generation.js b/dist/harness/generation.js
|
||
index ac42dfe741b20726ab8eb3d8139b1296d664210d..1e7f1ac26d17b83cb15bf00408fa70b0aee4e747 100644
|
||
--- a/dist/harness/generation.js
|
||
+++ b/dist/harness/generation.js
|
||
@@ -458,7 +458,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()) };
|
||
}
|
||
/**
|
||
* End a failed run, settling its inputs with `settlement`, and do with the inputs queued behind it what
|
||
@@ -668,7 +668,7 @@ async function answer(runtime, message, context) {
|
||
const conversationId = runtime.conversationId;
|
||
await runtime.commit(async (tx) => {
|
||
// Queue modes are read on the Session line, when the boundary is decided.
|
||
- 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 entry = await appendAssistant(tx, conversationId, message);
|
||
const result = { status: "terminal", outcome: { status: "completed", result: { entryId: entry.id } } };
|
||
@@ -762,7 +762,7 @@ function prepareRunEndTasks(runtime, requests, prepared) {
|
||
async function endAnsweredRun(runtime, answer, 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 });
|
||
@@ -868,7 +868,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 !== undefined ? "final" : "postTools", runtime.now());
|
||
if (added.length > 0)
|
||
await addTools(tx, conversationId, added);
|
||
const live = await tx.doc(LiveDoc, conversationId);
|
||
diff --git a/dist/harness/harness.js b/dist/harness/harness.js
|
||
index e286f00f872ebec79367a594938040c6a37a879f..44979b05c2bcf30580841797175c099c8c621462 100644
|
||
--- a/dist/harness/harness.js
|
||
+++ b/dist/harness/harness.js
|
||
@@ -5,7 +5,7 @@ import { ResetEntry } from "../entries.js";
|
||
import { contained, SessionImpl } from "../session/session.js";
|
||
import { hostTimers } from "../timers.js";
|
||
import { ROOT_CONVERSATION_ID } from "../types.js";
|
||
-import { AgentDoc, configure, createAgent, resolveAgent, resolveSettings } from "./agent.js";
|
||
+import { AgentDoc, agentHooks, configure, createAgent, resolveAgent, resolveSettings } from "./agent.js";
|
||
import { createCompaction } from "./compaction.js";
|
||
import { readContext } from "./context.js";
|
||
import { InboxDoc, withdrawQueuedInputs } from "./inbox.js";
|
||
@@ -157,6 +157,15 @@ class HarnessImpl extends SessionImpl {
|
||
create: (target, createOptions, context) => this.#create(target, createOptions, context),
|
||
};
|
||
}
|
||
+ 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 entries = [];
|
||
+ for (const { handlers } of agentHooks(agent, "pi.generation")) {
|
||
+ const result = await handlers.beforeInput?.(input, { conversationId, tx }, context);
|
||
+ entries.push(...(result?.entries ?? []));
|
||
+ }
|
||
+ return entries;
|
||
+ }
|
||
/** Resolve a conversation's committed `pi.agent` against `snapshot`, or the current one, and the current settings. */
|
||
async resolveAgent(id, snapshot, context) {
|
||
const registry = snapshot ?? this.#options.registry.snapshot();
|
||
@@ -179,6 +188,12 @@ class HarnessImpl extends SessionImpl {
|
||
this.assertUsable();
|
||
this.#tasks.resume();
|
||
}
|
||
+ hold() {
|
||
+ this.#tasks.hold();
|
||
+ }
|
||
+ release() {
|
||
+ this.#tasks.release();
|
||
+ }
|
||
wake() {
|
||
this.assertUsable();
|
||
return this.#tasks.wake();
|
||
diff --git a/dist/harness/inbox.d.ts b/dist/harness/inbox.d.ts
|
||
index 1197339369188857a1ad62b528cea65e3833f27e..a61d9712ce55e80441847442772da6f3e354fb2b 100644
|
||
--- a/dist/harness/inbox.d.ts
|
||
+++ b/dist/harness/inbox.d.ts
|
||
@@ -23,6 +23,7 @@ export declare const InboxDoc: import("../types.ts").ConversationDocToken<InboxS
|
||
export type QueueModes = Pick<Settings, "steeringMode" | "followUpMode">;
|
||
/** What a boundary reads before the commit's first table write, and the newest head it has seen so far. */
|
||
export type Boundary = {
|
||
+ readonly entries: readonly EntryDraft[];
|
||
readonly conversationId: ConversationId;
|
||
readonly inbox: Draft<InboxState>;
|
||
readonly steeringMode: QueueMode;
|
||
@@ -43,7 +44,7 @@ export type BoundaryResult = {
|
||
* Read what a boundary needs. Table reads must precede the commit's first table write, so callers prepare the
|
||
* boundary at the start of their commit.
|
||
*/
|
||
-export declare function prepareBoundary(tx: Tx, conversationId: ConversationId, modes: QueueModes): Promise<Boundary>;
|
||
+export declare function prepareBoundary(tx: Tx, conversationId: ConversationId, modes: QueueModes, at: "postTools" | "final", now: number, incoming?: EntryDraft): Promise<Boundary>;
|
||
/**
|
||
* Place the queued items a boundary selects (spec §6): every write, the first or all steers, and at `final` the first
|
||
* or all follow-ups. A selected reset turns a `postTools` boundary into `final`. Writes are placed first and user
|
||
@@ -53,6 +54,7 @@ export declare function prepareBoundary(tx: Tx, conversationId: ConversationId,
|
||
*/
|
||
export declare function applyBoundary(tx: Tx, boundary: Boundary, at: "postTools" | "final", now: number): Promise<BoundaryResult>;
|
||
/** Append the entry an input is placed as: `entry`'s kind and data, or `pi.user`, with the user message of `content`. */
|
||
+export declare function inputDraft(content: UserInput, entry: InputEntry | undefined, now: number): EntryDraft;
|
||
export declare function appendInput(tx: Tx, conversationId: ConversationId, content: UserInput, entry: InputEntry | undefined, now: number): Promise<EntryRecord>;
|
||
/** The kind and data of an input's entry as strict JSON, without other fields an untyped caller might pass. */
|
||
export declare function pickInputEntry({ kind, data }: InputEntry): JsonRepresentation<InputEntry>;
|
||
diff --git a/dist/harness/inbox.js b/dist/harness/inbox.js
|
||
index 5ff8c1703808c5ffd703164e6c339150e1f1e131..ec4cf6d5adc599e5fe428514a7cd7e0dac1a7c9d 100644
|
||
--- a/dist/harness/inbox.js
|
||
+++ b/dist/harness/inbox.js
|
||
@@ -14,10 +14,15 @@ export const InboxDoc = defineDoc({
|
||
* Read what a boundary needs. Table reads must precede the commit's first table write, so callers prepare the
|
||
* boundary at the start of their commit.
|
||
*/
|
||
-export async function prepareBoundary(tx, conversationId, modes) {
|
||
+export 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 === undefined ? incoming : inputDraft(first.content, first.entry, now);
|
||
+ const entries = input !== undefined && (at === "final" || hasQueuedReset(inbox.items))
|
||
+ ? await tx.beforeInput(conversationId, input) : [];
|
||
return {
|
||
+ entries,
|
||
conversationId,
|
||
inbox,
|
||
steeringMode: modes.steeringMode,
|
||
@@ -61,6 +66,9 @@ export 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 entry = await appendInput(tx, conversationId, item.content, item.entry, now);
|
||
tx.placeSubmission(item.id, entry.id);
|
||
@@ -74,10 +82,11 @@ export async function applyBoundary(tx, boundary, at, now) {
|
||
}
|
||
/** Append the entry an input is placed as: `entry`'s kind and data, or `pi.user`, with the user message of `content`. */
|
||
export function appendInput(tx, conversationId, content, entry, now) {
|
||
+ return tx.appendEntry(conversationId, inputDraft(content, entry, now));
|
||
+}
|
||
+export function inputDraft(content, entry, now) {
|
||
const message = { role: "user", content, timestamp: now };
|
||
- if (entry === undefined)
|
||
- return tx.appendEntry(UserEntry, conversationId, { model: [message] });
|
||
- return tx.appendEntry(conversationId, { ...pickInputEntry(entry), model: [message] });
|
||
+ return { ...(entry === undefined ? { kind: UserEntry.kind } : pickInputEntry(entry)), input: true, model: [message] };
|
||
}
|
||
/** The kind and data of an input's entry as strict JSON, without other fields an untyped caller might pass. */
|
||
export function pickInputEntry({ kind, data }) {
|
||
diff --git a/dist/harness/scheduler.d.ts b/dist/harness/scheduler.d.ts
|
||
index f6189edb71ac0557060b623625fc4be4bec5e904..269a3c4ab1676da22db5a723f30d2974f84b6bea 100644
|
||
--- a/dist/harness/scheduler.d.ts
|
||
+++ b/dist/harness/scheduler.d.ts
|
||
@@ -107,6 +107,8 @@ export declare class TaskScheduler {
|
||
* Idempotent; the kick does nothing once closing.
|
||
*/
|
||
resume(): void;
|
||
+ hold(): void;
|
||
+ release(): void;
|
||
/**
|
||
* Why a live task cannot run here, if it cannot: its definition is missing, too old, or failed to migrate. A running
|
||
* task, an abort-marked one, and one holding its outcome are never blocked.
|
||
diff --git a/dist/harness/scheduler.js b/dist/harness/scheduler.js
|
||
index 365ea3ab42dd4406ab11a03d6702b238dfd0776b..3e8170fdb790e492e95a390fa6ea4693eb3d2f97 100644
|
||
--- a/dist/harness/scheduler.js
|
||
+++ b/dist/harness/scheduler.js
|
||
@@ -179,6 +179,7 @@ export class TaskScheduler {
|
||
#cascadePending = false;
|
||
#unsubscribeRegistry = () => { };
|
||
#enabled = false;
|
||
+ #held = false;
|
||
#readOnly;
|
||
#closing = false;
|
||
#dirty = false;
|
||
@@ -207,6 +208,14 @@ export class TaskScheduler {
|
||
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
|
||
@@ -1771,6 +1780,8 @@ export class TaskScheduler {
|
||
for (const record of plan.ready) {
|
||
const id = record.id;
|
||
const mode = record.abortRequested ? "abort" : "run";
|
||
+ if (mode === "run" && this.#held)
|
||
+ continue;
|
||
// Work cut off `restartLimit` times in a row would run forever: an abort handler, or a phase of a kind that
|
||
// does not end itself at the limit, ends the task `faulted` instead.
|
||
// The settings are read only for a task that was cut off at all.
|
||
@@ -1973,7 +1984,7 @@ export class TaskScheduler {
|
||
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) {
|
||
@@ -2151,6 +2162,11 @@ export class TaskScheduler {
|
||
* invocation continues.
|
||
*/
|
||
#progress(tx, current, state, extension, context) {
|
||
+ // Held: park at this phase boundary, as a handover does; reservation starts it again after `release()`.
|
||
+ if (this.#held) {
|
||
+ tx.setTask(withState(current, { status: "pending", checkpoint: current.state.checkpoint }));
|
||
+ return false;
|
||
+ }
|
||
state.snapshot = this.#registry.snapshot();
|
||
const next = state.snapshot.task(current.kind);
|
||
if (next === state.task)
|
||
diff --git a/dist/harness/submissions.js b/dist/harness/submissions.js
|
||
index 6e882a8c252dda942b8b45e5b1837cf2fee28341..89a85e8a52bf67044ff15c3c856a650dea4c7c27 100644
|
||
--- a/dist/harness/submissions.js
|
||
+++ b/dist/harness/submissions.js
|
||
@@ -3,7 +3,7 @@ import { withTelemetry } from "@earendil-works/chord/context";
|
||
import { createTypedSpanStarter, NOOP_TELEMETRY_CONTEXT, } from "@earendil-works/pi-telemetry";
|
||
import { ConversationBusy, ConversationIdle, ReadAfterWrite } from "../errors.js";
|
||
import { startRun } from "./generation.js";
|
||
-import { appendInput, applyBoundary, hasQueuedReset, InboxDoc, isDisplayOnly, isStale, pickInputEntry, prepareBoundary, removeInboxItem, } from "./inbox.js";
|
||
+import { appendInput, inputDraft, applyBoundary, hasQueuedReset, InboxDoc, isDisplayOnly, isStale, pickInputEntry, prepareBoundary, removeInboxItem, } from "./inbox.js";
|
||
import { LiveDoc } from "./live.js";
|
||
import { DURABLE_TELEMETRY_SCHEMA } from "./telemetry.js";
|
||
import { closedError, Waiters } from "./util.js";
|
||
@@ -205,7 +205,7 @@ export async function admitSubmission(tx, conversationId, draft, now, queueModes
|
||
// Never queued: whether a run can continue depends on how the current one ends. Rejected before any write.
|
||
if (busy)
|
||
throw new ConversationBusy(conversationId);
|
||
- const boundary = await prepareBoundary(tx, conversationId, queueModes);
|
||
+ const boundary = await prepareBoundary(tx, conversationId, queueModes, "final", now);
|
||
const { id } = await tx.createSubmission({ ...fields, type: "continue", status: "placed" });
|
||
// Items an earlier failed run left queued (`afterFailedRun: "keep"`) are placed first and join the run. Its history
|
||
// starts with the failed run's, so `afterRun` hooks see what that run did.
|
||
@@ -214,7 +214,7 @@ export async function admitSubmission(tx, conversationId, draft, now, queueModes
|
||
return { id, status: "placed" };
|
||
}
|
||
// A boundary reads the table, so it is prepared before the first table write; a busy one needs none.
|
||
- const boundary = busy ? undefined : await prepareBoundary(tx, conversationId, queueModes);
|
||
+ const boundary = busy ? undefined : await prepareBoundary(tx, conversationId, queueModes, "final", now, draft.type === "input" ? inputDraft(draft.content, draft.entry, now) : undefined);
|
||
if (boundary === undefined || boundary.inbox.items.length > 0) {
|
||
const { id } = await tx.createSubmission({
|
||
...fields,
|
||
@@ -253,6 +253,8 @@ export async function admitSubmission(tx, conversationId, draft, now, queueModes
|
||
const write = { ...fields, type: "write", status: "done", entry: entry.id };
|
||
return { id: (await tx.createSubmission(write)).id, status: "done" };
|
||
}
|
||
+ for (const before of boundary.entries)
|
||
+ await tx.appendEntry(conversationId, before);
|
||
const entry = await appendInput(tx, conversationId, draft.content, draft.entry, now);
|
||
const input = { ...fields, type: "input", status: "placed", entry: entry.id };
|
||
const { id } = await tx.createSubmission(input);
|
||
diff --git a/dist/harness/types.d.ts b/dist/harness/types.d.ts
|
||
index eb64e474162b158a43563e6c68d8ffea8c18b74a..cce695c6d87c79dadea54560bafcca62cf073ca1 100644
|
||
--- a/dist/harness/types.d.ts
|
||
+++ b/dist/harness/types.d.ts
|
||
@@ -816,6 +816,9 @@ export interface Harness extends Session {
|
||
* never do.
|
||
*/
|
||
resume(): void;
|
||
+ /** Start no task run until `release()`, whatever `resume()` says; running invocations and abort handlers go on. */
|
||
+ hold(): void;
|
||
+ release(): void;
|
||
/**
|
||
* When this Session next needs a running process, from the scheduler's memory: `active` while some task can run now
|
||
* or is running, `at` for the earliest sleep. Answers also while scheduling is paused. A host that wakes the
|
||
@@ -888,6 +891,8 @@ export interface HookApi extends DocumentReader {
|
||
export type HookResult<T> = T | undefined | Promise<T | undefined>;
|
||
/** Hooks of the built-in generation task. */
|
||
export interface GenerationHooks {
|
||
+ /** Before a new run’s first input is placed. Use only tx for storage access; effects commit with the input. */
|
||
+ beforeInput(input: EntryDraft, api: { readonly conversationId: ConversationId; readonly tx: Tx }, context: Context): HookResult<{ readonly entries?: readonly EntryDraft[] }>;
|
||
/** Before every request attempt, including recovery; the result is used for that request only. */
|
||
beforeRequest(request: {
|
||
readonly messages: readonly Message[];
|
||
diff --git a/dist/session/session.d.ts b/dist/session/session.d.ts
|
||
index 8201d7cfefb710516a3d4d89ba208adc2cbebd19..37c9f362401b5bc81f2f24233aa3dd7f57e29204 100644
|
||
--- a/dist/session/session.d.ts
|
||
+++ b/dist/session/session.d.ts
|
||
@@ -1,5 +1,5 @@
|
||
import { type Context, type JsonValue, type WatchEnd } from "@earendil-works/chord";
|
||
-import type { CommitChange, CommitObservation, CommitObserver, CommitOptions, ConversationDocFamilyToken, ConversationDocToken, ConversationId, ConversationRecord, DocumentCommitChange, DocumentRecord, DocumentWatch, EntryId, JsonObject, Observation, Observer, ReportInfo, RewindableConversationDocFamilyToken, RewindableConversationDocToken, Seq, Session, SessionDocFamilyToken, SessionDocToken, SessionEnd, SetTimer, Storage, StorageWrite, TaskDocFamilyToken, TaskDocToken, TaskId, Tx } from "../types.ts";
|
||
+import type { CommitChange, CommitObservation, CommitObserver, CommitOptions, ConversationDocFamilyToken, ConversationDocToken, EntryDraft, ConversationId, ConversationRecord, DocumentCommitChange, DocumentRecord, DocumentWatch, EntryId, JsonObject, Observation, Observer, ReportInfo, RewindableConversationDocFamilyToken, RewindableConversationDocToken, Seq, Session, SessionDocFamilyToken, SessionDocToken, SessionEnd, SetTimer, Storage, StorageWrite, TaskDocFamilyToken, TaskDocToken, TaskId, Tx } from "../types.ts";
|
||
import { type HeadMarker } from "./conversation-tails.ts";
|
||
import { Transaction, type TransactionScope } from "./transaction.ts";
|
||
/**
|
||
@@ -171,6 +171,7 @@ export declare class SessionImpl implements Session {
|
||
* listener. A plain Session drops them.
|
||
*/
|
||
report(_error: unknown, _info?: ReportInfo, _context?: Context): void;
|
||
+ protected beforeInput(tx: Transaction, id: ConversationId, input: EntryDraft, context: Context): Promise<readonly EntryDraft[]>;
|
||
/**
|
||
* 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.
|
||
diff --git a/dist/session/session.js b/dist/session/session.js
|
||
index e9ae9ebfc2c45765ccda141d7e60cdc9a15a0894..e1e5c69deed9f0d09cf67c26cdb903fe4593f9c6 100644
|
||
--- a/dist/session/session.js
|
||
+++ b/dist/session/session.js
|
||
@@ -224,6 +224,7 @@ export class SessionImpl {
|
||
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),
|
||
@@ -570,6 +571,9 @@ export class SessionImpl {
|
||
* listener. A plain Session drops them.
|
||
*/
|
||
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.
|
||
diff --git a/dist/session/transaction.d.ts b/dist/session/transaction.d.ts
|
||
index 5508fa2d021d2153f4f104b4ef69b99ef98a6a74..1c94a4a6e29b8371d49f7d6cb4d2e4e7cd6aec50 100644
|
||
--- a/dist/session/transaction.d.ts
|
||
+++ b/dist/session/transaction.d.ts
|
||
@@ -48,6 +48,7 @@ export interface TransactionHost {
|
||
install(document: LoadedDocument): void;
|
||
/** Remove a retired incarnation if it is still the cached occupant of its address. */
|
||
evict(addressId: string, recordId: DocumentId): void;
|
||
+ beforeInput(tx: Transaction, conversationId: ConversationId, input: EntryDraft, context: Context): Promise<readonly EntryDraft[]>;
|
||
/** Stage writes that belong to every newly created or forked conversation, in its creating transaction. */
|
||
conversationCreated(tx: Transaction, record: ConversationRecord): Promise<void>;
|
||
/** The newest entry ID visible from a conversation, known from the Session's commits. */
|
||
@@ -112,6 +113,7 @@ export declare class Transaction implements Tx {
|
||
forkConversation(parentConversationId: ConversationId, at: EntryId, options: {
|
||
readonly ownership: ConversationOwnership;
|
||
}): Promise<ConversationRecord>;
|
||
+ beforeInput(conversationId: ConversationId, input: EntryDraft): Promise<readonly EntryDraft[]>;
|
||
appendEntry(conversationId: ConversationId, value: EntryDraft): Promise<EntryRecord>;
|
||
appendEntry<D extends JsonValue>(token: Entry<D>, conversationId: ConversationId, value: TypedEntryDraft<NoInfer<D>>): Promise<TypedEntry<D>>;
|
||
createTask<I, S extends {
|
||
diff --git a/dist/session/transaction.js b/dist/session/transaction.js
|
||
index 7fbe75b11bb2a9ab28f4f23800143231172d71b7..a516853e2f9c4a4437dbc2bb8f1c8d2117386e9a 100644
|
||
--- a/dist/session/transaction.js
|
||
+++ b/dist/session/transaction.js
|
||
@@ -167,6 +167,9 @@ export class Transaction {
|
||
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);
|
||
diff --git a/dist/types.d.ts b/dist/types.d.ts
|
||
index b43143c5e7998b36034ffe9577f74f3d88ab932b..a9ca10ae41c48ad0ebb813e62caa3699dec5c51e 100644
|
||
--- a/dist/types.d.ts
|
||
+++ b/dist/types.d.ts
|
||
@@ -409,6 +409,8 @@ export type ContextEdit = {
|
||
});
|
||
/** Immutable transcript event with separate model-facing and application-facing payloads. */
|
||
export type EntryRecord = {
|
||
+ /** Placed by the harness from an input submission, not a write. */
|
||
+ readonly input?: true;
|
||
readonly id: EntryId;
|
||
readonly conversationId: ConversationId;
|
||
/** Application-defined entry discriminator. */
|
||
@@ -891,6 +893,8 @@ export type CommitPublication = {
|
||
* Session may freeze them (its caches and context ranges keep them), so mutating one can throw.
|
||
*/
|
||
export interface Tx {
|
||
+ /** Prepare entries before a new run’s first input; called before table writes, in the placement transaction. */
|
||
+ beforeInput(conversationId: ConversationId, input: EntryDraft): Promise<readonly EntryDraft[]>;
|
||
conversation(id: ConversationId): Promise<ConversationRecord | undefined>;
|
||
entry(id: EntryId): Promise<EntryRecord | undefined>;
|
||
/** Undefined when the entry is absent or has another kind. */
|