pi-mono/packages/durable/test/harness-submit-in-commit.test.ts
Mario Zechner 6536f80129 feat(ai,agent,coding-agent): validate tool arguments with pi-schema, drop TypeBox
pi-schema: opt-in coercion, compile(schema, { coerce: true }) with coerce() and
parse(), the superset of TypeBox's Value.Convert rules and pi-ai's old coercion;
unresolvedRefs option; url and json-pointer-uri-fragment formats.
pi-ai: Tool takes any JSON Schema; validation through pi-schema; Type, Static
and TSchema are no longer exported (breaking). pi-agent-core and pi-durable:
tools on pi-schema, no TypeBox dependency. Coding agent: extensions keep Type,
Static, TSchema and StringEnum through an extension view of pi-ai; built-in
tools stay on TypeBox.
2026-11-01 10:08:42 +00:00

647 lines
28 KiB
TypeScript

// Admission for hosts that must not start runs: `whenIdle: "reject"`, and `submit(tx, draft)` inside the caller's
// commit (#11149).
import { withTelemetry } from "@earendil-works/chord/context";
import {
type AssistantMessage,
type FauxResponseStep,
fauxAssistantMessage,
fauxText,
fauxToolCall,
} from "@earendil-works/pi-ai";
import {
type Conversation,
ConversationBusy,
type ConversationHandle,
ConversationIdle,
defineDoc,
defineTask,
defineTool,
Harness,
InboxDoc,
LiveDoc,
MemoryStorage,
ReadAfterWrite,
type SubmissionId,
type Tx,
UserEntry,
} from "@earendil-works/pi-durable";
import { obj } from "@earendil-works/pi-schema";
import { InMemoryTelemetryContext } from "@earendil-works/pi-telemetry";
import { describe, expect, it } from "vitest";
import { type ChatSetup, chatSetup, openChat, unanswered, waitFor } from "./chat-support.ts";
import { addTask, addTool } from "./harness-support.ts";
import { ControlledStorage, context } from "./session-support.ts";
import { deferred } from "./task-support.ts";
const MODEL = { provider: "faux", modelId: "faux-1" } as const;
const failure = (): AssistantMessage =>
fauxAssistantMessage([], { stopReason: "error", errorMessage: "Invalid request" });
/** A host's own conversation document. */
const Sent = defineDoc<{ ids: number[] }>({
checkpointWhen: () => false,
kind: "test.sent",
version: 1,
scope: "conversation",
history: "latest",
fork: "initial",
initial: () => ({ ids: [] }),
});
/** Open a chat whose run hangs until aborted, and wait until it works on its first input. */
async function openBusy(storage = new ControlledStorage()) {
const setup = chatSetup();
const busy = unanswered();
setup.faux.setResponses([busy.step]);
const opened = await openChat(storage, setup);
const first = await opened.root.submit({ type: "input", content: "work" }, context);
await busy.reached;
return { ...opened, storage, first };
}
/** Open a chat whose Harness records telemetry. */
async function openTraced(setup: ChatSetup, telemetry: InMemoryTelemetryContext) {
const harness = await Harness.open(
new MemoryStorage(),
{ models: setup.models, registry: setup.registry, settings: setup.settings, telemetry },
context,
);
const root = await harness.root(context, { agent: { model: MODEL } });
return { harness, root };
}
/** Install tool `hold`, which reports its call and waits for `release`; its first call answers `tool`. */
function addHold(setup: ChatSetup) {
const reached = deferred();
const release = deferred();
addTool(
setup.registry,
defineTool({
name: "hold",
description: "Holds until released",
parameters: obj({}),
execute: async () => {
reached.resolve();
await release.promise;
return { output: [] };
},
}),
);
const call = fauxAssistantMessage([fauxToolCall("hold", {}, { id: "c1" })], { stopReason: "toolUse" });
return { reached, release, call };
}
/** Settled status of submission `id`, polled without asking for progress (`Submission.wait()` would). */
async function settledStatus(harness: Harness, id: SubmissionId) {
const submission = (await harness.submission(id, context))!;
await waitFor(async () => {
const { status } = await submission.status(context);
return status === "done" || status === "unanswered";
});
return submission.status(context);
}
describe("whenIdle reject", () => {
// #11149
it("rejects input to an idle conversation without writing", async () => {
const storage = new ControlledStorage();
const { harness, root } = await openChat(storage, chatSetup());
const commits = storage.commits.length;
const rejected = root.submit({ type: "input", content: "hi", whenBusy: "steer", whenIdle: "reject" }, context);
await expect(rejected).rejects.toBeInstanceOf(ConversationIdle);
await expect(rejected).rejects.toMatchObject({ conversationId: root.id });
expect(storage.commits.length).toBe(commits);
expect((await harness.snapshot(LiveDoc, root.id, context))?.run).toBeUndefined();
await harness.close(context);
});
// #11149
it("rejects input to an idle conversation that keeps queued items after a failed run", async () => {
const setup = chatSetup();
const failing = deferred();
const requested = deferred();
setup.faux.setResponses([
async () => {
requested.resolve();
await failing.promise;
return failure();
},
]);
const { harness, root } = await openChat(new ControlledStorage(), setup);
const first = await root.submit({ type: "input", content: "first" }, context);
await requested.promise;
const queued = await root.submit({ type: "input", content: "queued" }, context);
failing.resolve();
expect(await first.wait(context)).toMatchObject({ status: "unanswered", reason: "model_error" });
await harness.waitForIdle(context);
const rejected = root.submit({ type: "input", content: "steer", whenBusy: "steer", whenIdle: "reject" }, context);
await expect(rejected).rejects.toBeInstanceOf(ConversationIdle);
expect(await queued.status(context)).toMatchObject({ status: "queued" });
expect((await harness.snapshot(LiveDoc, root.id, context))?.run).toBeUndefined();
await harness.close(context);
});
// #11149
it("steers a working run", async () => {
const { harness, root, first } = await openBusy();
const steer = await root.submit(
{ type: "input", content: "also this", whenBusy: "steer", whenIdle: "reject" },
context,
);
expect(await steer.status(context)).toMatchObject({ type: "input", status: "queued" });
const inbox = await harness.snapshot(InboxDoc, root.id, context);
expect(inbox?.items).toEqual([{ id: steer.id, mode: "steer", content: "also this" }]);
expect((await harness.snapshot(LiveDoc, root.id, context))?.run?.inputs).toEqual([first.id]);
await harness.close(context);
});
// #11149
it("returns the submission of a known request ID, also once idle", async () => {
const setup = chatSetup();
setup.faux.setResponses([fauxAssistantMessage([fauxText("done")])]);
const { harness, root } = await openChat(new ControlledStorage(), setup);
const first = await root.submit({ type: "input", content: "work", requestId: "r1" }, context);
await first.wait(context);
const draft = { type: "input", content: "work", requestId: "r1", whenBusy: "steer", whenIdle: "reject" } as const;
expect((await root.submit(draft, context)).id).toBe(first.id);
// Deduplication runs before the idle rejection in the caller's commit too.
expect(await harness.commit((tx) => root.submit(tx, draft), context)).toBe(first.id);
await harness.close(context);
});
// #11149
it("rejects through a tool's conversation handle", async () => {
const setup = chatSetup();
let other: Conversation | undefined;
let rejection: unknown;
addTool(
setup.registry,
defineTool({
name: "nudge",
description: "Steers another conversation",
parameters: obj({}),
execute: async (_args, api, callContext) => {
const handle = (await api.conversation(other!.id, callContext))!;
const draft = { type: "input", content: "nudge", whenBusy: "steer", whenIdle: "reject" } as const;
rejection = await handle.submit(draft, callContext).catch((error: unknown) => error);
return { output: [{ type: "text", text: "nudged" }] };
},
}),
);
setup.faux.setResponses([
fauxAssistantMessage([fauxToolCall("nudge", {}, { id: "c1" })], { stopReason: "toolUse" }),
fauxAssistantMessage([fauxText("ok")]),
]);
const { harness, root } = await openChat(new ControlledStorage(), setup);
other = await harness.createConversation({ ownership: { kind: "ownerless" }, agent: { model: MODEL } }, context);
await (await root.submit({ type: "input", content: "go" }, context)).wait(context);
expect(rejection).toBeInstanceOf(ConversationIdle);
expect((await harness.snapshot(LiveDoc, other.id, context))?.run).toBeUndefined();
await harness.close(context);
});
});
describe("submit inside the caller's commit", () => {
// #11149
it("admits with the host's own check and writes in one commit", async () => {
const { harness, root, storage, first } = await openBusy();
const commits = storage.commits.length;
const id = await harness.commit(async (tx) => {
if ((await tx.doc(LiveDoc, root.id)).run === undefined) throw new Error("not working");
// The host's own document write may come before the admission.
const sent = await tx.doc(Sent, root.id);
const admitted = await root.submit(tx, { type: "input", content: "steer", whenBusy: "steer" });
sent.ids.push(admitted);
// The host's own table write comes after it.
await tx.appendEntry(root.id, { kind: "app.sent", data: { submission: admitted } });
return admitted;
}, context);
expect(storage.commits.length).toBe(commits + 1);
const writes = storage.commits.at(-1)!;
expect(writes.some((write) => write.type === "submission" && write.value.id === id)).toBe(true);
expect(writes.some((write) => write.type === "entry" && write.value.kind === "app.sent")).toBe(true);
expect((await harness.snapshot(Sent, root.id, context))?.ids).toEqual([id]);
const inbox = await harness.snapshot(InboxDoc, root.id, context);
expect(inbox?.items).toEqual([{ id, mode: "steer", content: "steer" }]);
expect((await harness.snapshot(LiveDoc, root.id, context))?.run?.inputs).toEqual([first.id]);
await harness.close(context);
});
// #11149
it("is rolled back with the commit when the callback throws later", async () => {
const storage = new ControlledStorage();
const { harness, root } = await openChat(storage, chatSetup());
const commits = storage.commits.length;
const failed = harness.commit(async (tx) => {
await root.submit(tx, { type: "input", content: "hi", requestId: "r1" });
throw new Error("host check failed");
}, context);
await expect(failed).rejects.toThrow("host check failed");
expect(storage.commits.length).toBe(commits);
expect((await harness.snapshot(LiveDoc, root.id, context))?.run).toBeUndefined();
expect(await harness.commit((tx) => tx.submissionByRequest(root.id, "r1"), context)).toBeUndefined();
await harness.close(context);
});
// #11149
it("leaves no submission and no run when Storage fails the commit, also after reopening", async () => {
const storage = new ControlledStorage();
const setup = chatSetup();
const { harness, root } = await openChat(storage, setup);
const failure = new Error("disk full");
storage.failNextCommit(failure);
const failed = harness.commit(async (tx) => {
await root.submit(tx, { type: "input", content: "hi", requestId: "r1" });
await tx.appendEntry(root.id, { kind: "app.sent" });
}, context);
await expect(failed).rejects.toBe(failure);
await harness.closed;
const reopened = await openChat(storage.reopen(), setup);
expect(reopened.root.id).toBe(root.id);
const found = await reopened.harness.commit(async (tx) => {
const submission = await tx.submissionByRequest(root.id, "r1");
const tasks = await tx.scanTasks({ conversationId: root.id }, 10);
const entries = await tx.scanEntries({ conversationId: root.id }, 10);
return { submission, tasks: tasks.items, entries: entries.items };
}, context);
expect(found).toEqual({ submission: undefined, tasks: [], entries: [] });
expect((await reopened.harness.snapshot(LiveDoc, root.id, context))?.run).toBeUndefined();
await reopened.harness.close(context);
});
// #11149
it("starts the run of an idle input once the commit is stored", async () => {
const setup = chatSetup();
setup.faux.setResponses([fauxAssistantMessage([fauxText("answer")])]);
// No `resume()`: the admission asks for progress itself.
const { harness, root } = await openChat(new ControlledStorage(), setup);
const id = await harness.commit((tx) => root.submit(tx, { type: "input", content: "hi" }), context);
expect(await settledStatus(harness, id)).toMatchObject({ type: "input", status: "done" });
await harness.close(context);
});
// #11149
it("queues a model-visible write while busy and places it at the next boundary", async () => {
const setup = chatSetup();
const hold = addHold(setup);
setup.faux.setResponses([hold.call, fauxAssistantMessage([fauxText("answer")])]);
const { harness, root } = await openChat(new ControlledStorage(), setup);
const first = await root.submit({ type: "input", content: "work" }, context);
await hold.reached.promise;
const note = { role: "user", content: "a note the model sees", timestamp: 1 } as const;
const id = await harness.commit(
(tx) => root.submit(tx, { type: "write", entry: { kind: "test.note", model: [note] } }),
context,
);
expect((await harness.snapshot(InboxDoc, root.id, context))?.items.map((item) => item.id)).toEqual([id]);
hold.release.resolve();
expect(await first.wait(context)).toMatchObject({ status: "done" });
expect(await settledStatus(harness, id)).toMatchObject({
type: "write",
status: "done",
entry: expect.any(Number),
});
await harness.close(context);
});
// #11149
it("deduplicates request IDs", async () => {
const { harness, root, storage } = await openBusy();
const draft = { type: "input", content: "steer", whenBusy: "steer", requestId: "r1" } as const;
const first = await harness.commit((tx) => root.submit(tx, draft), context);
const commits = storage.commits.length;
const again = await harness.commit((tx) => root.submit(tx, draft), context);
expect(again).toBe(first);
expect(storage.commits.length).toBe(commits);
await harness.close(context);
});
// #11149
it("rejects before any write, so the callback may catch the rejection", async () => {
const storage = new ControlledStorage();
const { harness, root } = await openChat(storage, chatSetup());
const commits = storage.commits.length;
const outcome = await harness.commit(async (tx) => {
try {
await root.submit(tx, { type: "input", content: "hi", whenBusy: "steer", whenIdle: "reject" });
return "admitted";
} catch (error) {
return error instanceof ConversationIdle ? "idle" : "other";
}
}, context);
expect(outcome).toBe("idle");
expect(storage.commits.length).toBe(commits);
expect((await harness.snapshot(LiveDoc, root.id, context))?.run).toBeUndefined();
await harness.close(context);
});
// #11149
it("throws ReadAfterWrite after a table write of the commit, on every path", async () => {
const { harness, root } = await openBusy();
// A busy input without request ID or trace reads no table, and is still refused.
const afterAppend = harness.commit(async (tx) => {
await tx.appendEntry(root.id, { kind: "app.note" });
await root.submit(tx, { type: "input", content: "steer", whenBusy: "steer" });
}, context);
await expect(afterAppend).rejects.toBeInstanceOf(ReadAfterWrite);
await expect(afterAppend).rejects.toThrow("submit(tx, draft) cannot read tables after the first table write");
const second = harness.commit(async (tx) => {
await root.submit(tx, { type: "input", content: "one", whenBusy: "steer" });
await root.submit(tx, { type: "input", content: "two", whenBusy: "steer" });
}, context);
await expect(second).rejects.toBeInstanceOf(ReadAfterWrite);
expect((await harness.snapshot(InboxDoc, root.id, context))?.items).toEqual([]);
const idle = await openChat(new ControlledStorage(), chatSetup());
const late = idle.harness.commit(async (tx) => {
await tx.appendEntry(idle.root.id, { kind: "app.note" });
await idle.root.submit(tx, { type: "input", content: "hi" });
}, context);
await expect(late).rejects.toBeInstanceOf(ReadAfterWrite);
await idle.harness.close(context);
await harness.close(context);
});
// #11149
it("continues a failed run, and must precede the commit's table writes", async () => {
const setup = chatSetup();
setup.faux.setResponses([failure, fauxAssistantMessage([fauxText("recovered")])]);
const { harness, root } = await openChat(new ControlledStorage(), setup);
const first = await root.submit({ type: "input", content: "Analyze this" }, context);
expect(await first.wait(context)).toMatchObject({ status: "unanswered", reason: "model_error" });
const late = harness.commit(async (tx) => {
await tx.appendEntry(root.id, { kind: "app.note" });
await root.submit(tx, { type: "continue" });
}, context);
await expect(late).rejects.toBeInstanceOf(ReadAfterWrite);
const id = await harness.commit(async (tx) => {
const continued = await root.submit(tx, { type: "continue" });
await tx.appendEntry(root.id, { kind: "app.note" });
return continued;
}, context);
expect(await settledStatus(harness, id)).toMatchObject({ type: "continue", status: "done" });
await harness.close(context);
});
// #11149
it("rejects a busy continue and steers with whenIdle inside the commit", async () => {
const { harness, root } = await openBusy();
const busy = harness.commit((tx) => root.submit(tx, { type: "continue" }), context);
await expect(busy).rejects.toBeInstanceOf(ConversationBusy);
const steer = await harness.commit(
(tx) => root.submit(tx, { type: "input", content: "steer", whenBusy: "steer", whenIdle: "reject" }),
context,
);
expect((await harness.snapshot(InboxDoc, root.id, context))?.items.map((item) => item.id)).toEqual([steer]);
await harness.close(context);
});
// #11149
it("throws for a transaction of another Session, or a wrapped one", async () => {
const a = await openChat(new ControlledStorage(), chatSetup());
const b = await openChat(new ControlledStorage(), chatSetup());
const draft = { type: "input", content: "hi" } as const;
const foreign = b.harness.commit((tx) => a.root.submit(tx, draft), context);
await expect(foreign).rejects.toThrow("The transaction belongs to another Session");
const proxied = a.harness.commit((tx) => a.root.submit(new Proxy(tx, {}), draft), context);
await expect(proxied).rejects.toThrow("The transaction belongs to another Session");
const plain = a.harness.commit((tx) => a.root.submit({ doc: tx.doc.bind(tx) } as unknown as Tx, draft), context);
await expect(plain).rejects.toThrow("The transaction belongs to another Session");
expect((await a.harness.snapshot(LiveDoc, a.root.id, context))?.run).toBeUndefined();
await a.harness.close(context);
await b.harness.close(context);
});
// #11149
it("records the trace carrier and links a steer to its caller and the running trace", async () => {
const setup = chatSetup();
const telemetry = new InMemoryTelemetryContext();
const busy = unanswered();
setup.faux.setResponses([busy.step]);
const { harness, root } = await openTraced(setup, telemetry);
await root.submit({ type: "input", content: "work" }, context);
await busy.reached;
const run = (await harness.inspect(context)).tasks.find((task) => task.record.kind === "pi.generation")!.record;
const id = await telemetry.startSpan({ name: "caller" }, (caller) =>
harness.commit(
(tx) => root.submit(tx, { type: "input", content: "steer", whenBusy: "steer" }),
withTelemetry(caller, context),
),
);
const spans = telemetry.getSpans();
const caller = spans.find((span) => span.name === "caller")!;
const submit = spans.find(
(span) => span.name === "pi.durable.submit" && span.attributes["submission.id"] === id,
)!;
expect(submit).toMatchObject({ parentId: null, attributes: { result: "queued" } });
expect(submit.links).toEqual([
{ context: caller.context, attributes: {} },
{ context: run.trace, attributes: {} },
]);
expect((await (await harness.submission(id, context))!.status(context)).trace).toEqual(submit.context);
await harness.close(context);
});
// #11149
it("starts an idle input's run in the admission's trace", async () => {
const setup = chatSetup();
const telemetry = new InMemoryTelemetryContext();
const busy = unanswered();
setup.faux.setResponses([busy.step]);
const { harness, root } = await openTraced(setup, telemetry);
const id = await telemetry.startSpan({ name: "caller" }, (caller) =>
harness.commit((tx) => root.submit(tx, { type: "input", content: "hi" }), withTelemetry(caller, context)),
);
await busy.reached;
const submit = telemetry
.getSpans()
.find((span) => span.name === "pi.durable.submit" && span.attributes["submission.id"] === id)!;
const record = await (await harness.submission(id, context))!.status(context);
expect(record.trace).toEqual(submit.context);
const run = (await harness.inspect(context)).tasks.find((task) => task.record.kind === "pi.generation")!.record;
expect(run.trace).toEqual(record.trace);
await harness.close(context);
});
// #11149
it("makes a write a child of its caller and leaves the commit's events to the caller's commit", async () => {
const setup = chatSetup();
const telemetry = new InMemoryTelemetryContext();
const { harness, root } = await openTraced(setup, telemetry);
const id = await telemetry.startSpan({ name: "caller" }, (caller) =>
harness.commit(
(tx) => root.submit(tx, { type: "write", entry: { kind: "app.note" } }),
withTelemetry(caller, context),
),
);
const spans = telemetry.getSpans();
const caller = spans.find((span) => span.name === "caller")!;
const submit = spans.find((span) => span.name === "pi.durable.submit")!;
expect(submit).toMatchObject({
parentContext: caller.context,
settled: true,
attributes: { "submission.id": id, result: "done" },
events: [],
});
expect(caller.events.map((event) => event.name)).toEqual(["pi.durable.entry", "pi.durable.submission"]);
await harness.close(context);
});
// #11149
it("ends its span before the commit, and a later throw leaves no submission", async () => {
const setup = chatSetup();
const telemetry = new InMemoryTelemetryContext();
const { harness, root } = await openTraced(setup, telemetry);
await telemetry.startSpan({ name: "caller" }, async (caller) => {
const failed = harness.commit(
async (tx) => {
await root.submit(tx, { type: "write", entry: { kind: "app.note" } });
throw new Error("later");
},
withTelemetry(caller, context),
);
await expect(failed).rejects.toThrow("later");
});
const spans = telemetry.getSpans();
const submit = spans.find((span) => span.name === "pi.durable.submit")!;
// The span reported an admission the commit then discarded.
expect(submit).toMatchObject({ settled: true, attributes: { result: "done" } });
const id = submit.attributes["submission.id"] as SubmissionId;
expect(await harness.submission(id, context)).toBeUndefined();
expect(spans.find((span) => span.name === "caller")!.events).toEqual([]);
await harness.close(context);
});
// #11149
it("admits atomically from a tool's commit through its conversation handle", async () => {
const setup = chatSetup();
let other: Conversation | undefined;
let toolTask: number | undefined;
addTool(
setup.registry,
defineTool({
name: "delegate",
description: "Delegates to another conversation",
parameters: obj({}),
execute: async (_args, api, callContext) => {
toolTask = api.taskId;
const handle = (await api.conversation(other!.id, callContext))!;
const id = await api.commit(async (tx) => {
const admitted = await handle.submit(tx, { type: "input", content: "from the tool" });
await tx.appendEntry(other!.id, { kind: "app.delegated", data: { submission: admitted } });
return admitted;
}, callContext);
return { output: [{ type: "text", text: `submitted ${id}` }] };
},
}),
);
const reply: FauxResponseStep = (request) => {
const last = request.messages.at(-1);
if (last?.role === "user" && last.content === "delegate") {
return fauxAssistantMessage([fauxToolCall("delegate", {}, { id: "c1" })], { stopReason: "toolUse" });
}
return fauxAssistantMessage([fauxText("ok")]);
};
setup.faux.setResponses([reply, reply, reply]);
const { harness, root } = await openChat(new ControlledStorage(), setup);
other = await harness.createConversation({ ownership: { kind: "ownerless" }, agent: { model: MODEL } }, context);
const run = await root.submit({ type: "input", content: "delegate" }, context);
expect(await run.wait(context)).toMatchObject({ status: "done" });
await harness.waitForIdle(context);
const entries = [...(await other.entries({}, 100, undefined, context)).items].reverse();
const delegated = entries.find((entry) => entry.kind === "app.delegated");
const submission = (delegated?.data as { submission: SubmissionId }).submission;
const settled = await settledStatus(harness, submission);
if (settled.type !== "input" || settled.status !== "done") throw new Error(`Unexpected ${settled.status}`);
// The placed input carries the tool task's byTaskId, as the commit's own appends do.
const user = await other.commit((tx) => tx.entry(UserEntry, settled.entry), context);
expect(user?.byTaskId).toBe(toolTask);
await harness.close(context);
});
// #11149
it("admits inside a task's terminal commit", async () => {
const setup = chatSetup();
setup.faux.setResponses([fauxAssistantMessage([fauxText("answer")])]);
const Admitting = defineTask<null, { phase: "run" }, SubmissionId>({
name: "test.admitting",
version: 1,
initial: () => ({ phase: "run" }),
phases: {
run: async (_task, runtime, taskContext) => {
const handle = (await runtime.conversation(runtime.conversationId, taskContext))!;
await runtime.commit(async (tx) => {
const id = await handle.submit(tx, { type: "input", content: "from the task" });
return { status: "terminal", outcome: { status: "completed", result: id } };
}, taskContext);
},
},
abort: (_task, runtime, taskContext) =>
runtime.commit(() => ({ status: "terminal", outcome: { status: "aborted" } }), taskContext),
});
addTask(setup.registry, Admitting);
const { harness, root } = await openChat(new ControlledStorage(), setup);
const task = await root.commit(
(tx) => tx.createTask(Admitting, null, { ownership: { kind: "conversation" } }),
context,
);
harness.resume();
const settled = await harness.waitForTask(task, context);
if (settled.state.outcome.status !== "completed") throw new Error(settled.state.outcome.status);
const status = await settledStatus(harness, settled.state.outcome.result);
if (status.type !== "input" || status.status !== "done") throw new Error(`Unexpected ${status.status}`);
const user = await root.commit((tx) => tx.entry(UserEntry, status.entry), context);
expect(user?.byTaskId).toBe(task);
await harness.close(context);
});
// #11149
it("rejects through a handle once its invocation is aborted, and once it ended", async () => {
const setup = chatSetup();
let other: Conversation | undefined;
let handle: ConversationHandle | undefined;
const started = deferred();
const abortSeen = deferred();
const release = deferred();
addTool(
setup.registry,
defineTool({
name: "hold",
description: "Holds until released, also when aborted",
parameters: obj({}),
execute: async (_args, api, callContext) => {
handle = (await api.conversation(other!.id, callContext))!;
callContext.abortSignal!.addEventListener("abort", () => abortSeen.resolve(), { once: true });
started.resolve();
await release.promise;
return { output: [] };
},
}),
);
setup.faux.setResponses([
fauxAssistantMessage([fauxToolCall("hold", {}, { id: "c1" })], { stopReason: "toolUse" }),
fauxAssistantMessage([fauxText("ok")]),
]);
const { harness, root } = await openChat(new ControlledStorage(), setup);
other = await harness.createConversation({ ownership: { kind: "ownerless" }, agent: { model: MODEL } }, context);
const run = await root.submit({ type: "input", content: "go" }, context);
await started.promise;
const tool = (await harness.inspect(context)).tasks.find((task) => task.record.kind === "pi.tool")!.record;
const aborting = harness.abortTask(tool.id, context);
await abortSeen.promise;
const draft = { type: "input", content: "late" } as const;
await expect(handle!.submit(draft, context)).rejects.toThrow();
await expect(harness.commit((tx) => handle!.submit(tx, draft), context)).rejects.toThrow();
expect((await harness.snapshot(LiveDoc, other.id, context))?.run).toBeUndefined();
release.resolve();
await aborting;
await run.wait(context);
await expect(harness.commit((tx) => handle!.submit(tx, draft), context)).rejects.toThrow();
expect((await harness.snapshot(LiveDoc, other.id, context))?.run).toBeUndefined();
await harness.close(context);
});
});