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_V2_PROMPT,
15} from "../shared.ts";
16
17export function assertSubagentV2Output(
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_V2_PROMPT]);
35 assert.lengthOf(projection.subagents, 1);
36 assert.lengthOf(result.shellSnapshot.threads, 2);
37
38 const subagent = projection.subagents[0]!;
39 assert.equal(subagent.origin, "provider_native");
40 assert.equal(subagent.createdBy, "agent");
41 assert.equal(subagent.driver, "codex");
42 // Codex names v2 agents by path from the root; the model picks the leaf name.
43 assert.match(subagent.title ?? "", /^\/root\/[^/]+$/u);
44 assert.equal(subagent.prompt, "");
45 assert.equal(subagent.status, "completed");
46 assert.equal(subagent.result, "Hello.");
47 assert.isNotNull(subagent.childThreadId);
48 assert.isNotNull(subagent.providerThreadId);
49 assert.isNotNull(subagent.nativeTaskRef);
50 assert.isNotNull(subagent.completedAt);
51 const parentItem = projection.turnItems.find(
52 (item) => item.type === "subagent" && item.subagentId === subagent.id,
53 );
54 assert.isDefined(parentItem);
55 if (parentItem?.type !== "subagent") {
56 throw new Error(`Missing parent lifecycle item for subagent ${subagent.id}`);
57 }
58 assert.equal(parentItem.result, subagent.result);
59 if (subagent.childThreadId === null) {
60 throw new Error(`Subagent ${subagent.id} is missing its child thread`);
61 }
62
63 const providerThread = projection.providerThreads.find(
64 (thread) => thread.id === subagent.providerThreadId,
65 );
66 assert.isDefined(providerThread);
67 assert.equal(providerThread.appThreadId, subagent.childThreadId);
68 assert.isNull(providerThread.ownerNodeId);
69
70 const childProjection = result.projections.get(subagent.childThreadId);
71 assert.isDefined(childProjection);
72 assert.equal(childProjection.thread.lineage.parentThreadId, projection.thread.id);
73 assert.equal(childProjection.thread.lineage.relationshipToParent, "subagent");
74 assert.equal(childProjection.thread.activeProviderThreadId, providerThread.id);
75 assert.lengthOf(childProjection.runs, 0);
76 assert.lengthOf(childProjection.providerThreads, 1);
77 assert.lengthOf(childProjection.providerTurns, 1);
78 assertTurnItemTypes(childProjection, ["assistant_message"]);
79 assert.isTrue(
80 childProjection.turnItems.some(
81 (item) => item.type === "assistant_message" && item.text.includes("Hello."),
82 ),
83 "child thread must contain the post-root subagent response",
84 );
85 assert.equal(childProjection.providerTurns[0]?.status, "completed");
86}