1import { assert } from "@effect/vitest";
2import type { ProviderReplayTranscript } from "@t3tools/contracts";
3
4import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
5import {
6 assertBaseProjection,
7 assertSemanticProjectionIntegrity,
8 assertUserMessagesInclude,
9 backgroundNotifications,
10 projectionFor,
11} from "../shared.ts";
12import { GROK_BACKGROUND_SUBAGENT_PROMPT } from "./input.ts";
13
14function runAssistantTexts(
15 projection: ReturnType<typeof projectionFor>,
16 runId: string | undefined,
17): ReadonlyArray<string> {
18 return projection.turnItems.flatMap((item) =>
19 item.runId === runId && item.type === "assistant_message" ? [item.text.trim()] : [],
20 );
21}
22
23// A background subagent outlives its root turn. Grok ends it only with the
24// root-session `subagent_finished` notification: the spawn tool completed at
25// launch and nothing names the subagent done in text. Run 1 is held open for
26// the subagent and completes with it; Grok's own reply to the finished
27// subagent (its `subagent-completed-*` wake) is a provider continuation.
28export function assertGrokBackgroundSubagentOutput(
29 result: OrchestratorV2ScenarioResult,
30 transcript: ProviderReplayTranscript,
31) {
32 assertBaseProjection({
33 result,
34 transcript,
35 runCount: 2,
36 runStatuses: ["completed", "completed"],
37 });
38 const projection = projectionFor(result, transcript.scenario);
39 assertSemanticProjectionIntegrity(projection);
40 assertUserMessagesInclude(projection, [GROK_BACKGROUND_SUBAGENT_PROMPT]);
41 const [rootRun, wakeRun] = projection.runs;
42
43 assert.lengthOf(projection.subagents, 1);
44 const subagent = projection.subagents[0];
45 assert.equal(subagent?.status, "completed");
46 assert.equal(subagent?.origin, "provider_native");
47 assert.equal(subagent?.runId, rootRun?.id, "the subagent belongs to the run that spawned it");
48 const subagentItem = projection.turnItems.find((item) => item.type === "subagent");
49 assert.equal(subagentItem?.status, "completed");
50 assert.equal(subagentItem?.runId, rootRun?.id);
51
52 // Run 1 settles only after the subagent's structured end.
53 const subagentCompletedAt = result.domainEvents.findIndex(
54 (event) =>
55 event.type === "turn-item.updated" &&
56 event.payload.type === "subagent" &&
57 event.payload.status === "completed",
58 );
59 const rootCompletedAt = result.domainEvents.findIndex(
60 (event) =>
61 event.type === "run.updated" &&
62 event.payload.id === rootRun?.id &&
63 event.payload.status === "completed",
64 );
65 assert.isAtLeast(subagentCompletedAt, 0);
66 assert.isAbove(rootCompletedAt, subagentCompletedAt, "run 1 completed before the subagent");
67 assert.include(runAssistantTexts(projection, rootRun?.id), "ROOT_DONE");
68
69 // Grok ends ROOT_DONE with its prompt; the subagent running on must not keep
70 // the reply streaming.
71 const rootDoneCompletedAt = result.domainEvents.findIndex(
72 (event) =>
73 event.type === "turn-item.updated" &&
74 event.payload.runId === rootRun?.id &&
75 event.payload.type === "assistant_message" &&
76 event.payload.text.trim() === "ROOT_DONE" &&
77 event.payload.status === "completed",
78 );
79 assert.isAtLeast(rootDoneCompletedAt, 0);
80 assert.isBelow(
81 rootDoneCompletedAt,
82 subagentCompletedAt,
83 "ROOT_DONE streamed until the subagent finished",
84 );
85
86 // The subagent's own reply stays in its child thread.
87 if (subagent?.childThreadId == null) {
88 throw new Error("The background subagent is missing its child thread.");
89 }
90 const child = result.projections.get(subagent.childThreadId);
91 assert.isDefined(child);
92 assert.isTrue(
93 child?.turnItems.some(
94 (item) => item.type === "assistant_message" && item.text.includes("SUBAGENT_DONE"),
95 ),
96 "the subagent's reply must land in its child thread",
97 );
98 assert.notInclude(runAssistantTexts(projection, rootRun?.id), "SUBAGENT_DONE");
99
100 // The timeline says which subagent finished, and opens its thread.
101 assert.deepEqual(backgroundNotifications(projection), [
102 {
103 summary: 'Subagent "Sleep then reply done" finished',
104 outcome: "completed",
105 source: { kind: "subagent", childThreadId: subagent.childThreadId },
106 },
107 ]);
108
109 // Grok's reply to the finished subagent is a provider continuation.
110 const wakeMessage = projection.messages.find((message) => message.id === wakeRun?.userMessageId);
111 assert.equal(`${wakeMessage?.createdBy}:${wakeMessage?.creationSource}`, "agent:provider");
112 assert.isNotEmpty(
113 runAssistantTexts(projection, wakeRun?.id),
114 "Grok's reply to the finished subagent must land in the continuation run",
115 );
116}