char-slop/berry

multipurpose self-modifiable agentic harness

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

Charlotte Somsubagent: show compact lifecycle events2c34403

main
50.5 KiB1162 linesraw
1import { assert, assertEquals, assertRejects, assertStringIncludes } from "@std/assert";
2import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context";
3import {
4  type JsonObject,
5  type Message,
6  type ModelThinkingLevel,
7  Type,
8} from "@earendil-works/pi-ai";
9import { getCurrentSystemPrompt, getCurrentTools } from "@earendil-works/pi-ai/utils/transcript";
10import { createModels } from "@earendil-works/pi-ai/models";
11import {
12  fauxAssistantMessage,
13  fauxProvider,
14  fauxToolCall,
15} from "@earendil-works/pi-ai/providers/faux";
16import { AgentDoc, LiveDoc, MemoryStorage } from "@earendil-works/pi-durable";
17import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
18import { defineExtension } from "@berry/api";
19import { localEnvironment } from "@berry/api/environment.ts";
20import { Conversations, openBerry } from "./berry.ts";
21import {
22  blocking,
23  dir,
24  lastToolResult,
25  script,
26  sectionsSeen,
27  setup,
28  toolsOffered,
29  turn,
30} from "./fixture.ts";
31import { tempDir, until } from "@berry/util";
32
33async function within<T>(promise: Promise<T>): Promise<T> {
34  let timer: ReturnType<typeof setTimeout> | undefined;
35  try {
36    return await Promise.race([
37      promise,
38      new Promise<never>((_, reject) =>
39        timer = setTimeout(() => reject(new Error("Timed out")), 5000)
40      ),
41    ]);
42  } finally {
43    clearTimeout(timer);
44  }
45}
46
47const last = (messages: readonly Message[]) => messages.findLast((m) => m.role !== "system")!;
48const textOf = (message: Message) =>
49  typeof message.content === "string"
50    ? message.content
51    : message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n");
52
53Deno.test("selecting subagent controls direct tools, the codemode catalog, and script invocation, and old conversations keep it", async () => {
54  await using harness = await setup();
55  const { berry, model } = harness;
56  const tid = await berry.create({ extensions: {}, model });
57  const [initial] = await turn(harness, tid, []);
58  assert(!getCurrentTools(initial).some((tool) => tool.name === "subagent"));
59  assert(!sectionsSeen(initial).some((section) => section.codemode?.includes("subagent:")));
60
61  await berry.configure(tid, { extensions: ["subagent"] });
62  const [enabled] = await turn(harness, tid, []);
63  assert(getCurrentTools(enabled).some((tool) => tool.name === "subagent"));
64  assert(sectionsSeen(enabled).some((section) => section.codemode?.includes("subagent:")));
65
66  await berry.configure(tid, { extensions: [] });
67  const disabled = await turn(harness, tid, [
68    script(`return await tools.subagent({task:"must not run", model:${JSON.stringify(model)}});`),
69  ]);
70  assert(!getCurrentTools(disabled[0]).some((tool) => tool.name === "subagent"));
71  const catalog =
72    sectionsSeen(disabled[0]).findLast((section) => section.codemode !== undefined)?.codemode ?? "";
73  assert(!catalog.includes("subagent:"));
74  const result = lastToolResult(disabled[1]);
75  assert(result.isError);
76  assertStringIncludes(result.text, "subagent");
77  assertEquals(Object.keys((await berry.harness.snapshot(Conversations, context))!), [tid]);
78
79  // as conversations from before subagent was a choice stored their extensions, which keep it
80  const selected = async (tid: string) =>
81    (await (await berry.conversation(tid))!.agent(context)).extensions.some((ext) =>
82      ext.name === "subagent"
83    );
84  const legacy = await berry.harness.createConversation({
85    ownership: { kind: "ownerless" },
86    agent: { extensions: { add: [] } },
87  }, context);
88  const old = await berry.tidOf(legacy.id);
89  assertEquals(await selected(old), true);
90  await berry.configure(old, { extensions: [] });
91  assertEquals(await selected(old), false);
92});
93
94Deno.test("delegation needs a worker model enabled for it, and continues only the conversation's own subagents", async () => {
95  await using harness = await setup(undefined, {
96    models: [
97      { id: "parent" },
98      { id: "chosen-worker" },
99      { id: "other-worker" },
100      { id: "disabled-worker" },
101    ],
102  });
103  const { berry, faux, model } = harness;
104  const worker = { provider: "faux", modelId: "chosen-worker" };
105  const other = { provider: "faux", modelId: "other-worker" };
106  const ran: string[] = [];
107  const delegate = async (tid: string, args: JsonObject) => {
108    faux.setResponses(Array.from({ length: 4 }, () => (request, _, __, chosen) => {
109      const message = last(request.messages);
110      if (message.role === "toolResult") return fauxAssistantMessage("done");
111      if (textOf(message) === "go") {
112        return fauxAssistantMessage(fauxToolCall("subagent", args), { stopReason: "toolUse" });
113      }
114      ran.push(chosen.id);
115      return fauxAssistantMessage("worker answer");
116    }));
117    const parent = (await berry.conversation(tid))!;
118    await within((await parent.submit({ type: "input", content: "go" }, context)).wait(context));
119    await within(parent.waitForIdle(context));
120    const messages = (await parent.context(context)).messages;
121    return { result: lastToolResult(messages), prompt: getCurrentSystemPrompt(messages) };
122  };
123  const children = async (tid: string) => {
124    const index = (await berry.harness.snapshot(Conversations, context))!;
125    return Object.keys(index).filter((key) => index[key].owner === tid);
126  };
127  await berry.enableModels([model, worker, other]);
128  const owner = await berry.create({
129    extensions: { subagent: { models: ["faux/chosen-worker"] } },
130    model,
131  });
132  const refusals: [JsonObject, string][] = [
133    [{ task: "must not run" }, "model"],
134    [
135      { task: "must not run", model: { provider: "faux", modelId: "disabled-worker" } },
136      "not enabled",
137    ],
138    [{ task: "must not run", model: other }, "not enabled for delegation"],
139  ];
140  for (const [args, error] of refusals) {
141    const { result, prompt } = await delegate(owner, args);
142    assert(result.isError);
143    assertStringIncludes(result.text, error);
144    assertStringIncludes(prompt, "chosen-worker");
145    assert(!prompt.includes("other-worker") && !prompt.includes("disabled-worker"));
146  }
147  assertEquals(await children(owner), []);
148  const graph = await berry.harness.taskGraph(context);
149  assertEquals(Object.keys(graph.value.tasks), []);
150  graph.dispose();
151  assert(!(await delegate(owner, { task: "start", model: worker })).result.isError);
152  const [child] = await children(owner);
153
154  const stranger = await berry.create({ extensions: { subagent: {} }, model });
155  const foreign = (await delegate(stranger, { task: "steal", conversation: child })).result;
156  assert(foreign.isError);
157  assertStringIncludes(foreign.text, "not one of this conversation's subagents");
158
159  await berry.configureExtension(owner, "subagent", { models: ["faux/other-worker"] });
160  const stale = (await delegate(owner, { task: "again", conversation: child })).result;
161  assert(stale.isError);
162  assertStringIncludes(stale.text, "faux/chosen-worker is not enabled for delegation");
163  const moved = await delegate(owner, { task: "again", conversation: child, model: other });
164  assert(!moved.result.isError);
165  assertEquals((await (await berry.conversation(child))!.agent(context)).model, other);
166
167  // choosing none leaves every model enabled in settings
168  assertEquals(await berry.configureExtension(owner, "subagent", { models: [] }), {
169    models: [],
170    limit: 4,
171  });
172  const fallback = await delegate(owner, { task: "more", model: worker });
173  assert(!fallback.result.isError);
174  assertStringIncludes(fallback.prompt, "other-worker");
175  assertEquals(ran, ["chosen-worker", "other-worker", "chosen-worker"]);
176  await assertRejects(
177    () => berry.configureExtension(owner, "subagent", { models: ["faux/disabled-worker"] }),
178    Error,
179    "faux/disabled-worker is not enabled in settings",
180  );
181});
182
183Deno.test("delegation can override thinking without changing the parent", async (t) => {
184  await using harness = await setup(undefined, {
185    models: [
186      { id: "parent", reasoning: true },
187      { id: "worker", reasoning: true },
188      { id: "plain-worker", reasoning: false },
189    ],
190  });
191  const { berry, faux, model } = harness;
192  const worker = { provider: "faux", modelId: "worker" };
193  const plain = { provider: "faux", modelId: "plain-worker" };
194  await berry.enableModels([model, worker, plain]);
195  const cases: {
196    name: string;
197    requested?: ModelThinkingLevel;
198    expected: ModelThinkingLevel;
199    plain?: boolean;
200  }[] = [
201    { name: "omission inherits", expected: "high" },
202    { name: "a request overrides", requested: "low", expected: "low" },
203    {
204      name: "a non-reasoning worker clamps to off",
205      requested: "high",
206      expected: "off",
207      plain: true,
208    },
209  ];
210  for (const scenario of cases) {
211    await t.step(scenario.name, async () => {
212      const selected = scenario.plain ? plain : worker;
213      const tid = await berry.create({
214        extensions: { subagent: {} },
215        model,
216        thinkingLevel: "high",
217      });
218      const parent = (await berry.conversation(tid))!;
219      const args = {
220        task: scenario.name,
221        model: selected,
222        ...(scenario.requested === undefined ? {} : { thinkingLevel: scenario.requested }),
223      };
224      const requests: { modelId: string; thinking: ModelThinkingLevel }[] = [];
225      faux.setResponses([
226        fauxAssistantMessage(fauxToolCall("subagent", args), { stopReason: "toolUse" }),
227        (_, options, __, model) => {
228          requests.push({ modelId: model.id, thinking: options?.reasoning ?? "off" });
229          return fauxAssistantMessage("worker answer");
230        },
231        fauxAssistantMessage("done"),
232      ]);
233      await (await parent.submit({ type: "input", content: "go" }, context)).wait(context);
234      await parent.waitForIdle(context);
235      assertEquals(requests, [{ modelId: selected.modelId, thinking: scenario.expected }]);
236      assert(!lastToolResult((await parent.context(context)).messages).isError);
237      const index = (await berry.harness.snapshot(Conversations, context))!;
238      const children = Object.keys(index).filter((child) => index[child].owner === tid);
239      assertEquals(children.length, 1);
240      const child = (await berry.conversation(children[0]))!;
241      assertEquals((await child.agent(context)).thinkingLevel, scenario.expected);
242      const unchanged = await parent.agent(context);
243      assertEquals(unchanged.model, model);
244      assertEquals(unchanged.thinkingLevel, "high");
245    });
246  }
247});
248
249Deno.test("a continued subagent keeps its conversation, context, model, and thinking, and queues follow-ups", async () => {
250  await using harness = await setup(undefined, {
251    models: [{ id: "parent" }, { id: "worker", reasoning: true }],
252  });
253  const { berry, faux, model } = harness;
254  const worker = { provider: "faux", modelId: "worker" };
255  const children: { model: string; thinking: unknown; users: string[] }[] = [];
256  faux.setResponses(Array.from({ length: 8 }, () => (request, options, _, chosen) => {
257    const users = request.messages.flatMap((m) => m.role === "user" ? [textOf(m)] : []);
258    if (users[0] !== "go") {
259      children.push({ model: chosen.id, thinking: options?.reasoning, users });
260      return fauxAssistantMessage(`${users.at(-1)} after ${users.length - 1}`);
261    }
262    if (last(request.messages).role === "toolResult") return fauxAssistantMessage("done");
263    return script(`
264      const first = await tools.subagent({ task: "one", model: ${JSON.stringify(worker)},
265        thinkingLevel: "low" });
266      const more = await Promise.all(["two", "three"].map((task) =>
267        tools.subagent({ task, conversation: first.conversation })));
268      text(JSON.stringify([first, ...more]));
269    `);
270  }));
271  await berry.enableModels([model, worker]);
272  const tid = await berry.create({ extensions: { subagent: {} }, model });
273  const parent = (await berry.conversation(tid))!;
274  await within((await parent.submit({ type: "input", content: "go" }, context)).wait(context));
275  await within(parent.waitForIdle(context));
276  const result = lastToolResult((await parent.context(context)).messages);
277  assert(!result.isError);
278  const answers = JSON.parse(result.text.slice(result.text.indexOf("["))) as {
279    conversation: string;
280    answer: string;
281  }[];
282  const index = (await berry.harness.snapshot(Conversations, context))!;
283  const [child] = Object.keys(index).filter((key) => index[key].owner === tid);
284  assertEquals(answers.map((a) => a.conversation), [child, child, child]);
285  const [task, after] = [0, 1].map((i) => answers.map((a) => a.answer.split(" after ")[i]));
286  assertEquals(task, ["one", "two", "three"]);
287  assertEquals([after[0], ...after.slice(1).sort()], ["0", "1", "2"]);
288  assertEquals(children.map((c) => c.users.length), [1, 2, 3]);
289  assertEquals(children[2].users[0], "one");
290  for (const request of children) {
291    assertEquals([request.model, request.thinking], ["worker", "low"]);
292  }
293});
294
295Deno.test("ending a codemode script cancels its unawaited subagents, new or continued, while the parent continues", async () => {
296  await using harness = await setup();
297  const { berry, faux, model } = harness;
298  const blocker = blocking();
299  await berry.install(blocker.extension);
300  const tid = await berry.create({ extensions: { subagent: {}, blocking: {} }, model });
301  const parent = (await berry.conversation(tid))!;
302  const unawaited = async (args: JsonObject) => {
303    faux.setResponses([
304      script(
305        `tools.subagent(${JSON.stringify(args)}); await tools.ready({}); return "moving on";`,
306      ),
307      fauxAssistantMessage(fauxToolCall("block", {}), { stopReason: "toolUse" }),
308      fauxAssistantMessage("done"),
309    ]);
310    const cancelled = blocker.cancelled.length;
311    const submission = await parent.submit({ type: "input", content: "go" }, context);
312    await until(() => blocker.cancelled.length > cancelled);
313    assertEquals((await within(submission.wait(context))).status, "done");
314    await within(berry.harness.waitForIdle(context));
315    const result = lastToolResult((await parent.context(context)).messages);
316    assert(!result.isError);
317    assertStringIncludes(result.text, "moving on");
318    const graph = await berry.harness.taskGraph(context);
319    assertEquals(Object.keys(graph.value.tasks), []);
320    graph.dispose();
321  };
322
323  await unawaited({ task: "block", model });
324  const index = (await berry.harness.snapshot(Conversations, context))!;
325  const [child] = Object.keys(index).filter((key) => index[key].owner === tid);
326  await unawaited({ task: "block", conversation: child });
327  const childId = (await berry.conversation(child))!.id;
328  assertEquals((await berry.harness.snapshot(LiveDoc, childId, context))?.run, undefined);
329});
330
331Deno.test("delegation fans out on a selected cheaper model with inherited configuration and no recursive delegation", async () => {
332  const faux = fauxProvider({
333    models: [
334      { id: "parent-thinker", reasoning: true },
335      { id: "cheap-worker", reasoning: false },
336    ],
337  });
338  const models = createModels();
339  models.setProvider(faux.provider);
340  const berry = await openBerry(new MemoryStorage(), models, undefined, {
341    open: (name, cwd) =>
342      Promise.resolve(Object.assign(localEnvironment(cwd ?? Deno.cwd()), { name })),
343    close: () => Promise.resolve(),
344  });
345  const model = { provider: "faux", modelId: "parent-thinker" };
346  const worker = { provider: "faux", modelId: "cheap-worker" };
347  await berry.enableModels([model, worker]);
348  await berry.install(dir());
349  const entered = Promise.withResolvers<void>();
350  const release = Promise.withResolvers<void>();
351  const held: { name: string; tid: string; cwd: string; machine: string }[] = [];
352  const requests: (readonly Message[])[] = [];
353  await berry.install(defineExtension({
354    name: "workers",
355    initial: () => null,
356    setup(ext) {
357      ext.tool({
358        name: "hold",
359        description: "hold until released",
360        parameters: Type.Object({ name: Type.String() }),
361        expose: "both",
362        async execute({ name }, ctx) {
363          held.push({ name, tid: ctx.tid, cwd: ctx.env.cwd, machine: ctx.env.name });
364          if (held.length === 2) entered.resolve();
365          await release.promise;
366          return `answer ${name}`;
367        },
368      });
369    },
370  }));
371  const tid = await berry.create({
372    extensions: {
373      subagent: {},
374      dir: { cwd: "/chosen" },
375      workers: {},
376      environment: { environment: "remote", cwd: Deno.cwd() },
377    },
378    model,
379    thinkingLevel: "high",
380  });
381  const parent = (await berry.conversation(tid))!;
382  await (await berry.emit(tid, "dir", { type: "cd", cwd: "/updated" })).wait(context);
383  const router = (request: { messages: readonly Message[] }) => {
384    requests.push(request.messages);
385    const message = last(request.messages);
386    const text = textOf(message);
387    if (message.role === "user" && text === "go") {
388      return fauxAssistantMessage([
389        fauxToolCall("subagent", { task: "alpha", model: worker }, { id: "alpha" }),
390        fauxToolCall("subagent", { task: "beta", model: worker }, { id: "beta" }),
391      ], { stopReason: "toolUse" });
392    }
393    if (message.role === "user") {
394      assert(["alpha", "beta"].includes(text));
395      assert(!request.messages.some((m) => m.role === "user" && textOf(m) === "go"));
396      assert(!toolsOffered(request.messages).includes("subagent"));
397      assertStringIncludes(
398        sectionsSeen(request.messages).find((s) => s.dir)?.dir ?? "",
399        "/updated",
400      );
401      return fauxAssistantMessage(fauxToolCall("hold", { name: text }), {
402        stopReason: "toolUse",
403      });
404    }
405    if (message.role === "toolResult" && message.toolName === "hold") {
406      return fauxAssistantMessage(text);
407    }
408    return fauxAssistantMessage("done");
409  };
410  faux.setResponses(Array.from({ length: 15 }, () => (request, _options, _state, selected) => {
411    const isParent = request.messages.some((m) => m.role === "user" && textOf(m) === "go");
412    assertEquals(
413      { provider: selected.provider, modelId: selected.id },
414      isParent ? model : worker,
415    );
416    return router(request);
417  }));
418  try {
419    const submission = await parent.submit({ type: "input", content: "go" }, context);
420    await within(entered.promise);
421    assert((await berry.harness.snapshot(LiveDoc, parent.id, context))?.run);
422    assertEquals(held.map((item) => item.name).sort(), ["alpha", "beta"]);
423    assertEquals(new Set(held.map((item) => item.tid)).size, 2);
424    const index = (await berry.harness.snapshot(Conversations, context))!;
425    const parentAgent = await parent.agent(context);
426    assertEquals(parentAgent.model, model);
427    assertEquals(parentAgent.thinkingLevel, "high");
428    const regular = await berry.create({ extensions: { dir: {}, workers: {} }, model: worker });
429    const regularAgent = await berry.harness.snapshot(
430      AgentDoc,
431      (await berry.conversation(regular))!.id,
432      context,
433    );
434    for (const child of held) {
435      assertEquals(index[child.tid].owner, tid);
436      assertEquals(child.cwd, Deno.cwd());
437      assertEquals(child.machine, "remote");
438      assertEquals(await berry.state(child.tid, "dir"), { cwd: "/updated" });
439      const conversation = (await berry.conversation(child.tid))!;
440      const stored = await berry.harness.snapshot(AgentDoc, conversation.id, context);
441      assertEquals(stored?.extensions, regularAgent?.extensions);
442      const agent = await conversation.agent(context);
443      assertEquals(agent.model, worker);
444      assertEquals(agent.thinkingLevel, "off");
445      assertEquals(
446        agent.extensions.map((ext) => ext.name),
447        parentAgent.extensions.map((ext) => ext.name).filter((name) => name !== "subagent"),
448      );
449    }
450    release.resolve();
451    assertEquals((await within(submission.wait(context))).status, "done");
452    await within(parent.waitForIdle(context));
453    assertEquals((await parent.agent(context)).model, model);
454    assertEquals((await parent.agent(context)).thinkingLevel, "high");
455    const resumed = requests.at(-1)!;
456    const results = resumed.filter((m) => m.role === "toolResult");
457    const text = results.map(textOf).join("\n");
458    assertStringIncludes(text, "answer alpha");
459    assertStringIncludes(text, "answer beta");
460    for (const result of results) {
461      const details = result.role === "toolResult" ? result.details as { tid?: string } : undefined;
462      assert(held.some((child) => child.tid === details?.tid));
463    }
464    const graph = await berry.harness.taskGraph(context);
465    assertEquals(Object.keys(graph.value.tasks), []);
466    graph.dispose();
467  } finally {
468    release.resolve();
469    await berry.close();
470  }
471});
472
473Deno.test("delegation is reserved and an owned conversation cannot re-enable it to delegate", async () => {
474  await using harness = await setup();
475  const { berry, faux, model } = harness;
476  await assertRejects(
477    () => berry.install(defineExtension({ name: "subagent", initial: () => null, setup() {} })),
478    Error,
479    "harness's own",
480  );
481  const tid = await berry.create({ extensions: { subagent: {} }, model });
482  const parent = (await berry.conversation(tid))!;
483  const anchor = Promise.withResolvers<void>();
484  let childTid = "";
485  await berry.install(defineExtension({
486    name: "capture",
487    initial: () => null,
488    setup(ext) {
489      ext.tool({
490        name: "capture",
491        description: "capture child",
492        parameters: Type.Object({}),
493        expose: "model",
494        execute(_, ctx) {
495          childTid = ctx.tid;
496          anchor.resolve();
497          return "captured";
498        },
499      });
500    },
501  }));
502  await berry.configure(tid, { extensions: ["subagent", "capture"] });
503  faux.setResponses([
504    fauxAssistantMessage(fauxToolCall("subagent", { task: "child", model }), {
505      stopReason: "toolUse",
506    }),
507    fauxAssistantMessage(fauxToolCall("capture", {}), { stopReason: "toolUse" }),
508    fauxAssistantMessage("child answer"),
509    fauxAssistantMessage("done"),
510  ]);
511  await within((await parent.submit({ type: "input", content: "go" }, context)).wait(context));
512  await within(anchor.promise);
513  await berry.configure(childTid, { extensions: ["subagent"] });
514  faux.setResponses([
515    fauxAssistantMessage(fauxToolCall("subagent", { task: "grandchild", model }), {
516      stopReason: "toolUse",
517    }),
518    fauxAssistantMessage("done"),
519  ]);
520  const child = (await berry.conversation(childTid))!;
521  await within(
522    (await child.submit({ type: "input", content: "try again" }, context)).wait(context),
523  );
524  const entries = (await child.entries({ order: "ascending" }, 256, undefined, context)).items;
525  const result = entries.flatMap((entry) => entry.model ?? []).findLast((m) =>
526    m.role === "toolResult"
527  );
528  assert(result?.role === "toolResult" && result.isError);
529  assertStringIncludes(textOf(result), "Only top-level agents");
530  assertEquals(Object.keys((await berry.harness.snapshot(Conversations, context))!).length, 2);
531});
532
533/** Answers "go" with two subagent calls, alpha and beta, and has each child block under its task's name. */
534const fanOut = (
535  model: { provider: string; modelId: string },
536  request: { messages: readonly Message[] },
537) => {
538  const message = last(request.messages);
539  if (message.role === "user" && textOf(message) === "go") {
540    return fauxAssistantMessage([
541      fauxToolCall("subagent", { task: "alpha", model }, { id: "alpha" }),
542      fauxToolCall("subagent", { task: "beta", model }, { id: "beta" }),
543    ], { stopReason: "toolUse" });
544  }
545  if (message.role === "user") {
546    return fauxAssistantMessage(fauxToolCall("block", { name: textOf(message) }), {
547      stopReason: "toolUse",
548    });
549  }
550  if (message.role === "toolResult" && message.toolName === "block") {
551    return fauxAssistantMessage(textOf(message));
552  }
553  return fauxAssistantMessage("done");
554};
555
556Deno.test("aborting the parent stops every foreground child", async () => {
557  await using harness = await setup();
558  const { berry, faux, model } = harness;
559  const blocker = blocking();
560  await berry.install(blocker.extension);
561  const tid = await berry.create({ extensions: { subagent: {}, blocking: {} }, model });
562  const parent = (await berry.conversation(tid))!;
563  faux.setResponses(Array.from({ length: 10 }, () => (request) => fanOut(model, request)));
564  const submission = await parent.submit({ type: "input", content: "go" }, context);
565  await until(() => blocker.running === 2);
566  await within(parent.abort(context));
567  const settled = await within(submission.wait(context));
568  assert(settled.status === "unanswered" && settled.reason === "aborted");
569  assertEquals(blocker.cancelled.sort(), ["alpha", "beta"]);
570  const graph = await berry.harness.taskGraph(context);
571  assertEquals(Object.keys(graph.value.tasks), []);
572  graph.dispose();
573});
574
575Deno.test("a failed delegation leaves its sibling working", async () => {
576  await using harness = await setup();
577  const { berry, faux, model } = harness;
578  const blocker = blocking();
579  await berry.install(blocker.extension);
580  let resumed: readonly Message[] = [];
581  const tid = await berry.create({ extensions: { subagent: {}, blocking: {} }, model });
582  const parent = (await berry.conversation(tid))!;
583  faux.setResponses(Array.from({ length: 15 }, () => (request) => {
584    const message = last(request.messages);
585    if (message.role === "toolResult" && message.toolName === "subagent") {
586      resumed = request.messages;
587    }
588    return fanOut(model, request);
589  }));
590  try {
591    const submission = await parent.submit({ type: "input", content: "go" }, context);
592    await until(() => blocker.running === 2);
593    await within((await berry.conversation(blocker.started.get("alpha")!))!.abort(context));
594    assertEquals(blocker.cancelled, ["alpha"]);
595    assert((await berry.harness.snapshot(LiveDoc, parent.id, context))?.run);
596    blocker.release.resolve("beta finished");
597    assertEquals((await within(submission.wait(context))).status, "done");
598    await within(parent.waitForIdle(context));
599    const results = resumed.filter((m) => m.role === "toolResult").map(textOf).join("\n");
600    assertStringIncludes(results, "Subagent failed: aborted");
601    const failure = resumed.find((m) => m.role === "toolResult" && m.isError)!;
602    assertStringIncludes(textOf(failure), blocker.started.get("alpha")!);
603    assertStringIncludes(results, "beta finished");
604    assertEquals(blocker.cancelled, ["alpha"]);
605  } finally {
606    blocker.release.resolve("cleanup");
607  }
608});
609
610Deno.test("an aborted codemode subagent reports a conversation the parent can continue", async () => {
611  await using harness = await setup();
612  const { berry, faux, model } = harness;
613  const blocker = blocking();
614  await berry.install(blocker.extension);
615  const tid = await berry.create({ extensions: { subagent: {}, blocking: {} }, model });
616  const parent = (await berry.conversation(tid))!;
617  faux.setResponses([
618    script(`return await tools.subagent({ task: "block", model: ${JSON.stringify(model)} });`),
619    fauxAssistantMessage(fauxToolCall("block", {}), { stopReason: "toolUse" }),
620    fauxAssistantMessage("paused"),
621  ]);
622  const submission = await parent.submit({ type: "input", content: "go" }, context);
623  await until(() => blocker.running === 1);
624  const child = blocker.started.get("")!;
625  await within((await berry.conversation(child))!.abort(context));
626  assertEquals((await within(submission.wait(context))).status, "done");
627  const result = lastToolResult((await parent.context(context)).messages);
628  assert(result.isError);
629  assertStringIncludes(result.text, "aborted");
630  assertStringIncludes(result.text, child);
631
632  const reported = result.text.match(/subagent conversation ([^;]+);/)?.[1];
633  assertEquals(reported, child);
634  const seen = await turn(harness, tid, [
635    script(
636      `return await tools.subagent({ task: "continue", conversation: ${
637        JSON.stringify(reported)
638      } });`,
639    ),
640    fauxAssistantMessage("recovered answer"),
641  ]);
642  const continued = lastToolResult(seen.at(-1)!);
643  assert(!continued.isError);
644  assertStringIncludes(continued.text, "recovered answer");
645  assertStringIncludes(continued.text, child);
646});
647
648// a codemode script cannot resume after a restart, so recovery differs by how the call was made
649for (const mode of ["direct", "codemode"] as const) {
650  Deno.test(`${mode} delegation recovers without duplicating children or submissions`, async () => {
651    await using temporary = await tempDir();
652    const directory = temporary.path;
653    const faux = fauxProvider({
654      models: [{ id: "parent", reasoning: true }, { id: "worker", reasoning: true }],
655    });
656    const models = createModels();
657    models.setProvider(faux.provider);
658    const model = { provider: "faux", modelId: "parent" };
659    const worker = { provider: "faux", modelId: "worker" };
660    const open = () =>
661      openNodeSqliteStorage(`${directory}/session.sqlite`).then((storage) =>
662        openBerry(storage, models)
663      );
664    let berry = await open();
665    const blocker = blocking();
666    try {
667      await berry.install(blocker.extension);
668      await berry.enableModels([model, worker]);
669      const tid = await berry.create({
670        extensions: { subagent: {}, blocking: {} },
671        model,
672        thinkingLevel: "high",
673      });
674      const parent = (await berry.conversation(tid))!;
675      const args = { task: "child", model: worker, thinkingLevel: "low" };
676      faux.setResponses([
677        mode === "codemode"
678          ? script(`return await tools.subagent(${JSON.stringify(args)});`)
679          : fauxAssistantMessage(fauxToolCall("subagent", args), { stopReason: "toolUse" }),
680        fauxAssistantMessage(fauxToolCall("block", {}), { stopReason: "toolUse" }),
681      ]);
682      const submission = await parent.submit({ type: "input", content: "go" }, context);
683      await until(() => blocker.running > 0);
684      const before = (await berry.harness.snapshot(Conversations, context))!;
685      const [childTid] = Object.keys(before).filter((key) => key !== tid);
686      assertEquals((await (await berry.conversation(childTid))!.agent(context)).model, worker);
687      assertEquals(
688        (await (await berry.conversation(childTid))!.agent(context)).thinkingLevel,
689        "low",
690      );
691      const resumed: (readonly Message[])[] = [];
692      await within(berry.close());
693      faux.setResponses(Array.from({ length: 10 }, () => (request) => {
694        resumed.push(request.messages);
695        const message = last(request.messages);
696        assert(message.role === "toolResult");
697        return fauxAssistantMessage(
698          message.toolName === "block" ? "recovered child answer" : "done",
699        );
700      }));
701      berry = await open();
702      await berry.install(blocker.extension);
703      berry.harness.resume();
704      const receipt = (await berry.harness.submission(submission.id, context))!;
705      assertEquals((await within(receipt.wait(context))).status, "done");
706      await within(berry.harness.waitForIdle(context));
707      assert(
708        (await (await berry.conversation(tid))!.agent(context)).extensions.some((ext) =>
709          ext.name === "subagent"
710        ),
711      );
712      const after = (await berry.harness.snapshot(Conversations, context))!;
713      assertEquals(Object.keys(after).sort(), Object.keys(before).sort());
714      const child = (await berry.conversation(childTid))!;
715      assertEquals((await child.agent(context)).model, worker);
716      assertEquals((await child.agent(context)).thinkingLevel, "low");
717      assertEquals((await (await berry.conversation(tid))!.agent(context)).model, model);
718      assertEquals((await (await berry.conversation(tid))!.agent(context)).thinkingLevel, "high");
719      const entries = (await child.entries({ order: "ascending" }, 256, undefined, context)).items;
720      assertEquals(
721        entries.flatMap((entry) => entry.model ?? []).filter((m) => m.role === "user").length,
722        1,
723      );
724      const result = lastToolResult(resumed.at(-1)!);
725      if (mode === "direct") {
726        assertEquals(result.text.split("\n")[0], "recovered child answer");
727        assertStringIncludes(result.text, childTid);
728        assert(!result.isError);
729        assertEquals(resumed.length, 2);
730      } else {
731        assert(result.isError);
732        assertStringIncludes(result.text.toLowerCase(), "interrupted");
733        assertEquals(resumed.length, 1);
734        assertEquals((await berry.harness.snapshot(LiveDoc, child.id, context))?.run, undefined);
735      }
736    } finally {
737      await berry.close();
738    }
739  });
740}
741
742Deno.test("subagent guidance is editable prose with enabled models always present", async () => {
743  await using harness = await setup(undefined, {
744    models: [{ id: "parent" }, { id: "worker" }],
745  });
746  const { berry, model } = harness;
747  const worker = { provider: "faux", modelId: "worker" };
748  await berry.enableModels([model, worker]);
749  const def = berry.promptProseDefault("subagent");
750  assert(def?.trim());
751
752  const tid = await berry.create({ extensions: { subagent: {} }, model });
753  const [first] = await turn(harness, tid, []);
754  const seen = sectionsSeen(first)[0];
755  assertStringIncludes(seen.subagent!, def!);
756  assertStringIncludes(seen.subagent!, "Enabled models:");
757  assertStringIncludes(seen.subagent!, "worker");
758
759  await berry.setSystemPrompt({
760    userName: "Sam",
761    promptProse: { subagent: "Custom guidance for $USER." },
762  });
763  const [overrode] = await turn(harness, tid, []);
764  const patch = sectionsSeen(overrode).at(-1)!;
765  assertStringIncludes(patch.subagent!, "Custom guidance for Sam.");
766  assertStringIncludes(patch.subagent!, "Enabled models:");
767  assertStringIncludes(patch.subagent!, "worker");
768  assert(!patch.subagent!.includes(def!));
769
770  await berry.setSystemPrompt({ promptProse: { subagent: "" } });
771  const [blanked] = await turn(harness, tid, []);
772  const cleared = sectionsSeen(blanked).at(-1)!;
773  assertStringIncludes(cleared.subagent!, "Enabled models:");
774  assertStringIncludes(cleared.subagent!, "worker");
775  assert(!cleared.subagent!.includes("Custom guidance"));
776});
777
778Deno.test("a background spawn returns at once and its report wakes an idle parent", async () => {
779  await using harness = await setup();
780  const { berry, faux, model } = harness;
781  const blocker = blocking();
782  await berry.install(blocker.extension);
783  const tid = await berry.create({ extensions: { subagent: {}, blocking: {} }, model });
784  const parent = (await berry.conversation(tid))!;
785  let spawned = false;
786  faux.setResponses(Array.from({ length: 20 }, () => (request) => {
787    const message = last(request.messages);
788    const text = textOf(message);
789    if (message.role === "user" && text === "go" && !spawned) {
790      spawned = true;
791      return fauxAssistantMessage(
792        fauxToolCall("codemode", {
793          code: `return await tools.subagent({task:"child work", model:${
794            JSON.stringify(model)
795          }, background:true})`,
796        }),
797        { stopReason: "toolUse" },
798      );
799    }
800    if (message.role === "user" && text === "child work") {
801      return fauxAssistantMessage(fauxToolCall("block", {}), { stopReason: "toolUse" });
802    }
803    if (message.role === "toolResult" && message.toolName === "block") {
804      return fauxAssistantMessage(text);
805    }
806    return fauxAssistantMessage("done");
807  }));
808  const submission = await parent.submit({ type: "input", content: "go" }, context);
809  await until(() => blocker.running > 0);
810  // the spawn returns without waiting for the child, so the parent idles while it stays blocked
811  await within(parent.waitForIdle(context));
812  assertEquals(blocker.running, 1);
813  assertEquals((await within(submission.wait(context))).status, "done");
814  const index = (await berry.harness.snapshot(Conversations, context))!;
815  const [child] = Object.keys(index).filter((key) => index[key].owner === tid);
816  assert(child);
817  blocker.release.resolve("child answer");
818  const reports = await within((async () => {
819    for (;;) {
820      const items = (await parent.entries({ order: "ascending" }, 256, undefined, context)).items;
821      const found = items.filter((entry) => entry.kind === "berry.subagent-report");
822      if (found.length > 0) return found;
823      await new Promise((resolve) => setTimeout(resolve, 20));
824    }
825  })());
826  assertEquals(reports.length, 1);
827  const data = reports[0].data as { tid: string; task: string; status: string; answer?: string };
828  assertEquals(data.tid, child);
829  assertEquals(data.task, "child work");
830  assertEquals(data.status, "completed");
831  assertEquals(data.answer, "child answer");
832  const messages = (await parent.context(context)).messages;
833  assert(messages.some((message) => textOf(message).includes("child answer")));
834});
835
836Deno.test("the cap refuses a spawn and allows one after a stop", async () => {
837  await using harness = await setup();
838  const { berry, faux, model } = harness;
839  const blocker = blocking();
840  await berry.install(blocker.extension);
841  const tid = await berry.create({ extensions: { subagent: {}, blocking: {} }, model });
842  await berry.configureExtension(tid, "subagent", { models: [], limit: 1 });
843  const parent = (await berry.conversation(tid))!;
844  const scripts = [
845    `return await tools.subagent({task:"one", model:${JSON.stringify(model)}, background:true})`,
846    `return await tools.subagent({task:"two", model:${JSON.stringify(model)}, background:true})`,
847    `return await tools.subagent_stop({})`,
848    `return await tools.subagent({task:"two", model:${JSON.stringify(model)}, background:true})`,
849  ];
850  faux.setResponses(Array.from({ length: 30 }, () => (request) => {
851    const message = last(request.messages);
852    const text = textOf(message);
853    if (message.role === "user" && text === "go") {
854      const code = scripts.shift() ?? "return 0;";
855      return fauxAssistantMessage(fauxToolCall("codemode", { code }), { stopReason: "toolUse" });
856    }
857    if (message.role === "user" && (text === "one" || text === "two")) {
858      return fauxAssistantMessage(fauxToolCall("block", {}), { stopReason: "toolUse" });
859    }
860    return fauxAssistantMessage("done");
861  }));
862  const run = async () => {
863    await within((await parent.submit({ type: "input", content: "go" }, context)).wait(context));
864    await within(parent.waitForIdle(context));
865    return lastToolResult((await parent.context(context)).messages);
866  };
867  const first = await run();
868  assert(!first.isError);
869  assertStringIncludes(first.text, "running");
870  await until(() => blocker.running > 0);
871  const refused = await run();
872  assert(refused.isError);
873  assertStringIncludes(refused.text, "Subagent limit reached");
874  assertStringIncludes(refused.text, "1 running");
875  assertStringIncludes(refused.text, "subagent_wait");
876  assertStringIncludes(refused.text, "subagent_stop");
877  const stopped = await run();
878  assert(!stopped.isError);
879  assertStringIncludes(stopped.text, "stopped");
880  const retry = await run();
881  assert(!retry.isError);
882  assertStringIncludes(retry.text, "running");
883});
884
885Deno.test("subagent_wait collects a settled child and times out without killing work", async () => {
886  await using harness = await setup();
887  const { berry, faux, model } = harness;
888  const blocker = blocking();
889  await berry.install(blocker.extension);
890  const tid = await berry.create({ extensions: { subagent: {}, blocking: {} }, model });
891  const parent = (await berry.conversation(tid))!;
892  const scripts: string[] = [
893    `const spawned = await tools.subagent({task:"waited", model:${
894      JSON.stringify(model)
895    }, background:true}); store("child", spawned.conversation); return spawned;`,
896    `return await tools.subagent_wait({timeoutMs:50})`,
897    `const child = load("child"); return await tools.subagent_wait({conversations:[child]})`,
898  ];
899  faux.setResponses(Array.from({ length: 40 }, () => (request) => {
900    const message = last(request.messages);
901    const text = textOf(message);
902    if (message.role === "user" && text === "go") {
903      const code = scripts.shift() ?? "return 0;";
904      return fauxAssistantMessage(fauxToolCall("codemode", { code }), { stopReason: "toolUse" });
905    }
906    if (message.role === "user" && text === "waited") {
907      return fauxAssistantMessage(fauxToolCall("block", {}), { stopReason: "toolUse" });
908    }
909    if (message.role === "toolResult" && message.toolName === "block") {
910      return fauxAssistantMessage(text);
911    }
912    return fauxAssistantMessage("done");
913  }));
914  const run = async () => {
915    await within((await parent.submit({ type: "input", content: "go" }, context)).wait(context));
916    await within(parent.waitForIdle(context));
917    return lastToolResult((await parent.context(context)).messages);
918  };
919  const spawned = await run();
920  assert(!spawned.isError);
921  await until(() => blocker.running > 0);
922  const index = (await berry.harness.snapshot(Conversations, context))!;
923  const [child] = Object.keys(index).filter((key) => index[key].owner === tid);
924  assert(child);
925  // the wait times out while the child stays blocked, so the work survives it
926  const timedOut = await run();
927  assert(!timedOut.isError);
928  assertStringIncludes(timedOut.text, "running");
929  assertStringIncludes(timedOut.text, child);
930  assertEquals(blocker.running, 1);
931  blocker.release.resolve("waited answer");
932  const collected = await run();
933  assert(!collected.isError);
934  assertStringIncludes(collected.text, "done");
935  assertStringIncludes(collected.text, "waited answer");
936  assertStringIncludes(collected.text, child);
937});
938
939Deno.test("subagent_stop stops the child and leaves a note without starting a run", async () => {
940  await using harness = await setup();
941  const { berry, faux, model } = harness;
942  const blocker = blocking();
943  await berry.install(blocker.extension);
944  const tid = await berry.create({ extensions: { subagent: {}, blocking: {} }, model });
945  const parent = (await berry.conversation(tid))!;
946  const scripts = [
947    `return await tools.subagent({task:"stop me", model:${
948      JSON.stringify(model)
949    }, background:true})`,
950    `return await tools.subagent_stop({})`,
951  ];
952  faux.setResponses(Array.from({ length: 30 }, () => (request) => {
953    const message = last(request.messages);
954    const text = textOf(message);
955    if (message.role === "user" && text === "go") {
956      const code = scripts.shift() ?? "return 0;";
957      return fauxAssistantMessage(fauxToolCall("codemode", { code }), { stopReason: "toolUse" });
958    }
959    if (message.role === "user" && text === "stop me") {
960      return fauxAssistantMessage(fauxToolCall("block", { name: "stop me" }), {
961        stopReason: "toolUse",
962      });
963    }
964    return fauxAssistantMessage("done");
965  }));
966  const run = async () => {
967    await within((await parent.submit({ type: "input", content: "go" }, context)).wait(context));
968    await within(parent.waitForIdle(context));
969    return lastToolResult((await parent.context(context)).messages);
970  };
971  const spawned = await run();
972  assert(!spawned.isError);
973  await until(() => blocker.running > 0);
974  const index = (await berry.harness.snapshot(Conversations, context))!;
975  const [child] = Object.keys(index).filter((key) => index[key].owner === tid);
976  assert(child);
977  const stopped = await run();
978  assert(!stopped.isError);
979  assertStringIncludes(stopped.text, "stopped");
980  assertStringIncludes(stopped.text, child);
981  assertEquals(blocker.running, 0);
982  assertEquals(blocker.cancelled, ["stop me"]);
983  const entries = (await parent.entries({ order: "ascending" }, 256, undefined, context)).items;
984  const notes = entries.filter((entry) => entry.kind === "berry.subagent-report");
985  assertEquals(notes.length, 1);
986  const data = notes[0].data as { tid: string; task: string; status: string };
987  assertEquals(data.tid, child);
988  assertEquals(data.task, "stop me");
989  assertEquals(data.status, "stopped");
990  // the note does not wake the parent: no new run starts on its own
991  const count = entries.length;
992  await new Promise((resolve) => setTimeout(resolve, 100));
993  await within(parent.waitForIdle(context));
994  assertEquals((await berry.harness.snapshot(LiveDoc, parent.id, context))?.run, undefined);
995  const later = (await parent.entries({ order: "ascending" }, 256, undefined, context)).items;
996  assertEquals(later.length, count);
997});
998
999Deno.test("a background delegation survives a restart and reports exactly once", async () => {
1000  await using temporary = await tempDir();
1001  const directory = temporary.path;
1002  const faux = fauxProvider({ models: [{ id: "parent" }, { id: "worker" }] });
1003  const models = createModels();
1004  models.setProvider(faux.provider);
1005  const model = { provider: "faux", modelId: "parent" };
1006  const worker = { provider: "faux", modelId: "worker" };
1007  const open = () =>
1008    openNodeSqliteStorage(`${directory}/session.sqlite`).then((storage) =>
1009      openBerry(storage, models)
1010    );
1011  let berry = await open();
1012  const blocker = blocking();
1013  try {
1014    await berry.install(blocker.extension);
1015    await berry.enableModels([model, worker]);
1016    const tid = await berry.create({ extensions: { subagent: {}, blocking: {} }, model });
1017    const parent = (await berry.conversation(tid))!;
1018    const respond = () => (request: { messages: readonly Message[] }) => {
1019      const message = last(request.messages);
1020      const text = textOf(message);
1021      if (message.role === "user" && text === "go") {
1022        return fauxAssistantMessage(
1023          fauxToolCall("codemode", {
1024            code: `return await tools.subagent({task:"restart work", model:${
1025              JSON.stringify(worker)
1026            }, background:true})`,
1027          }),
1028          { stopReason: "toolUse" },
1029        );
1030      }
1031      if (message.role === "user" && text === "restart work") {
1032        return fauxAssistantMessage(fauxToolCall("block", {}), { stopReason: "toolUse" });
1033      }
1034      if (message.role === "toolResult" && message.toolName === "block") {
1035        return fauxAssistantMessage(text);
1036      }
1037      return fauxAssistantMessage("done");
1038    };
1039    faux.setResponses(Array.from({ length: 20 }, respond));
1040    await parent.submit({ type: "input", content: "go" }, context);
1041    await until(() => blocker.running > 0);
1042    const before = (await berry.harness.snapshot(Conversations, context))!;
1043    const [childTid] = Object.keys(before).filter((key) => key !== tid);
1044    assert(childTid);
1045    await within(berry.close());
1046    // after a restart the interrupted block settles and the child answers anew
1047    faux.setResponses(
1048      Array.from({ length: 20 }, () => (request: { messages: readonly Message[] }) => {
1049        const message = last(request.messages);
1050        if (message.role === "toolResult" && message.toolName === "block") {
1051          return fauxAssistantMessage("restarted answer");
1052        }
1053        return fauxAssistantMessage("done");
1054      }),
1055    );
1056    berry = await open();
1057    await berry.install(blocker.extension);
1058    berry.harness.resume();
1059    const parentAfter = (await berry.conversation(tid))!;
1060    await within(berry.harness.waitForIdle(context));
1061    const after = (await berry.harness.snapshot(Conversations, context))!;
1062    assertEquals(Object.keys(after).sort(), Object.keys(before).sort());
1063    const entries =
1064      (await parentAfter.entries({ order: "ascending" }, 256, undefined, context)).items;
1065    const reports = entries.filter((entry) => entry.kind === "berry.subagent-report");
1066    assertEquals(reports.length, 1);
1067    const data = reports[0].data as { tid: string; task: string; status: string; answer?: string };
1068    assertEquals(data.tid, childTid);
1069    assertEquals(data.task, "restart work");
1070    assertEquals(data.status, "completed");
1071    assertEquals(data.answer, "restarted answer");
1072  } finally {
1073    await berry.close();
1074  }
1075});
1076
1077Deno.test("continuing a subagent in the background reports the new answer and stops", async () => {
1078  await using harness = await setup();
1079  const { berry, faux, model } = harness;
1080  const blocker = blocking();
1081  await berry.install(blocker.extension);
1082  const tid = await berry.create({ extensions: { subagent: {}, blocking: {} }, model });
1083  const parent = (await berry.conversation(tid))!;
1084  const scripts = [
1085    `const first = await tools.subagent({task:"one", model:${
1086      JSON.stringify(model)
1087    }, background:true}); store("child", first.conversation); return first;`,
1088    `const child = load("child"); return await tools.subagent({task:"two", conversation: child, background:true});`,
1089    `const child = load("child"); return await tools.subagent({task:"three", conversation: child, background:true});`,
1090    `const child = load("child"); return await tools.subagent_stop({conversations:[child]});`,
1091  ];
1092  faux.setResponses(Array.from({ length: 40 }, () => (request) => {
1093    const message = last(request.messages);
1094    const text = textOf(message);
1095    if (message.role === "user" && text === "go") {
1096      const code = scripts.shift() ?? "return 0;";
1097      return fauxAssistantMessage(fauxToolCall("codemode", { code }), { stopReason: "toolUse" });
1098    }
1099    if (message.role === "user" && (text === "one" || text === "two")) {
1100      return fauxAssistantMessage(text === "one" ? "ans1" : "ans2");
1101    }
1102    if (message.role === "user" && text === "three") {
1103      return fauxAssistantMessage(fauxToolCall("block", { name: "three" }), {
1104        stopReason: "toolUse",
1105      });
1106    }
1107    if (message.role === "toolResult" && message.toolName === "block") {
1108      return fauxAssistantMessage(text);
1109    }
1110    return fauxAssistantMessage("done");
1111  }));
1112  const run = async () => {
1113    await within((await parent.submit({ type: "input", content: "go" }, context)).wait(context));
1114    await within(parent.waitForIdle(context));
1115    return lastToolResult((await parent.context(context)).messages);
1116  };
1117  const reportWith = (answer: string) =>
1118    within((async () => {
1119      for (;;) {
1120        const items = (await parent.entries({ order: "ascending" }, 256, undefined, context)).items;
1121        const match = items.find((entry) =>
1122          entry.kind === "berry.subagent-report" &&
1123          (entry.data as { answer?: string }).answer === answer
1124        );
1125        if (match) return match;
1126        await new Promise((resolve) => setTimeout(resolve, 20));
1127      }
1128    })());
1129  const spawned = await run();
1130  assert(!spawned.isError);
1131  const first = await reportWith("ans1");
1132  assertEquals((first.data as { task: string }).task, "one");
1133  const childTid = (first.data as { tid: string }).tid;
1134  assertEquals((await berry.harness.snapshot(Conversations, context))![childTid].title, "one");
1135  const continued = await run();
1136  assert(!continued.isError);
1137  assertStringIncludes(continued.text, "running");
1138  const second = await reportWith("ans2");
1139  assertEquals((second.data as { task: string }).task, "two");
1140  assertEquals((await berry.harness.snapshot(Conversations, context))![childTid].title, "two");
1141  assertEquals((second.data as { tid: string }).tid, (first.data as { tid: string }).tid);
1142  const live = await run();
1143  assert(!live.isError);
1144  await until(() => blocker.running > 0);
1145  const index = (await berry.harness.snapshot(Conversations, context))!;
1146  const [child] = Object.keys(index).filter((key) => index[key].owner === tid);
1147  assert(child);
1148  const stopped = await run();
1149  assert(!stopped.isError);
1150  assertStringIncludes(stopped.text, "stopped");
1151  assertEquals(blocker.running, 0);
1152  assertEquals(blocker.cancelled, ["three"]);
1153  assertEquals((await berry.harness.snapshot(Conversations, context))![childTid].title, "three");
1154  const entries = (await parent.entries({ order: "ascending" }, 256, undefined, context)).items;
1155  const note = entries.find((entry) =>
1156    entry.kind === "berry.subagent-report" &&
1157    (entry.data as { status?: string }).status === "stopped"
1158  );
1159  assert(note);
1160  assertEquals((note!.data as { tid: string }).tid, child);
1161  assertEquals((note!.data as { task: string }).task, "three");
1162});