char-slop/berry

multipurpose self-modifiable agentic harness

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

Charlotte Somtests: drop those another test already covers1188259

main
29.0 KiB691 linesraw
1import {
2  assert,
3  assertEquals,
4  assertMatch,
5  assertRejects,
6  assertStringIncludes,
7} from "@std/assert";
8import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context";
9import {
10  type ImageContent,
11  type Message,
12  type ToolResultMessage,
13  Type,
14} from "@earendil-works/pi-ai";
15import { createModels } from "@earendil-works/pi-ai/models";
16import {
17  fauxAssistantMessage,
18  fauxProvider,
19  fauxToolCall,
20} from "@earendil-works/pi-ai/providers/faux";
21import { MemoryStorage } from "@earendil-works/pi-durable";
22import { defineExtension } from "@berry/api";
23import { mkdir, writeFile } from "node:fs/promises";
24import { join } from "node:path";
25import { userInfo } from "node:os";
26import code from "../extensions/code/mod.ts";
27import { BerryError, Conversations, openBerry, Settings } from "./berry.ts";
28import { dir, lastToolResult, sectionsSeen, setup, toolsOffered, turn } from "./fixture.ts";
29import { TID_PATTERN } from "./tid.ts";
30import { localOnly } from "./environment.ts";
31import { tempDir } from "@berry/util";
32
33Deno.test("configured and emitted state reaches the prompt, as an extension's section and as named ones", async () => {
34  await using harness = await setup();
35  const { berry, model } = harness;
36  await assertRejects(
37    () => berry.create({ extensions: { dir: { cwd: 5 } }, model }),
38    BerryError,
39    "dir: cwd:",
40  );
41  await assertRejects(
42    () => berry.create({ extensions: { dir: { cwd: "here" } }, model }),
43    BerryError,
44    "dir: not absolute: here",
45  );
46  assertEquals(Object.keys(berry.configurationParameters("dir")?.properties ?? {}), ["cwd"]);
47  assertEquals(berry.configurationParameters("gate"), null);
48
49  const tid = await berry.create({ extensions: { dir: { cwd: "/seed" } }, model });
50  assertMatch(tid, TID_PATTERN);
51  const seen = await turn(harness, tid, [
52    fauxAssistantMessage(fauxToolCall("cd", { path: "/next" }), { stopReason: "toolUse" }),
53  ]);
54  assertEquals(sectionsSeen(seen[0])[0].dir, "<dir>\ncwd: /seed\n</dir>");
55  assertEquals(sectionsSeen(seen[1]).at(-1)?.dir, "<dir>\ncwd: /next\n</dir>");
56  assertEquals(await berry.state(tid, "dir"), { cwd: "/next" });
57
58  await berry.install(defineExtension({
59    name: "sections",
60    initial: () => ({ text: "first" }),
61    apply: (_state, event: { text: string }) => event,
62    setup(ext) {
63      ext.prompt("extra", ({ state }) => state.text || undefined);
64      return (conv) => conv.prompt(() => "stable");
65    },
66  }));
67  const named = await berry.create({ extensions: { sections: {} }, model });
68  const [initial] = await turn(harness, named, []);
69  const shown = sectionsSeen(initial)[0];
70  assertEquals(shown.sections, "<sections>\nstable\n</sections>");
71  assertEquals(shown["sections_extra"], "<sections_extra>\nfirst\n</sections_extra>");
72  for (const text of ["second", ""]) {
73    await (await berry.emit(named, "sections", { text })).wait(context);
74    const [seen] = await turn(harness, named, []);
75    assertEquals(sectionsSeen(seen).at(-1), {
76      "sections_extra": text ? `<sections_extra>\n${text}\n</sections_extra>` : null,
77    });
78  }
79});
80
81Deno.test("global prompt edits reach existing conversations and code prose follows the code extension", async () => {
82  await using harness = await setup();
83  const { berry, model } = harness;
84  await berry.install(code);
85  const coding = await berry.create({ extensions: { code: {} }, model });
86  const plain = await berry.create({ extensions: {}, model });
87  await turn(harness, coding, []);
88  await turn(harness, plain, []);
89
90  await berry.setSystemPrompt({
91    userName: "Alex",
92    generalGuidelines: "Ask $USER before deleting anything.",
93    promptProse: { code: "Keep $USER's APIs small." },
94  });
95  const [changed] = await turn(harness, coding, []);
96  const patch = sectionsSeen(changed).at(-1)!;
97  assertStringIncludes(patch.lede!, "(Alex)");
98  assertStringIncludes(patch.guidelines!, "Ask Alex before deleting anything.");
99  assertStringIncludes(patch.code!, "Keep Alex's APIs small.");
100  assertStringIncludes(patch.code!, "current working directory:");
101  assert(!Object.hasOwn(patch, "code_guidelines"));
102  assert(!Object.hasOwn(patch, "code_project") || patch["code_project"] === null);
103
104  const [withoutCode] = await turn(harness, plain, []);
105  const plainSections = Object.assign({}, ...sectionsSeen(withoutCode));
106  assertStringIncludes(plainSections.guidelines!, "Ask Alex before deleting anything.");
107  assert(!plainSections.code);
108
109  await berry.setSystemPrompt({ generalGuidelines: "", promptProse: { code: "" } });
110  const [cleared] = await turn(harness, coding, []);
111  const removed = sectionsSeen(cleared).at(-1)!;
112  assertEquals(removed.guidelines, null);
113  // blank prose leaves the dynamic part of the section behind
114  assertStringIncludes(removed.code!, "current working directory:");
115  assert(!removed.code!.includes("Keep"));
116  assert(!Object.hasOwn(removed, "lede"));
117});
118
119Deno.test("startup fills missing prompt settings without replacing saved fields or other settings", async () => {
120  await using directory = await tempDir();
121  const path = join(directory.path, "settings.sqlite");
122  const { openNodeSqliteStorage } = await import("@earendil-works/pi-durable/storage/sqlite/node");
123  const models = createModels();
124  const berry = await openBerry(await openNodeSqliteStorage(path), models);
125  assertEquals(
126    (await berry.harness.snapshot(Settings, context))?.systemPrompt?.userName,
127    userInfo().username,
128  );
129  await berry.harness.commit(async (tx) => {
130    const settings = await tx.doc(Settings);
131    settings.models = [{ provider: "offline", modelId: "kept" }];
132    settings.environments = { remote: { ssh: "host" } };
133    settings.systemPrompt = { userName: "Sam", generalGuidelines: "" } as NonNullable<
134      typeof settings.systemPrompt
135    >;
136  }, context);
137  await berry.close();
138  const reopened = await openBerry(await openNodeSqliteStorage(path), models);
139  try {
140    const settings = (await reopened.harness.snapshot(Settings, context))!;
141    assertEquals(settings.systemPrompt?.userName, "Sam");
142    assertEquals(settings.systemPrompt?.generalGuidelines, "");
143    assertEquals(settings.systemPrompt?.promptProse ?? {}, {});
144    assertEquals(settings.models, [{ provider: "offline", modelId: "kept" }]);
145    assertEquals(settings.environments, { remote: { ssh: "host" } });
146  } finally {
147    await reopened.close();
148  }
149});
150
151Deno.test("AGENTS.md edits update the project section without repeating code guidelines", async () => {
152  await using rootDir = await tempDir();
153  const root = rootDir.path;
154  await using harness = await setup();
155  const { berry, model } = harness;
156  await mkdir(join(root, "inner"));
157  await writeFile(join(root, "AGENTS.md"), "outer rules");
158  await writeFile(join(root, "inner", "AGENTS.md"), "inner rules");
159  await berry.install(code);
160  const tid = await berry.create({ extensions: { environment: { cwd: root }, code: {} }, model });
161  const [initial] = await turn(harness, tid, []);
162  const shown = sectionsSeen(initial)[0];
163  assertStringIncludes(shown.code!, `current working directory: ${root}`);
164  assertStringIncludes(shown.code!, "guidelines:");
165  assert(!shown.code!.includes("outer rules"));
166  assertStringIncludes(shown["code_project"]!, "outer rules");
167  assert(!shown["code_project"]!.includes("current working directory:"));
168
169  await writeFile(join(root, "AGENTS.md"), "revised rules");
170  const [edited] = await turn(harness, tid, []);
171  const patch = sectionsSeen(edited).at(-1)!;
172  assertEquals(Object.keys(patch), ["code_project"]);
173  assertStringIncludes(patch["code_project"]!, "revised rules");
174
175  await berry.command(tid, "cd", "inner");
176  const [moved] = await turn(harness, tid, []);
177  const changed = sectionsSeen(moved).at(-1)!;
178  assertStringIncludes(changed.code!, `current working directory: ${join(root, "inner")}`);
179  assertStringIncludes(changed["code_project"]!, "inner rules");
180});
181
182Deno.test("direct tools are offered beside codemode, and the model reads their text or their images", async () => {
183  const image: ImageContent = { type: "image", data: "bytes", mimeType: "image/png" };
184  const values = ["plain text", { ...image, id: "capture" }, { type: "image", data: "bytes" }];
185  await using harness = await setup();
186  const { berry, model } = harness;
187  await berry.install(defineExtension({
188    name: "results",
189    initial: () => null,
190    setup(ext) {
191      ext.tool({
192        name: "result",
193        description: "Return a value",
194        parameters: Type.Object({ index: Type.Integer() }),
195        expose: "model",
196        execute: ({ index }) => values[index],
197      });
198    },
199  }));
200  const tid = await berry.create({ extensions: { dir: {}, results: {} }, model });
201  const call = (name: string, args = {}) =>
202    fauxAssistantMessage(fauxToolCall(name, args), { stopReason: "toolUse" });
203  const seen = await turn(harness, tid, [
204    call("shout"),
205    ...values.map((_, index) => call("result", { index })),
206  ]);
207  assertEquals(toolsOffered(seen[0]).sort(), ["cd", "codemode", "result", "shout"]);
208  assertEquals(lastToolResult(seen[1]).text, "HEY!");
209  const expected: ToolResultMessage["content"][] = [
210    [{ type: "text", text: "plain text" }],
211    [image],
212    [{ type: "text", text: JSON.stringify(values[2], null, 2) }],
213  ];
214  for (const [index, content] of expected.entries()) {
215    const result = seen[index + 2].findLast((message) => message.role === "toolResult");
216    assert(result?.role === "toolResult");
217    assertEquals(result.content, content);
218  }
219});
220
221Deno.test("an extension installs whole or not at all, and a new reducer re-derives state from history", async () => {
222  await using harness = await setup();
223  const { berry, model } = harness;
224  for (const name of ["berry", "environment", "subagent"]) {
225    await assertRejects(
226      () => berry.install(defineExtension({ name, initial: () => null, setup() {} })),
227      BerryError,
228    );
229  }
230  let disposed = false;
231  const shadow = (expose: "model" | "codemode") =>
232    defineExtension({
233      name: "shadow",
234      initial: () => null,
235      setup(ext) {
236        ext.tool({
237          name: "codemode",
238          description: "",
239          parameters: Type.Object({}),
240          expose,
241          execute: () => "",
242        });
243        ext.onDispose(() => void (disposed = true));
244      },
245    });
246  await assertRejects(() => berry.install(shadow("model")), BerryError);
247  assert(disposed, "a refused extension is disposed");
248  assertEquals(berry.extensions().map((extension) => extension.name), ["dir", "gate"]);
249  // scripts may have a tool by that name; it cannot reach the model
250  await berry.install(shadow("codemode"));
251
252  const tid = await berry.create({ extensions: { dir: { cwd: "/a" } }, model });
253  await (await berry.emit(tid, "dir", { type: "cd", cwd: "b" })).wait(context);
254  await assertRejects(() =>
255    berry.install(defineExtension({
256      name: "dir",
257      initial: () => ({}),
258      setup() {
259        throw new Error("broken");
260      },
261    }))
262  );
263  assertEquals(await berry.state(tid, "dir"), { cwd: "b" });
264  // v2 resolves relative paths against the current cwd
265  await berry.install(
266    dir((s, e) => ({ cwd: e.cwd.startsWith("/") ? e.cwd : `${s.cwd}/${e.cwd}` })),
267  );
268  assertEquals(await berry.state(tid, "dir"), { cwd: "/a/b" });
269});
270
271Deno.test("a conversation has its chosen extensions, the always-on ones, and the harness's own, under any name they have had", async () => {
272  await using harness = await setup();
273  const { berry, model } = harness;
274  const names = async (id: string) =>
275    (await (await berry.conversation(id))!.agent(context)).extensions.map((e) => e.name);
276  // naming the harness's own changes nothing: every conversation has it
277  const tid = await berry.create({ extensions: { berry: {}, dir: { cwd: "/a" } }, model });
278  assertEquals(await names(tid), ["berry", "environment", "dir"]);
279  await berry.install(defineExtension({
280    name: "always",
281    alwaysOn: true,
282    initial: () => null,
283    setup() {
284      return (conv) => {
285        conv.command({ name: "ping", description: "always there", execute: () => "pong" });
286      };
287    },
288  }));
289  assertEquals(await names(tid), ["berry", "environment", "always", "dir"]);
290  const named = await berry.create({ extensions: { always: {}, dir: {} }, model });
291  assertEquals(await names(named), ["berry", "environment", "always", "dir"]);
292  assertEquals((await berry.commands(tid)).map(({ name }) => name), ["cd", "ping", "go"]);
293  assertEquals(await berry.command(tid, "ping", ""), "pong");
294  await berry.configure(named, { extensions: [] });
295  assertEquals(await names(named), ["berry", "environment", "always"]);
296
297  const conversation = (await berry.conversation(tid))!;
298  const [, seed] =
299    (await conversation.entries({ order: "ascending" }, 10, undefined, context)).items;
300  await (await berry.emit(tid, "dir", { type: "cd", cwd: "/b" })).wait(context);
301  berry.uninstall("dir");
302  await berry.install({ ...dir(), name: "place", formerly: ["dir"] });
303  assertEquals(await names(tid), ["berry", "environment", "always", "place"]);
304  assertEquals(await berry.state(tid, "place"), { cwd: "/b" });
305  // a fork from before the rename, and a conversation asked for by the old name
306  const fork = await conversation.fork(seed.id, { ownership: { kind: "ownerless" } }, context);
307  const forkTid = await berry.tidOf(fork.id);
308  assertEquals(await names(forkTid), ["berry", "environment", "always", "place"]);
309  assertEquals(await berry.state(forkTid, "place"), { cwd: "/a" });
310  const asked = await berry.create({ extensions: { dir: { cwd: "/c" } }, model });
311  assertEquals(await berry.state(asked, "place"), { cwd: "/c" });
312});
313
314Deno.test("a replaced load closes its channels and is disposed once the calls it started end", async () => {
315  const { faux, berry, model } = await setup();
316  const held = { entered: Promise.withResolvers<void>(), release: Promise.withResolvers<string>() };
317  const opened: number[] = [];
318  let disposed = 0;
319  const version = (mark: number) =>
320    defineExtension({
321      name: "slow",
322      alwaysOn: true,
323      initial: () => null,
324      setup(ext) {
325        ext.channel({
326          name: "line",
327          parameters: Type.Object({}),
328          open: () => void opened.push(mark),
329        });
330        ext.tool({
331          name: "hold",
332          description: "wait",
333          parameters: Type.Object({}),
334          expose: "model",
335          execute: () => {
336            held.entered.resolve();
337            return held.release.promise;
338          },
339        });
340        ext.onDispose(() => void (disposed += mark));
341      },
342    });
343  await berry.install(version(1));
344  const tid = await berry.create({ extensions: {}, model });
345  let closed = 0;
346  const channel = { send() {}, messages: new ReadableStream(), close: () => void closed++ };
347  await berry.channel(tid, "slow", "line", {}, channel, new AbortController().signal);
348  faux.setResponses([
349    fauxAssistantMessage(fauxToolCall("hold", {}), { stopReason: "toolUse" }),
350    fauxAssistantMessage("done"),
351  ]);
352  const conversation = (await berry.conversation(tid))!;
353  const submission = await conversation.submit({ type: "input", content: "go" }, context);
354  await held.entered.promise;
355
356  await berry.install(version(10));
357  assertEquals(disposed, 0);
358  held.release.resolve("released");
359  await submission.wait(context);
360  await conversation.waitForIdle(context);
361  assertEquals({ opened, closed, disposed }, { opened: [1], closed: 1, disposed: 1 });
362  await berry.close();
363  assertEquals(disposed, 11);
364});
365
366Deno.test("handlers answer only in conversations that have their extension", async () => {
367  await using harness = await setup();
368  const { berry, model } = harness;
369  await berry.install(defineExtension({
370    name: "asked",
371    initial: () => "folded",
372    setup(ext) {
373      ext.handle({
374        name: "echo",
375        parameters: Type.Object({ times: Type.Integer({ default: 1 }) }),
376        execute: ({ times }, ctx) => ({ state: ctx.state, times, aborted: ctx.signal.aborted }),
377      });
378      ext.handle({
379        name: "fail",
380        parameters: Type.Object({}),
381        execute: () => {
382          throw new Error("no such thing");
383        },
384      });
385    },
386  }));
387  const tid = await berry.create({ extensions: { asked: {} }, model });
388  assertEquals(await berry.request(tid, "asked", "echo", undefined), {
389    state: "folded",
390    times: 1,
391    aborted: false,
392  });
393  assertEquals(
394    (await berry.request(tid, "asked", "echo", { times: 3 }) as { times: number }).times,
395    3,
396  );
397  await assertRejects(
398    () => berry.request(tid, "asked", "echo", { times: "3" }),
399    BerryError,
400    "times",
401  );
402  await assertRejects(() => berry.request(tid, "asked", "fail", {}), BerryError, "no such thing");
403  await assertRejects(() => berry.request(tid, "asked", "nope", {}), BerryError, "no handler");
404  const without = await berry.create({ extensions: {}, model });
405  await assertRejects(
406    () => berry.request(without, "asked", "echo", {}),
407    BerryError,
408    "not in this conversation",
409  );
410});
411
412Deno.test("forks and branches start from the state at their entry, and a branch keeps the conversation's tid, agent, and name", async () => {
413  await using harness = await setup();
414  const { faux, berry, model, gate } = harness;
415  const tid = await berry.create({ extensions: { dir: { cwd: "/a" } }, model });
416  const conversation = (await berry.conversation(tid))!;
417  const [start, seed] =
418    (await conversation.entries({ order: "ascending" }, 10, undefined, context)).items;
419  assertEquals([start.kind, seed.kind], ["berry.start", "berry.event"]);
420  await (await berry.emit(tid, "dir", { type: "cd", cwd: "/b" })).wait(context);
421
422  const fork = await conversation.fork(seed.id, { ownership: { kind: "ownerless" } }, context);
423  const forkTid = await berry.tidOf(fork.id);
424  assert(forkTid !== tid);
425  assertEquals(await berry.state(forkTid, "dir"), { cwd: "/a" });
426  assertEquals(await berry.state(tid, "dir"), { cwd: "/b" });
427
428  await berry.configure(tid, { extensions: ["dir", "gate"] });
429  faux.setResponses([
430    fauxAssistantMessage(fauxToolCall("hold", {}), { stopReason: "toolUse" }),
431    fauxAssistantMessage("done"),
432  ]);
433  const submission = await conversation.submit({ type: "input", content: "go" }, context);
434  await gate.entered.promise;
435  await assertRejects(() => berry.branch(tid, seed.id), BerryError, "busy");
436  gate.release.resolve("released");
437  await submission.wait(context);
438  await conversation.waitForIdle(context);
439  const tip = (await conversation.context(context)).entries.at(-1)!.id;
440  await berry.markRead(tid, tip);
441  await berry.rename(tid, "  a custom name  ");
442
443  const feed = (await berry.watchState(tid, "dir"))!;
444  const moved = Promise.withResolvers<unknown>();
445  feed.start((value) => Promise.resolve(moved.resolve(value)));
446  await berry.branch(tid, seed.id);
447  const branch = (await berry.conversation(tid))!;
448  assert(branch.id !== conversation.id);
449  assertEquals(await berry.tidOf(branch.id), tid);
450  const index = (await berry.harness.snapshot(Conversations, context))!;
451  assertEquals(Object.keys(index).sort(), [tid, forkTid].sort());
452  assertEquals([index[tid].title, index[tid].readThrough], ["a custom name", tip]);
453  assertEquals(await berry.state(tid, "dir"), { cwd: "/a" });
454  assertEquals(await moved.promise, { cwd: "/a" });
455  assertEquals(
456    (await branch.agent(context)).extensions.map((extension) => extension.name),
457    ["berry", "environment", "dir", "gate"],
458  );
459  await feed.stop();
460
461  await berry.branch(tid, tip);
462  assertEquals((await berry.harness.snapshot(Conversations, context))![tid].title, "a custom name");
463  await berry.rename(tid, "   ");
464  assertEquals((await berry.harness.snapshot(Conversations, context))![tid].title, undefined);
465  await assertRejects(() => berry.rename("missing", "name"), BerryError, "No conversation");
466});
467
468Deno.test("commands are listed per conversation and run as the user, landing at once while busy", async () => {
469  await using harness = await setup();
470  const { faux, berry, model, gate } = harness;
471  const tid = await berry.create({ extensions: { dir: {}, gate: {} }, model });
472  const conversation = (await berry.conversation(tid))!;
473  assertEquals((await berry.commands(tid)).map(({ name }) => name), ["cd", "go"]);
474  assertEquals(await berry.command(tid, "go", "/cmd"), "/cmd");
475  await conversation.waitForIdle(context);
476  assertEquals(await berry.state(tid, "dir"), { cwd: "/cmd" });
477  await assertRejects(() => berry.command(tid, "go", "relative"), BerryError, "not absolute");
478  await assertRejects(() => berry.command(tid, "nope", ""), BerryError, "No command /nope");
479  const other = await berry.create({ extensions: {}, model });
480  assertEquals((await berry.commands(other)).map(({ name }) => name), ["cd"]);
481
482  let next: readonly Message[] = [];
483  faux.setResponses([
484    fauxAssistantMessage(fauxToolCall("hold", {}), { stopReason: "toolUse" }),
485    (ctx) => (next = ctx.messages, fauxAssistantMessage("done")),
486  ]);
487  const submission = await conversation.submit({ type: "input", content: "go" }, context);
488  await gate.entered.promise;
489  assertEquals(await berry.command(tid, "go", "/mid"), "/mid");
490  assertEquals(await berry.state(tid, "dir"), { cwd: "/mid" });
491  gate.release.resolve("released");
492  await submission.wait(context);
493  await conversation.waitForIdle(context);
494  assertEquals(await berry.state(tid, "dir"), { cwd: "/mid" });
495  // and the model hears of it at the next request, not only on the next input
496  assertEquals(sectionsSeen(next).at(-1)?.dir, "<dir>\ncwd: /mid\n</dir>");
497});
498
499Deno.test("conversations use only models enabled in settings, at a thinking level the model supports", async () => {
500  await using harness = await setup(undefined, {
501    models: [{ id: "thinker", reasoning: true }, { id: "plain", reasoning: false }],
502  });
503  const { berry, model } = harness;
504  const plain = { provider: "faux", modelId: "plain" };
505  await assertRejects(
506    () => berry.create({ extensions: {}, model: plain }),
507    BerryError,
508    "not enabled",
509  );
510  const tid = await berry.create({ extensions: {}, model, thinkingLevel: "high" });
511  await assertRejects(() => berry.configure(tid, { model: plain }), BerryError, "not enabled");
512  await berry.enableModels([model, plain, model]);
513  assertEquals((await berry.harness.snapshot(Settings, context))?.models, [model, plain]);
514
515  const agent = async (id: string) => await (await berry.conversation(id))!.agent(context);
516  assertEquals((await agent(tid)).thinkingLevel, "high");
517  await berry.configure(tid, { model: plain });
518  assertEquals([(await agent(tid)).model, (await agent(tid)).thinkingLevel], [plain, "off"]);
519  await berry.configure(tid, { model, thinkingLevel: "max" });
520  assertEquals((await agent(tid)).thinkingLevel, "high");
521  const created = await berry.create({ extensions: {}, model: plain, thinkingLevel: "high" });
522  assertEquals((await agent(created)).thinkingLevel, "off");
523});
524
525Deno.test("extension prose defaults, overrides, blanks, selection, and $USER", async () => {
526  await using harness = await setup();
527  const { berry, model } = harness;
528  await berry.install(defineExtension({
529    name: "notes",
530    initial: () => null,
531    setup(ext) {
532      ext.promptProse({ default: "Greet $USER warmly." });
533      return (conv) => conv.prompt(() => "dynamic content");
534    },
535  }));
536  assertEquals(berry.promptProseDefault("notes"), "Greet $USER warmly.");
537  assertEquals(berry.promptProseDefault("dir"), null);
538  const withNotes = await berry.create({ extensions: { notes: {} }, model });
539  const withoutNotes = await berry.create({ extensions: {}, model });
540  const [first] = await turn(harness, withNotes, []);
541  const shown = sectionsSeen(first)[0];
542  assertStringIncludes(shown.notes!, "Greet");
543  assert(!shown.notes!.includes("$USER"));
544  assert(!shown.notes!.includes("guidelines:"));
545  assertStringIncludes(shown.notes!, "dynamic content");
546  const [absent] = await turn(harness, withoutNotes, []);
547  assert(!Object.assign({}, ...sectionsSeen(absent)).notes);
548
549  await berry.setSystemPrompt({ userName: "Sam", promptProse: { notes: "Bid $USER farewell." } });
550  const [overrode] = await turn(harness, withNotes, []);
551  assertStringIncludes(sectionsSeen(overrode).at(-1)!.notes!, "Bid Sam farewell.");
552  assertStringIncludes(sectionsSeen(overrode).at(-1)!.notes!, "dynamic content");
553
554  await berry.setSystemPrompt({ promptProse: { notes: "" } });
555  const [blanked] = await turn(harness, withNotes, []);
556  assertStringIncludes(sectionsSeen(blanked).at(-1)!.notes!, "dynamic content");
557  assert(!sectionsSeen(blanked).at(-1)!.notes!.includes("Bid"));
558
559  const stored = (await berry.harness.snapshot(Settings, context))?.systemPrompt;
560  assertEquals(stored?.promptProse?.notes, "");
561});
562
563Deno.test("startup migrates legacy code prose, preserving blank values and newer overrides", async () => {
564  const { openNodeSqliteStorage } = await import("@earendil-works/pi-durable/storage/sqlite/node");
565  for (
566    const { legacy, override, expected } of [
567      {
568        legacy: "Legacy for $USER.",
569        override: undefined,
570        expected: "guidelines:\nLegacy for $USER.",
571      },
572      { legacy: "", override: undefined, expected: "" },
573      { legacy: "Stale legacy.", override: "", expected: "" },
574    ]
575  ) {
576    await using directory = await tempDir();
577    const path = join(directory.path, "settings.sqlite");
578    const models = createModels();
579    const seed = await openBerry(await openNodeSqliteStorage(path), models);
580    try {
581      await seed.harness.commit(async (tx) => {
582        const settings = await tx.doc(Settings);
583        const stored = {
584          userName: "Sam",
585          generalGuidelines: "",
586          codingGuidelines: legacy,
587          promptProse: {
588            other: "untouched",
589            ...(override === undefined ? {} : { code: override }),
590          },
591        };
592        settings.systemPrompt = stored;
593      }, context);
594    } finally {
595      await seed.close();
596    }
597    const berry = await openBerry(await openNodeSqliteStorage(path), models);
598    try {
599      const stored = (await berry.harness.snapshot(Settings, context))?.systemPrompt;
600      assertEquals(stored?.promptProse, { code: expected, other: "untouched" });
601      assertEquals(stored?.userName, "Sam");
602      assertEquals(stored?.generalGuidelines, "");
603    } finally {
604      await berry.close();
605    }
606  }
607});
608
609Deno.test("prompt prose overrides survive a missing extension", async () => {
610  await using harness = await setup();
611  const { berry } = harness;
612  await berry.install(defineExtension({
613    name: "ephemeral",
614    initial: () => null,
615    setup(ext) {
616      ext.promptProse({ default: "default prose" });
617    },
618  }));
619  await berry.setSystemPrompt({ promptProse: { ephemeral: "kept override" } });
620  berry.uninstall("ephemeral");
621  assertEquals(berry.promptProseDefault("ephemeral"), null);
622  assertEquals(
623    (await berry.harness.snapshot(Settings, context))?.systemPrompt?.promptProse?.ephemeral,
624    "kept override",
625  );
626  await berry.install(defineExtension({
627    name: "ephemeral",
628    initial: () => null,
629    setup(ext) {
630      ext.promptProse({ default: "default prose" });
631    },
632  }));
633  assertEquals(berry.promptProseDefault("ephemeral"), "default prose");
634  assertEquals(
635    (await berry.harness.snapshot(Settings, context))?.systemPrompt?.promptProse?.ephemeral,
636    "kept override",
637  );
638});
639
640Deno.test("a host lends /reload its extensions, and the prompt a guide to berry's source", async () => {
641  const faux = fauxProvider();
642  const models = createModels();
643  models.setProvider(faux.provider);
644  const model = { provider: "faux", modelId: faux.getModel().id };
645  const reloaded: string[] = [];
646  const loaded = (name: string) =>
647    ({ name, description: null, status: "loaded", error: null }) as const;
648  const failed = { ...loaded("two"), status: "failed", error: "no mod.ts" } as const;
649  const berry = await openBerry(new MemoryStorage(), models, {
650    root: "/somewhere/berry",
651    check: () => Promise.resolve(null),
652    restart: () => {},
653    extensions: () => [loaded("one"), failed],
654    reload: (name) => {
655      reloaded.push(name);
656      return Promise.resolve(name === "two" ? failed : loaded(name));
657    },
658  }, {
659    async open(name, cwd, during) {
660      return Object.assign(await localOnly.open("local", cwd, during), { name });
661    },
662    close: () => Promise.resolve(),
663  });
664  try {
665    await berry.enableModels([model]);
666    const tid = await berry.create({ extensions: {}, model });
667    const seen = await turn({ faux, berry }, tid, []);
668    assertStringIncludes(sectionsSeen(seen[0])[0].berry ?? "", "/somewhere/berry");
669    assertEquals(toolsOffered(seen[0]), ["codemode"]);
670
671    assertEquals(await berry.command(tid, "reload", " one "), "reloaded one");
672    await assertRejects(() => berry.command(tid, "reload", ""), BerryError, "two: no mod.ts");
673    assertEquals(reloaded, ["one", "one", "two"]);
674
675    await berry.configureExtension(tid, "environment", { environment: "remote" });
676    const [remote] = await turn({ faux, berry }, tid, []);
677    assertEquals(sectionsSeen(remote).at(-1)?.berry, null);
678    const fresh = await berry.create({
679      extensions: { environment: { environment: "remote" } },
680      model,
681    });
682    const [firstRemote] = await turn({ faux, berry }, fresh, []);
683    assertEquals(sectionsSeen(firstRemote).at(-1)?.berry, undefined);
684
685    await berry.configureExtension(tid, "environment", { environment: "local" });
686    const [local] = await turn({ faux, berry }, tid, []);
687    assertStringIncludes(sectionsSeen(local).at(-1)?.berry ?? "", "/somewhere/berry");
688  } finally {
689    await berry.close();
690  }
691});