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