import { assert, assertEquals, assertStringIncludes } from "@std/assert"; import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context"; import { type Message } from "@earendil-works/pi-ai"; import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai/providers/faux"; import { defineExtension } from "@berry/api"; import { until } from "@berry/util"; import { lastToolResult, script, setup, toolsOffered, turn } from "./fixture.ts"; const images = (messages: readonly Message[]) => messages.flatMap((message) => message.role === "toolResult" ? message.content.flatMap((block) => block.type === "image" ? [block.data] : []) : [] ); Deno.test("selected context hooks run in order on every request, compaction's too, without changing what is stored", async () => { await using harness = await setup(); const { berry, faux, model } = harness; for (const name of ["first", "second", "unused", "refuse"]) { await berry.install(defineExtension({ name, initial: () => null, setup() { return (conv) => conv.onContext(async (messages, requestedModel) => { await Promise.resolve(); if (name === "unused") throw new Error("An unselected hook ran"); if (name === "refuse") throw new Error("context refused"); assertEquals(requestedModel.id, faux.getModel().id); // the second sees what the first returned return messages.map((message) => message.role !== "user" ? message : { ...message, content: name === "first" ? "filtered" : `${message.content} second`, } ); }); }, })); } const tid = await berry.create({ extensions: { first: {}, second: {} }, model }); const conversation = (await berry.conversation(tid))!; const longInput = "x".repeat(100_000); const [seen] = await turn(harness, tid, []); assertEquals(seen.findLast((message) => message.role === "user")?.content, "filtered second"); assertEquals(toolsOffered(seen), ["codemode"]); faux.setResponses([fauxAssistantMessage("done")]); await (await conversation.submit({ type: "input", content: longInput }, context)).wait(context); await conversation.waitForIdle(context); let summarized: readonly Message[] = []; faux.setResponses([(request) => { summarized = request.messages; return fauxAssistantMessage("A summary"); }]); await berry.harness.waitForTask(await conversation.compact(undefined, context), context); assertEquals( summarized.findLast((message) => message.role === "user")?.content, "filtered second", ); const stored = await conversation.context(context); assertEquals( stored.messages.findLast((message) => message.role === "user")?.content, longInput, ); // a failing hook ends the request without calling the provider const refused = await berry.create({ extensions: { refuse: {} }, model }); const calls = faux.state.callCount; assertEquals(await turn(harness, refused, []), []); assertEquals(faux.state.callCount, calls); const entries = (await (await berry.conversation(refused))!.entries( { order: "ascending" }, 100, undefined, context, )) .items; const failure = entries.flatMap((entry) => entry.model ?? []).findLast((message) => message.role === "assistant" ); assert(failure?.role === "assistant"); assertEquals(failure.stopReason, "error"); assertStringIncludes(failure.errorMessage ?? "", "context refused"); }); Deno.test("command context appends passively with its event, and busy writes wait for the boundary", async () => { await using harness = await setup(); const { berry, model, faux, gate } = harness; await berry.install(defineExtension({ name: "reports", initial: () => [], apply: (state, event) => [...state, event], setup() { return (conv) => conv.command({ name: "report", description: "", async execute(content, ctx) { await ctx.emit(content, { content }); }, }); }, })); const tid = await berry.create({ extensions: { reports: {}, gate: {} }, model }); const conversation = (await berry.conversation(tid))!; const [before] = await turn(harness, tid, []); const previous = (await conversation.context(context)).messages; const calls = faux.state.callCount; await berry.command(tid, "report", "first edit"); assertEquals(faux.state.callCount, calls); const stored = (await conversation.context(context)).messages; assertEquals(stored.slice(0, previous.length), previous); assertEquals(stored.at(-1)?.content, "first edit"); const [after] = await turn(harness, tid, []); assertEquals(after.slice(0, stored.length), stored); assertEquals(after.filter((m) => m.role === "system"), before.filter((m) => m.role === "system")); let next: readonly Message[] = []; faux.setResponses([ fauxAssistantMessage(fauxToolCall("hold", {}), { stopReason: "toolUse" }), (request) => (next = request.messages, fauxAssistantMessage("done")), ]); const running = await conversation.submit({ type: "input", content: "go" }, context); await gate.entered.promise; const reporting = berry.command(tid, "report", "second edit"); try { await until(async () => (await berry.harness.inspect(context)).submissions.some((s) => s.conversationId === conversation.id && s.type === "write" ) ); assertEquals(await berry.state(tid, "reports"), ["first edit"]); assert( !(await conversation.context(context)).messages.some((m) => m.content === "second edit"), ); } finally { gate.release.resolve("released"); } await reporting; await running.wait(context); await conversation.waitForIdle(context); assertEquals(await berry.state(tid, "reports"), ["first edit", "second edit"]); assertEquals(next.slice(0, stored.length), stored); assertEquals(next.at(-1)?.content, "second edit"); assert(next.findLastIndex((m) => m.role === "toolResult") < next.length - 1); }); Deno.test("a failed script's images reach anthropic in a result it accepts, and the history keeps the failure", async () => { await using harness = await setup(undefined, { api: "anthropic-messages", models: [{ id: "claude", input: ["text", "image"] }], }); const { berry, model } = harness; const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="; const tid = await berry.create({ extensions: {}, model }); const [, sent] = await turn(harness, tid, [ script(` image({ type: "image", data: "${png}", mimeType: "image/png" }); throw new Error("after the screenshot"); `), ]); const result = sent.findLast((message) => message.role === "toolResult"); assert(result?.role === "toolResult"); assertEquals(result.isError, false); assertEquals(images(sent), [png]); assertStringIncludes(lastToolResult(sent).text, "after the screenshot"); const stored = await (await berry.conversation(tid))!.context(context); const kept = stored.messages.findLast((message) => message.role === "toolResult"); assert(kept?.role === "toolResult"); assertEquals(kept.isError, true); });