1import { assert } from "@effect/vitest";
2import type { ProviderReplayTranscript } from "@t3tools/contracts";
3
4import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
5import {
6 assertAssistantTextIncludes,
7 assertBaseProjection,
8 assertExecutionNodeKinds,
9 assertNoExtraAppRunsForProviderChildren,
10 assertRunProviderTurnCardinality,
11 assertSemanticProjectionIntegrity,
12 assertTurnItemTypes,
13 assertUserMessagesInclude,
14 projectionFor,
15 SUBAGENT_PROMPT,
16} from "../shared.ts";
17
18export function assertClaudeSubagentOutput(
19 result: OrchestratorV2ScenarioResult,
20 transcript: ProviderReplayTranscript,
21) {
22 assertBaseProjection({
23 result,
24 transcript,
25 runCount: 1,
26 runStatuses: ["completed"],
27 });
28
29 const projection = projectionFor(result, transcript.scenario);
30 assertSemanticProjectionIntegrity(projection);
31 assertExecutionNodeKinds(projection, ["root_turn", "subagent"]);
32 assertTurnItemTypes(projection, ["user_message", "subagent", "assistant_message"]);
33 assertRunProviderTurnCardinality({ projection, rootRunCount: 1 });
34 assertNoExtraAppRunsForProviderChildren({ projection, expectedAppRuns: 1 });
35 assertUserMessagesInclude(projection, [SUBAGENT_PROMPT]);
36 assertAssistantTextIncludes(projection, "claude-read-only-fixture");
37 assertAssistantTextIncludes(projection, "ES2022");
38 assert.lengthOf(
39 projection.turnItems.filter((item) => item.type === "dynamic_tool"),
40 0,
41 "subagent tools must not be projected into the parent thread",
42 );
43
44 const subagentNodes = projection.nodes.filter((node) => node.kind === "subagent");
45 assert.lengthOf(subagentNodes, 2);
46 assert.deepEqual(
47 subagentNodes.map((node) => node.status),
48 ["completed", "completed"],
49 );
50
51 assert.lengthOf(projection.subagents, 2);
52 assert.lengthOf(result.shellSnapshot.threads, 3);
53 assert.isTrue(
54 projection.subagents.some(
55 (subagent) =>
56 subagent.prompt ===
57 "Read the file `package.json` in the current working directory and return its full contents." &&
58 subagent.result?.includes("claude-read-only-fixture"),
59 ),
60 );
61 assert.isTrue(
62 projection.subagents.some(
63 (subagent) =>
64 subagent.prompt ===
65 "Read the file `tsconfig.json` in the current working directory and return its full contents." &&
66 subagent.result?.includes("ES2022"),
67 ),
68 );
69 for (const subagent of projection.subagents) {
70 const expectedProgress = subagent.prompt.includes("package.json")
71 ? "Summarizing package.json"
72 : "Reading tsconfig.json";
73 assert.equal(subagent.origin, "provider_native");
74 assert.equal(subagent.createdBy, "agent");
75 assert.equal(subagent.driver, "claudeAgent");
76 assert.equal(subagent.status, "completed");
77 assert.equal(subagent.progress, expectedProgress);
78 assert.isNull(subagent.providerThreadId);
79 assert.isNotNull(subagent.childThreadId);
80 assert.isNotNull(subagent.nativeTaskRef);
81 assert.isNotNull(subagent.completedAt);
82 if (subagent.childThreadId === null) {
83 throw new Error(`Subagent ${subagent.id} is missing its child thread`);
84 }
85
86 const childProjection = result.projections.get(subagent.childThreadId);
87 assert.isDefined(childProjection);
88 assert.equal(childProjection.thread.lineage.parentThreadId, projection.thread.id);
89 assert.equal(childProjection.thread.lineage.relationshipToParent, "subagent");
90 assert.isNull(childProjection.thread.activeProviderThreadId);
91 assert.lengthOf(childProjection.runs, 0);
92 assert.lengthOf(childProjection.providerThreads, 0);
93 assert.lengthOf(childProjection.providerTurns, 0);
94 assertExecutionNodeKinds(childProjection, ["root_turn", "tool_call"]);
95 assertTurnItemTypes(childProjection, ["user_message", "dynamic_tool", "assistant_message"]);
96 assertUserMessagesInclude(childProjection, [subagent.prompt]);
97 assert.isTrue(
98 childProjection.turnItems.some(
99 (item) =>
100 item.type === "assistant_message" &&
101 subagent.result !== null &&
102 item.text.includes(subagent.result.slice(0, 40)),
103 ),
104 `child thread ${subagent.childThreadId} must contain the subagent response`,
105 );
106 // Progress stays on the subagent card; the child shows the work itself.
107 assert.isFalse(
108 childProjection.turnItems.some(
109 (item) => item.type === "reasoning" && item.text === expectedProgress,
110 ),
111 `child thread ${subagent.childThreadId} must not repeat the subagent's progress`,
112 );
113
114 const parentItem = projection.turnItems.find(
115 (item) => item.type === "subagent" && item.subagentId === subagent.id,
116 );
117 assert.isDefined(parentItem);
118 if (parentItem?.type !== "subagent") {
119 throw new Error(`Missing parent lifecycle item for subagent ${subagent.id}`);
120 }
121 assert.equal(parentItem.progress, expectedProgress);
122 }
123}