import { assert, assertEquals, assertStringIncludes } from "@std/assert"; import type { ConversationId, EntryId, EntryRecord, LiveState } from "@earendil-works/pi-durable"; import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai/providers/faux"; import { withBerry } from "./testing.ts"; const entry = (id: number, kind: string, message: unknown) => ({ id: id as EntryId, conversationId: 1 as ConversationId, kind, model: [message], }) as EntryRecord; Deno.test("direct and codemode subagent cards link to the child, stream its transcript, retain its tool cards, and release their subscriptions", async (t) => { await withBerry(async ({ server, settle }) => { const { transcript } = await import("./transcript.tsx"); const { watch } = await import("./client.ts"); for (const mode of ["direct", "codemode"] as const) { await t.step(mode, async () => { const view = transcript(); const tid = "a2b3c4d5e6f7g"; const args = { task: "Investigate **tests**", model: { provider: "faux", modelId: "worker" }, }; const name = mode === "direct" ? "subagent" : "codemode"; const entries = [entry( 1, "pi.assistant", fauxAssistantMessage( fauxToolCall(name, mode === "direct" ? args : { code: "await tools.subagent(task);" }, { id: "call", }), { stopReason: "toolUse" }, ), )]; const nested = { name: "subagent", extension: "subagent", args, status: "running", details: { tid }, }; const details = mode === "direct" ? { tid } : { calls: [nested] }; try { view.update(entries, { run: { taskId: 2 }, tools: [{ callId: "call", name, status: "running", details }], } as unknown as LiveState); const link = () => view.element.querySelector("a.tid")?.getAttribute("href"); assertEquals(link(), `#/${tid}`); const pane = view.element.querySelector( "[aria-label='Subagent transcript']", )!; assert(pane); const body = pane.parentElement!; assertEquals(body.querySelector("strong")?.textContent, "tests"); assertStringIncludes(body.textContent!, "faux/worker"); const childEntries = [ entry(1, "pi.user", { role: "user", content: args.task, timestamp: 0 }), ]; const worker = fauxAssistantMessage( fauxToolCall("inspect", { path: "tests" }, { id: "inspect" }), { stopReason: "toolUse", }, ); childEntries.push(entry(2, "pi.assistant", worker)); server.snapshot(`conversation:${tid}`, { entries: childEntries, docs: { "pi.agent": { model: args.model, thinkingLevel: "high" }, "pi.live": { run: { taskId: 3 }, tools: [{ callId: "inspect", name: "inspect", status: "running" }], }, }, }); await settle(); assert(!pane.hidden); assertStringIncludes(body.textContent!, "high"); assertStringIncludes(pane.textContent!, args.task); const tool = pane.querySelector(".tool-header")!; tool.click(); childEntries.push(entry(3, "pi.tool-result", { role: "toolResult", toolCallId: "inspect", toolName: "inspect", content: [{ type: "text", text: "located the failing test" }], isError: false, timestamp: 0, })); const final = fauxAssistantMessage("The fix is **ready**."); server.snapshot(`conversation:${tid}`, { entries: childEntries, docs: { "pi.live": { generation: { message: final } } }, }); await settle(); assert(pane.querySelector(".tool-header") === tool); assertEquals(tool.getAttribute("aria-expanded"), "true"); assertStringIncludes(pane.textContent!, "located the failing test"); const paragraph = pane.querySelector(".message.assistant .markdown p")!; assertEquals(paragraph.querySelector("strong")?.textContent, "ready"); childEntries.push(entry(4, "pi.assistant", final)); server.snapshot(`conversation:${tid}`, { entries: childEntries, docs: {} }); const finishedDetails = mode === "direct" ? { tid } : { calls: [{ ...nested, status: "ok", value: { conversation: tid, answer: "The fix is **ready**." }, }], }; view.update([ ...entries, entry(2, "pi.tool-result", { role: "toolResult", toolCallId: "call", toolName: name, content: [{ type: "text", text: "The fix is **ready**." }], details: finishedDetails, isError: false, timestamp: 0, }), ], {}); await settle(); assert(pane.hidden); assertEquals(pane.textContent, ""); assertEquals(link(), `#/${tid}`); assertEquals(body.querySelector(".markdown:not([hidden]) strong")?.textContent, "ready"); assert(!body.textContent!.includes("located the failing test")); let cached = false; const release = watch(`conversation:${tid}`, () => cached = true); release(); assert(!cached); } finally { view.dispose(); } }); } }); }); Deno.test("continuations leave earlier answers alone and only the running call follows the child", async () => { await withBerry(async ({ server, settle }) => { const { transcript } = await import("./transcript.tsx"); const view = transcript(); const tid = "a2b3c4d5e6f7g"; const result = (call: string, text: string) => ({ role: "toolResult", toolCallId: call, toolName: "subagent", content: [{ type: "text", text }], details: { tid }, isError: false, timestamp: 0, }); const entries = [ entry( 1, "pi.assistant", fauxAssistantMessage([ fauxToolCall("subagent", { task: "first task" }, { id: "first" }), fauxToolCall("subagent", { task: "follow up", conversation: tid }, { id: "next" }), ], { stopReason: "toolUse" }), ), entry(2, "pi.tool-result", result("first", "First **answer**.")), ]; try { view.update(entries, { run: { taskId: 2 }, tools: [{ callId: "next", name: "subagent", status: "running", details: { tid } }], } as unknown as LiveState); server.snapshot(`conversation:${tid}`, { entries: [entry(1, "pi.assistant", fauxAssistantMessage("First **answer**."))], docs: { "pi.live": { generation: { message: fauxAssistantMessage("Following up now…") } } }, }); await settle(); const bodies = view.element.querySelectorAll(".subagent-body"); assertEquals(bodies.length, 2); assert(bodies[0].querySelector(".subagent-history")!.hidden); assertEquals(bodies[0].querySelector("strong")?.textContent, "answer"); assert(!bodies[0].textContent!.includes("Following up now")); assert(!bodies[1].querySelector(".subagent-history")!.hidden); assertStringIncludes(bodies[1].textContent!, "Following up now"); view.update( [...entries, entry(3, "pi.tool-result", result("next", "Second **answer**."))], {}, ); await settle(); assert(bodies[1].querySelector(".subagent-history")!.hidden); assert(!bodies[1].textContent!.includes("Following up now")); assertStringIncludes(bodies[1].textContent!, "Second answer"); assertStringIncludes(bodies[0].textContent!, "First answer"); } finally { view.dispose(); } }); }); Deno.test("a subagent report renders its status and answer and links to the child", async () => { await withBerry(async ({ settle }) => { const { transcript } = await import("./transcript.tsx"); const { describe } = await import("./tree.tsx"); const view = transcript(); const tid = "a2b3c4d5e6f7g"; try { const record = { id: 1 as EntryId, conversationId: 1 as ConversationId, kind: "berry.subagent-report", model: [{ role: "user", content: `Background subagent ${tid} finished its task.`, timestamp: 0, }], data: { tid, task: "Investigate tests", status: "completed", answer: "The fix is **ready**.", }, } as unknown as EntryRecord; view.update([record], {}); assertEquals(describe({ kind: "berry.subagent-report", text: "finished" } as never), [ "report", "finished", ]); await settle(); const report = view.element.querySelector(".report")!; assert(report); assert(!report.closest(".message.user")); assertStringIncludes(report.textContent!, "completed"); assertStringIncludes(report.textContent!, "Investigate tests"); assertEquals(report.querySelector("strong")?.textContent, "ready"); assertEquals(report.querySelector("a.tid")?.getAttribute("href"), `#/${tid}`); // the raw kind never shows assert(!report.textContent!.includes("berry.subagent-report")); } finally { view.dispose(); } }); }); Deno.test("a background card keeps its transcript open while the child works and releases it when it idles", async (t) => { await withBerry(async ({ server, settle }) => { const { transcript } = await import("./transcript.tsx"); const { watch } = await import("./client.ts"); for (const mode of ["direct", "codemode"] as const) { await t.step(mode, async () => { const view = transcript(); const tid = "a2b3c4d5e6f7g"; const args = { task: "Investigate tests", model: { provider: "faux", modelId: "worker" }, background: true, }; const name = mode === "direct" ? "subagent" : "codemode"; const entries = [entry( 1, "pi.assistant", fauxAssistantMessage( fauxToolCall(name, mode === "direct" ? args : { code: "await tools.subagent(task);" }, { id: "call", }), { stopReason: "toolUse" }, ), )]; const running = { conversation: tid, status: "running" }; const nested = { name: "subagent", extension: "subagent", args, status: "running", details: { tid, conversationId: 1, status: "running" }, value: running, }; const details = mode === "direct" ? { tid, conversationId: 1, status: "running" } : { calls: [nested] }; try { view.update(entries, { run: { taskId: 2 }, tools: [{ callId: "call", name, status: "running", details }], } as unknown as LiveState); const link = () => view.element.querySelector("a.tid")?.getAttribute("href"); assertEquals(link(), `#/${tid}`); const pane = view.element.querySelector( "[aria-label='Subagent transcript']", )!; assert(pane); const body = pane.parentElement!; const childEntries = [ entry(1, "pi.user", { role: "user", content: args.task, timestamp: 0 }), ]; server.snapshot(`conversation:${tid}`, { entries: childEntries, docs: { "pi.agent": { model: args.model }, "pi.live": { run: { taskId: 3 }, tools: [] }, }, }); await settle(); assert(!pane.hidden); assertStringIncludes(body.textContent!, args.task); // the spawn settles while the child is still working: the card stays open const finishedDetails = mode === "direct" ? { tid, conversationId: 1, status: "running" } : { calls: [{ ...nested, value: running }] }; view.update([ ...entries, entry(2, "pi.tool-result", { role: "toolResult", toolCallId: "call", toolName: name, content: [{ type: "text", text: "spawned" }], details: finishedDetails, isError: false, timestamp: 0, }), ], {}); await settle(); assert(!pane.hidden); assertEquals(link(), `#/${tid}`); assertStringIncludes(body.textContent!, args.task); // once the child idles the card releases its subscription server.snapshot(`conversation:${tid}`, { entries: childEntries, docs: {} }); await settle(); assert(pane.hidden); assertEquals(pane.textContent, ""); assertEquals(link(), `#/${tid}`); let cached = false; const release = watch(`conversation:${tid}`, () => cached = true); release(); assert(!cached); } finally { view.dispose(); } }); } }); });