char-slop/berry

multipurpose self-modifiable agentic harness

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

Charlotte Somreduce verbosity of sysprompt section change summary49a2848

main
7.5 KiB200 linesraw
1import { assertEquals } from "@std/assert";
2import type { SystemMessage, Tool } from "@earendil-works/pi-ai";
3import type { ConversationId, EntryId, EntryRecord } from "@earendil-works/pi-durable";
4import { withDom } from "@berry/util/dom.ts";
5
6const record = (id: number, patch: Partial<SystemMessage>): EntryRecord => ({
7  id: id as EntryId,
8  conversationId: 1 as ConversationId,
9  kind: "pi.system",
10  model: [{ role: "system", content: "", timestamp: id, ...patch }],
11});
12
13Deno.test("system summaries show net tool changes, not replay churn", async (t) => {
14  await withDom(async () => {
15    const { transcript } = await import("./transcript.tsx");
16    const tool = (name: string, description = name): Tool => ({
17      name,
18      description,
19      parameters: { type: "object", properties: {} },
20    });
21    const codemode = tool("codemode");
22    const read = tool("read");
23    const write = tool("write");
24    const baseline = record(1, { toolsAdded: [codemode, read, write] });
25    const cases: { name: string; patch: Partial<SystemMessage>; expected: string }[] = [
26      {
27        name: "unchanged remove/re-add pairs disappear but additions remain",
28        patch: {
29          sections: { berry: "guide", codemode: "catalog" },
30          toolsAdded: [tool("subagent"), codemode, read, write],
31          toolsRemoved: [codemode, read, write],
32        },
33        expected: "prompt sections +berry +codemode tools +subagent",
34      },
35      {
36        name: "a changed declaration is shown once",
37        patch: {
38          toolsAdded: [tool("codemode", "updated"), read, write],
39          toolsRemoved: [codemode, read, write],
40        },
41        expected: "prompt tools +codemode",
42      },
43      {
44        name: "real removals remain visible",
45        patch: {
46          toolsAdded: [codemode, read],
47          toolsRemoved: [codemode, read, write],
48        },
49        expected: "prompt tools -write",
50      },
51      {
52        name: "reordering alone is not a tool interface change",
53        patch: {
54          sections: { berry: "guide" },
55          toolsAdded: [write, read, codemode],
56          toolsRemoved: [codemode, read, write],
57        },
58        expected: "prompt sections +berry",
59      },
60    ];
61    for (const { name, patch, expected } of cases) {
62      await t.step(name, () => {
63        const view = transcript();
64        try {
65          view.update([baseline], {});
66          view.update([baseline, record(2, patch)], {});
67          const summaries = view.element.querySelectorAll(".system summary");
68          assertEquals(summaries[0].textContent, "prompt tools +codemode +read +write");
69          assertEquals(summaries[1].textContent, expected);
70        } finally {
71          view.dispose();
72        }
73      });
74    }
75    await t.step("summaries are recalculated when earlier context changes", () => {
76      const view = transcript();
77      try {
78        const updatedTools = [tool("codemode", "updated"), read, write];
79        const update = record(2, {
80          toolsAdded: updatedTools,
81          toolsRemoved: [codemode, read, write],
82        });
83        view.update([baseline, update], {});
84        const summary = view.element.querySelectorAll(".system summary")[1];
85        assertEquals(summary.textContent, "prompt tools +codemode");
86        view.update([
87          record(3, { toolsAdded: updatedTools }),
88          update,
89        ], {});
90        assertEquals(view.element.querySelectorAll(".system summary")[1], summary);
91        assertEquals(summary.textContent, "prompt");
92      } finally {
93        view.dispose();
94      }
95    });
96  });
97});
98
99Deno.test("section summaries compare the net effect of a reorder's patches", async (t) => {
100  await withDom(async () => {
101    const { transcript } = await import("./transcript.tsx");
102    const sections = { berry: "guide", code: "coding", web: "browsing" };
103    const baseline = record(1, { sections });
104    const removal = record(2, { sections: { berry: null, code: null, web: null } });
105    const cases: { name: string; sections: Record<string, string>; expected: string[] }[] = [
106      {
107        name: "inserting a section does not report unchanged sections",
108        sections: { berry: "guide", code: "coding", code_project: "project", web: "browsing" },
109        expected: ["+code_project"],
110      },
111      {
112        name: "changed values and real removals survive replay churn",
113        sections: { code: "updated coding", berry: "guide" },
114        expected: ["code", "-web"],
115      },
116      {
117        name: "reordering alone does not report content changes",
118        sections: { web: "browsing", code: "coding", berry: "guide" },
119        expected: [],
120      },
121    ];
122    for (const { name, sections, expected } of cases) {
123      await t.step(name, () => {
124        const view = transcript();
125        try {
126          view.update([baseline, removal, record(3, { timestamp: 2, sections })], {});
127          const summaries = view.element.querySelectorAll(".system summary");
128          assertEquals(summaries.length, 2);
129          assertEquals(
130            Array.from(summaries[1].querySelectorAll(".system-name"), (name) => name.textContent),
131            expected,
132          );
133        } finally {
134          view.dispose();
135        }
136      });
137    }
138    await t.step("split delivery and rewinds update the same summary and its expanded body", () => {
139      const view = transcript();
140      try {
141        const addition = record(3, {
142          timestamp: 2,
143          sections: { ...sections, code_project: "project" },
144        });
145        view.update([baseline, removal], {});
146        const details = view.element.querySelectorAll<HTMLDetailsElement>(".system")[1];
147        const summary = details.querySelector("summary")!;
148        details.open = true;
149        details.dispatchEvent(new Event("toggle"));
150        view.update([baseline, removal, addition], {});
151        assertEquals(view.element.querySelectorAll(".system").length, 2);
152        assertEquals(details.querySelector("summary"), summary);
153        assertEquals(summary.textContent, "prompt sections +code_project");
154        assertEquals(Array.from(details.querySelectorAll("pre"), (pre) => pre.textContent), [
155          "guide",
156          "coding",
157          "browsing",
158          "project",
159        ]);
160        assertEquals(details.open, true);
161
162        view.update([baseline, removal], {});
163        assertEquals(summary.textContent, "prompt sections -berry -code -web");
164        assertEquals(details.querySelectorAll("pre").length, 0);
165
166        view.update([
167          record(4, { sections: { ...sections, code_project: "project" } }),
168          removal,
169          addition,
170        ], {});
171        assertEquals(details.querySelector("summary"), summary);
172        assertEquals(summary.textContent, "prompt");
173      } finally {
174        view.dispose();
175      }
176    });
177    await t.step("an intervening message prevents unrelated changes from cancelling out", () => {
178      const view = transcript();
179      try {
180        const user: EntryRecord = {
181          id: 3 as EntryId,
182          conversationId: 1 as ConversationId,
183          kind: "pi.user",
184          model: [{ role: "user", content: "continue", timestamp: 2 }],
185        };
186        view.update([baseline, removal, user, record(4, { timestamp: 2, sections })], {});
187        assertEquals(
188          Array.from(view.element.querySelectorAll(".system summary"), (node) => node.textContent),
189          [
190            "prompt sections +berry +code +web",
191            "prompt sections -berry -code -web",
192            "prompt sections +berry +code +web",
193          ],
194        );
195      } finally {
196        view.dispose();
197      }
198    });
199  });
200});