char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Sommove computer-use out of the repof5275c6

main
7.1 KiB174 linesraw
1import { assert, assertEquals, assertStringIncludes } from "@std/assert";
2import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context";
3import { type Message } from "@earendil-works/pi-ai";
4import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai/providers/faux";
5import { defineExtension } from "@berry/api";
6import { until } from "@berry/util";
7import { lastToolResult, script, setup, toolsOffered, turn } from "./fixture.ts";
8
9const images = (messages: readonly Message[]) =>
10  messages.flatMap((message) =>
11    message.role === "toolResult"
12      ? message.content.flatMap((block) => block.type === "image" ? [block.data] : [])
13      : []
14  );
15
16Deno.test("selected context hooks run in order on every request, compaction's too, without changing what is stored", async () => {
17  await using harness = await setup();
18  const { berry, faux, model } = harness;
19  for (const name of ["first", "second", "unused", "refuse"]) {
20    await berry.install(defineExtension({
21      name,
22      initial: () => null,
23      setup() {
24        return (conv) =>
25          conv.onContext(async (messages, requestedModel) => {
26            await Promise.resolve();
27            if (name === "unused") throw new Error("An unselected hook ran");
28            if (name === "refuse") throw new Error("context refused");
29            assertEquals(requestedModel.id, faux.getModel().id);
30            // the second sees what the first returned
31            return messages.map((message) =>
32              message.role !== "user" ? message : {
33                ...message,
34                content: name === "first" ? "filtered" : `${message.content} second`,
35              }
36            );
37          });
38      },
39    }));
40  }
41  const tid = await berry.create({ extensions: { first: {}, second: {} }, model });
42  const conversation = (await berry.conversation(tid))!;
43  const longInput = "x".repeat(100_000);
44  const [seen] = await turn(harness, tid, []);
45  assertEquals(seen.findLast((message) => message.role === "user")?.content, "filtered second");
46  assertEquals(toolsOffered(seen), ["codemode"]);
47
48  faux.setResponses([fauxAssistantMessage("done")]);
49  await (await conversation.submit({ type: "input", content: longInput }, context)).wait(context);
50  await conversation.waitForIdle(context);
51  let summarized: readonly Message[] = [];
52  faux.setResponses([(request) => {
53    summarized = request.messages;
54    return fauxAssistantMessage("A summary");
55  }]);
56  await berry.harness.waitForTask(await conversation.compact(undefined, context), context);
57  assertEquals(
58    summarized.findLast((message) => message.role === "user")?.content,
59    "filtered second",
60  );
61  const stored = await conversation.context(context);
62  assertEquals(
63    stored.messages.findLast((message) => message.role === "user")?.content,
64    longInput,
65  );
66
67  // a failing hook ends the request without calling the provider
68  const refused = await berry.create({ extensions: { refuse: {} }, model });
69  const calls = faux.state.callCount;
70  assertEquals(await turn(harness, refused, []), []);
71  assertEquals(faux.state.callCount, calls);
72  const entries = (await (await berry.conversation(refused))!.entries(
73    { order: "ascending" },
74    100,
75    undefined,
76    context,
77  ))
78    .items;
79  const failure = entries.flatMap((entry) => entry.model ?? []).findLast((message) =>
80    message.role === "assistant"
81  );
82  assert(failure?.role === "assistant");
83  assertEquals(failure.stopReason, "error");
84  assertStringIncludes(failure.errorMessage ?? "", "context refused");
85});
86
87Deno.test("command context appends passively with its event, and busy writes wait for the boundary", async () => {
88  await using harness = await setup();
89  const { berry, model, faux, gate } = harness;
90  await berry.install(defineExtension<string[], string>({
91    name: "reports",
92    initial: () => [],
93    apply: (state, event) => [...state, event],
94    setup() {
95      return (conv) =>
96        conv.command({
97          name: "report",
98          description: "",
99          async execute(content, ctx) {
100            await ctx.emit(content, { content });
101          },
102        });
103    },
104  }));
105  const tid = await berry.create({ extensions: { reports: {}, gate: {} }, model });
106  const conversation = (await berry.conversation(tid))!;
107  const [before] = await turn(harness, tid, []);
108  const previous = (await conversation.context(context)).messages;
109  const calls = faux.state.callCount;
110  await berry.command(tid, "report", "first edit");
111  assertEquals(faux.state.callCount, calls);
112  const stored = (await conversation.context(context)).messages;
113  assertEquals(stored.slice(0, previous.length), previous);
114  assertEquals(stored.at(-1)?.content, "first edit");
115
116  const [after] = await turn(harness, tid, []);
117  assertEquals(after.slice(0, stored.length), stored);
118  assertEquals(after.filter((m) => m.role === "system"), before.filter((m) => m.role === "system"));
119
120  let next: readonly Message[] = [];
121  faux.setResponses([
122    fauxAssistantMessage(fauxToolCall("hold", {}), { stopReason: "toolUse" }),
123    (request) => (next = request.messages, fauxAssistantMessage("done")),
124  ]);
125  const running = await conversation.submit({ type: "input", content: "go" }, context);
126  await gate.entered.promise;
127  const reporting = berry.command(tid, "report", "second edit");
128  try {
129    await until(async () =>
130      (await berry.harness.inspect(context)).submissions.some((s) =>
131        s.conversationId === conversation.id && s.type === "write"
132      )
133    );
134    assertEquals(await berry.state(tid, "reports"), ["first edit"]);
135    assert(
136      !(await conversation.context(context)).messages.some((m) => m.content === "second edit"),
137    );
138  } finally {
139    gate.release.resolve("released");
140  }
141  await reporting;
142  await running.wait(context);
143  await conversation.waitForIdle(context);
144  assertEquals(await berry.state(tid, "reports"), ["first edit", "second edit"]);
145  assertEquals(next.slice(0, stored.length), stored);
146  assertEquals(next.at(-1)?.content, "second edit");
147  assert(next.findLastIndex((m) => m.role === "toolResult") < next.length - 1);
148});
149
150Deno.test("a failed script's images reach anthropic in a result it accepts, and the history keeps the failure", async () => {
151  await using harness = await setup(undefined, {
152    api: "anthropic-messages",
153    models: [{ id: "claude", input: ["text", "image"] }],
154  });
155  const { berry, model } = harness;
156  const png =
157    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=";
158  const tid = await berry.create({ extensions: {}, model });
159  const [, sent] = await turn(harness, tid, [
160    script(`
161        image({ type: "image", data: "${png}", mimeType: "image/png" });
162        throw new Error("after the screenshot");
163      `),
164  ]);
165  const result = sent.findLast((message) => message.role === "toolResult");
166  assert(result?.role === "toolResult");
167  assertEquals(result.isError, false);
168  assertEquals(images(sent), [png]);
169  assertStringIncludes(lastToolResult(sent).text, "after the screenshot");
170  const stored = await (await berry.conversation(tid))!.context(context);
171  const kept = stored.messages.findLast((message) => message.role === "toolResult");
172  assert(kept?.role === "toolResult");
173  assertEquals(kept.isError, true);
174});