char-slop/berry

multipurpose self-modifiable agentic harness

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

Charlotte Somadd a global local-environment toggle and keep the berry prompt local-onlye8f8604

main
11.3 KiB260 linesraw
1import { assert, assertEquals, assertRejects, assertStringIncludes } from "@std/assert";
2import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context";
3import { Type } from "@earendil-works/pi-ai";
4import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai/providers/faux";
5import { mkdir, symlink, writeFile } from "node:fs/promises";
6import { homedir } from "node:os";
7import { join } from "node:path";
8import { defineExtension } from "@berry/api";
9import { type Environment, localEnvironment } from "@berry/api/environment.ts";
10import { BerryError, Settings } from "./berry.ts";
11import { type Environments, type EnvironmentSettings, localOnly } from "./environment.ts";
12import { lastToolResult, script, sectionsSeen, setup, turn } from "./fixture.ts";
13import { tempDir } from "@berry/util";
14
15/**
16 * This machine as `local`, a directory of it posing as a machine named `other`, and a machine named `down` that
17 * cannot be reached.
18 */
19function twoMachines(otherHome: string): Environments {
20  return {
21    open(name, cwd, during) {
22      if (name === "down") return Promise.reject(new Error("down is unreachable"));
23      if (name !== "other") return localOnly.open(name, cwd, during);
24      const env: Environment = Object.assign(localEnvironment(cwd ?? otherHome), {
25        name,
26        home: otherHome,
27      });
28      return Promise.resolve(env);
29    },
30    close: () => Promise.resolve(),
31  };
32}
33
34async function withDirectories(test: (root: string) => Promise<void>) {
35  await using rootDir = await tempDir({ prefix: "berry-environment-" });
36  const root = rootDir.path;
37  await mkdir(join(root, "a"));
38  await mkdir(join(root, "b"));
39  await mkdir(join(root, "other"));
40  await writeFile(join(root, "file"), "");
41  await symlink(join(root, "a"), join(root, "link"));
42  await test(root);
43}
44
45Deno.test("a conversation works in the local home until bound to a directory its machine resolves, which /cd moves", () =>
46  withDirectories(async (root) => {
47    await using harness = await setup(twoMachines(join(root, "other")));
48    const { berry, model } = harness;
49    const home = await berry.create({ extensions: {}, model });
50    assertEquals(await berry.state(home, "environment"), { environment: "local", cwd: null });
51    const env = await berry.environment(home);
52    assertEquals([env.name, env.cwd], ["local", homedir()]);
53
54    const tid = await berry.create({
55      extensions: { environment: { cwd: join(root, "link") } },
56      model,
57    });
58    assertEquals((await berry.environment(tid)).cwd, join(root, "a"));
59    for (const cwd of [join(root, "file"), join(root, "missing")]) {
60      await assertRejects(() => berry.create({ extensions: { environment: { cwd } }, model }));
61    }
62    await assertRejects(
63      () => berry.create({ extensions: { environment: { environment: "elsewhere" } }, model }),
64      BerryError,
65      "No environment named elsewhere",
66    );
67    await berry.configureExtension(tid, "environment", { cwd: "../b" });
68    assertEquals((await berry.environment(tid)).cwd, join(root, "b"));
69
70    const other = await berry.create({
71      extensions: { environment: { environment: "other", cwd: join(root, "a") } },
72      model,
73    });
74    assertEquals(await berry.command(other, "cd", "../link"), join(root, "a"));
75    assertEquals(await berry.command(other, "cd", " ../b "), join(root, "b"));
76    await assertRejects(() => berry.command(other, "cd", "../file"), BerryError, "Not a directory");
77    await assertRejects(() => berry.command(other, "cd", "missing"), BerryError);
78    // home is the machine's
79    assertEquals(await berry.command(other, "cd", ""), join(root, "other"));
80    assertEquals(await berry.state(other, "environment"), {
81      environment: "other",
82      cwd: join(root, "other"),
83    });
84  }));
85
86Deno.test("the code extension's working directory carries over from old history", async () => {
87  await using harness = await setup();
88  const { berry, model } = harness;
89  const tid = await berry.create({ extensions: {}, model });
90  await (await berry.emit(tid, "coding", { type: "cd", cwd: "/first" })).wait(context);
91  assertEquals(await berry.state(tid, "environment"), { environment: "local", cwd: "/first" });
92  await (await berry.emit(tid, "code", { type: "cd", cwd: "/second" })).wait(context);
93  assertEquals(await berry.state(tid, "environment"), { environment: "local", cwd: "/second" });
94});
95
96Deno.test("only an idle conversation moves to another machine; a new directory waits for the boundary", () =>
97  withDirectories(async (root) => {
98    await using harness = await setup(twoMachines(join(root, "other")));
99    const { berry, model, faux, gate } = harness;
100    const tid = await berry.create({
101      extensions: { environment: { cwd: join(root, "a") }, gate: {} },
102      model,
103    });
104    faux.setResponses([
105      fauxAssistantMessage(fauxToolCall("hold", {}), { stopReason: "toolUse" }),
106      fauxAssistantMessage("done"),
107    ]);
108    const conversation = (await berry.conversation(tid))!;
109    await conversation.submit({ type: "input", content: "go" }, context);
110    await gate.entered.promise;
111
112    await assertRejects(
113      () => berry.configureExtension(tid, "environment", { environment: "other" }),
114      BerryError,
115      "busy",
116    );
117    const moved = berry.configureExtension(tid, "environment", { cwd: join(root, "b") });
118    assertEquals(await berry.state(tid, "environment"), {
119      environment: "local",
120      cwd: join(root, "a"),
121    });
122    gate.release.resolve("released");
123    assertEquals(await moved, { environment: "local", cwd: join(root, "b") });
124    await conversation.waitForIdle(context);
125
126    // the form still holds this machine's directory, which means nothing on the other
127    assertEquals(
128      await berry.configureExtension(tid, "environment", {
129        environment: "other",
130        cwd: join(root, "b"),
131      }),
132      { environment: "other", cwd: join(root, "other") },
133    );
134    const env = await berry.environment(tid);
135    assertEquals([env.name, env.cwd], ["other", join(root, "other")]);
136  }));
137
138/** Reports the environment wherever an extension meets it. */
139const probe = defineExtension({
140  name: "probe",
141  initial: () => null,
142  setup(ext) {
143    const where = (env: Environment) => `${env.name}:${env.cwd}`;
144    ext.tool({
145      name: "where",
146      description: "where calls run",
147      parameters: Type.Object({}),
148      execute: (_, ctx) => where(ctx.env),
149    });
150    ext.prompt("named", ({ env }) => env && `named ${where(env)}`);
151    return (conv) => {
152      conv.prompt(({ env }) => env && `in ${where(env)}`);
153      conv.command({ name: "go", description: "", execute: (path, ctx) => ctx.cd(path) });
154      conv.command({ name: "here", description: "", execute: (_, ctx) => where(ctx.env) });
155      conv.command({ name: "ping", description: "", execute: () => "pong" });
156    };
157  },
158});
159
160Deno.test("calls, prompts, and commands all meet the conversation's environment, and fail with a machine out of reach", () =>
161  withDirectories(async (root) => {
162    await using harness = await setup(twoMachines(join(root, "other")));
163    const { berry, model } = harness;
164    await berry.install(probe);
165    const tid = await berry.create({
166      extensions: { environment: { cwd: join(root, "a") }, probe: {} },
167      model,
168    });
169    const seen = await turn(harness, tid, [script("text(await tools.where({}))")]);
170    assertEquals(lastToolResult(seen[1]).text, `Script completed\nlocal:${join(root, "a")}`);
171    const sections = sectionsSeen(seen[0])[0];
172    assertEquals(sections.probe, `<probe>\nin local:${join(root, "a")}\n</probe>`);
173    assertEquals(
174      sections.probe_named,
175      `<probe_named>\nnamed local:${join(root, "a")}\n</probe_named>`,
176    );
177    assertEquals(sections.environment, undefined);
178
179    assertEquals(await berry.command(tid, "go", "../b"), join(root, "b"));
180    assertEquals(await berry.command(tid, "here", ""), `local:${join(root, "b")}`);
181    await berry.configureExtension(tid, "environment", { environment: "other" });
182    const [moved] = await turn(harness, tid, []);
183    assertEquals(
184      sectionsSeen(moved).at(-1)?.probe,
185      `<probe>\nin other:${join(root, "other")}\n</probe>`,
186    );
187    assertEquals(
188      sectionsSeen(moved).at(-1)?.environment?.includes("files and commands are on other"),
189      true,
190    );
191
192    // as though it went down after the conversation moved there
193    await (await berry.emit(tid, "environment", { type: "bind", environment: "down", cwd: "/" }))
194      .wait(context);
195    const down = await turn(harness, tid, [script("text(await tools.where({}))")]);
196    const result = lastToolResult(down[1]);
197    assertEquals(result.isError, true);
198    assertEquals(result.text.includes("down is unreachable"), true);
199    const unreachable = sectionsSeen(down[0]).at(-1)!;
200    assertEquals(unreachable.probe, null);
201    assertEquals(unreachable.environment?.includes("down cannot be reached"), true);
202    assertEquals(await berry.command(tid, "ping", ""), "pong");
203    await assertRejects(() => berry.command(tid, "here", ""), BerryError, "down is unreachable");
204  }));
205
206Deno.test("disabling local fails its turns without contacting the model, but remote turns and recovery still work", async () => {
207  await using root = await tempDir();
208  await using harness = await setup(twoMachines(root.path));
209  const { berry, model, faux } = harness;
210  const tid = await berry.create({ extensions: {}, model });
211  await berry.setAllowLocalEnvironment(false);
212
213  const calls = faux.state.callCount;
214  assertEquals(await turn(harness, tid, []), []);
215  assertEquals(faux.state.callCount, calls);
216  const conversation = (await berry.conversation(tid))!;
217  const entries = await conversation.entries({ order: "ascending" }, 100, undefined, context);
218  const failure = entries.items.flatMap((entry) => entry.model ?? []).findLast((message) =>
219    message.role === "assistant"
220  );
221  assert(failure?.role === "assistant");
222  assertEquals(failure.stopReason, "error");
223  assertStringIncludes(failure.errorMessage ?? "", "local environment is disabled");
224  await assertRejects(() => berry.environment(tid), BerryError, "local environment is disabled");
225  await assertRejects(
226    () => berry.command(tid, "cd", ""),
227    BerryError,
228    "local environment is disabled",
229  );
230
231  await berry.configureExtension(tid, "environment", { environment: "other" });
232  assertEquals((await turn(harness, tid, [])).length, 1);
233  await assertRejects(
234    () => berry.configureExtension(tid, "environment", { environment: "local" }),
235    BerryError,
236    "local environment is disabled",
237  );
238  await berry.setAllowLocalEnvironment(true);
239  await berry.configureExtension(tid, "environment", { environment: "local" });
240  assertEquals((await turn(harness, tid, [])).length, 1);
241});
242
243Deno.test("settings name the other machines, and nothing ssh could take for an option", async () => {
244  await using harness = await setup();
245  const { berry } = harness;
246  const refusals: Record<string, EnvironmentSettings>[] = [
247    { local: { ssh: "elsewhere" } },
248    { "-oProxyCommand=x": { ssh: "elsewhere" } },
249    { devbox: { ssh: "-oProxyCommand=x" } },
250    { devbox: { ssh: " " } },
251    { devbox: { ssh: "devbox", deno: "" } },
252  ];
253  for (const refused of refusals) {
254    await assertRejects(() => berry.setEnvironments(refused), BerryError);
255  }
256  await berry.setEnvironments({ devbox: { ssh: " me@devbox ", deno: "/opt/deno" } });
257  assertEquals((await berry.harness.snapshot(Settings, context))?.environments, {
258    devbox: { ssh: "me@devbox", deno: "/opt/deno" },
259  });
260});