| 1 | import assert from "node:assert/strict"; |
| 2 | import test from "node:test"; |
| 3 | import { ChatSource } from "../lib/chatViewSource"; |
| 4 | import { TranscriptStore } from "../lib/transcriptStore"; |
| 5 | import type { HistoryEntry } from "../lib/types"; |
| 6 | import { FakeBackend } from "./helpers/transcriptFakeBackend"; |
| 7 | |
| 8 | const call = (id: string, name: string, parentId?: string) => ({ id, name, arguments: "{}", parentId }); |
| 9 | const assistant = (n: number, messageId: string, toolCalls: ReturnType<typeof call>[]): HistoryEntry => ({ |
| 10 | entryId: `m:${messageId}`, turn: 1, order: n, refs: [], message: { role: "assistant", messageId, content: "", toolCalls }, |
| 11 | }); |
| 12 | const result = (n: number, toolCallId: string, toolName: string): HistoryEntry => ({ |
| 13 | entryId: `tool:${toolCallId}`, turn: 1, order: n, refs: [], message: { role: "tool", toolCallId, toolName, content: "ok" }, |
| 14 | }); |
| 15 | |
| 16 | function project(entries: HistoryEntry[]) { |
| 17 | const user: HistoryEntry = { entryId: "m:u", turn: 1, order: 0, refs: [], message: { role: "user", messageId: "u", content: "go" } }; |
| 18 | const store = new TranscriptStore(new FakeBackend([])); |
| 19 | return store.installSlice("subagent-parent", "/subagent-parent", { |
| 20 | entries: [user, ...entries.map((entry, index) => ({ ...entry, order: index + 1 }))], |
| 21 | nextCursor: "", newerCursor: "", hasOlder: false, hasNewer: false, totalTurns: 1, startTurn: 1, endTurn: 1, |
| 22 | revision: 1, revisionKnown: true, digest: "subagent-parent", stale: false, |
| 23 | }); |
| 24 | } |
| 25 | |
| 26 | function topLevelTools(items: ReturnType<typeof project>["items"]): string[] { |
| 27 | const source = new ChatSource("subagent-parent-view"); |
| 28 | source.update({ items, localSubmissions: [], running: false, hydrating: false, hasOlder: false, loadingOlder: false }); |
| 29 | const names = source.getOrderSnapshot().flatMap(key => { |
| 30 | const node = source.getNodeSnapshot(key); |
| 31 | return node?.kind === "tool" ? [node.item.id] : []; |
| 32 | }); |
| 33 | source.dispose(); |
| 34 | return names; |
| 35 | } |
| 36 | |
| 37 | test("a task sub-agent's own calls keep their parent through the record projection", () => { |
| 38 | const projection = project([ |
| 39 | assistant(1, "parent", [call("task-1", "use_capability")]), |
| 40 | assistant(2, "child", [call("task-1/ls1", "ls", "task-1")]), |
| 41 | result(3, "task-1/ls1", "ls"), |
| 42 | result(4, "task-1", "use_capability"), |
| 43 | ]); |
| 44 | const child = projection.items.find(item => item.kind === "tool" && item.id === "task-1/ls1"); |
| 45 | assert.equal(child?.kind === "tool" && child.parentId, "task-1"); |
| 46 | assert.deepEqual(topLevelTools(projection.items), ["task-1"]); |
| 47 | }); |
| 48 | |
| 49 | test("a fleet worker's calls nest two levels under the group", () => { |
| 50 | const projection = project([ |
| 51 | assistant(1, "parent", [call("fl", "use_capability")]), |
| 52 | assistant(2, "group", [call("fl/fleet-1", "task", "fl")]), |
| 53 | assistant(3, "worker", [call("fl/fleet-1/read1", "read_file", "fl/fleet-1")]), |
| 54 | result(4, "fl/fleet-1/read1", "read_file"), |
| 55 | result(5, "fl/fleet-1", "task"), |
| 56 | result(6, "fl", "use_capability"), |
| 57 | ]); |
| 58 | const parents = new Map(projection.items.flatMap(item => item.kind === "tool" ? [[item.id, item.parentId]] : [])); |
| 59 | assert.equal(parents.get("fl/fleet-1"), "fl"); |
| 60 | assert.equal(parents.get("fl/fleet-1/read1"), "fl/fleet-1"); |
| 61 | assert.deepEqual(topLevelTools(projection.items), ["fl"]); |
| 62 | }); |
| 63 |