authorgravatar for 62337003+Bil0000@users.noreply.github.comBilal Bakr <62337003+Bil0000@users.noreply.github.com> 2026-10-06 21:19:01+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-06 13:19:01-05:00
logeea9f7cfebbdb51d27f07a816119c71a67509997
tree7dcb963ecf83a5d653823c9e0edd5d261b006c33
parent78552a8c6162c9b5248231bc70be8bffdb50cbd3
signature Signed by PGP key B5690EEEBB952194

fix(orchestration-v2): show reported subagent models (#14108)

Co-authored-by: Yash Singh <saiansh2525@gmail.com>

7 files changed, 165 insertions(+), 20 deletions(-)

apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts+25
......@@ -1049,6 +1049,19 @@ describe("AcpAdapterV2", () => {
10491049 // Production thread 54aeb6d7 split after "(command". Metadata shapes
10501050 // below were captured from live Devin sessions showy-mile/fragrant-chamomile.
10511051 const updates = [
1052 {
1053 sessionUpdate: "tool_call_update",
1054 toolCallId: "child-a",
1055 status: "in_progress",
1056 _meta: {
1057 "cognition.ai/subagent_started": {
1058 agentId: "child-a",
1059 title: "Map orchestration",
1060 task: "Run pwd, then reply ONE.",
1061 model: " \t ",
1062 },
1063 },
1064 },
10521065 {
10531066 sessionUpdate: "tool_call_update",
10541067 toolCallId: "child-a",
......@@ -1235,6 +1248,18 @@ describe("AcpAdapterV2", () => {
12351248 event.type === "subagent.updated" ? [event.subagent] : [],
12361249 );
12371250 const task = tasks.at(-1);
1251 assert.isNull(tasks[0]?.model);
1252 const childThread = events.find(
1253 (event) =>
1254 event.type === "app_thread.created" && event.appThread.id === task?.childThreadId,
1255 );
1256 assert.equal(
1257 childThread?.type === "app_thread.created"
1258 ? childThread.appThread.modelSelection.model
1259 : undefined,
1260 modelSelection.model,
1261 );
1262 assert.equal(task?.model, "SWE-1.7 Medium");
12381263 assert.equal(task?.status, "completed");
12391264 assert.equal(task?.result, "Final report: ONE");
12401265 const childMessages = new Map(
apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts+2-2
......@@ -2730,10 +2730,10 @@ export function makeAcpAdapterV2(
27302730 nativeTaskRef: nativeItemRef,
27312731 prompt: update.prompt,
27322732 title: update.title,
2733 model: update.model,
27342733 result: null,
27352734 startedAt: now,
27362735 }),
2736 model: update.model?.trim() || existing?.task.model || null,
27372737 status: taskStatus,
27382738 result: update.result ?? existing?.task.result ?? null,
27392739 completedAt: acpSubagentStatusIsTerminal(taskStatus) ? now : null,
......@@ -2769,7 +2769,7 @@ export function makeAcpAdapterV2(
27692769 providerInstanceId: context.input.modelSelection.instanceId,
27702770 modelSelection: {
27712771 ...context.input.modelSelection,
2772 model: update.model ?? context.input.modelSelection.model,
2772 model: task.model ?? context.input.modelSelection.model,
27732773 },
27742774 title: subagentThreadTitle({
27752775 parentTitle: context.input.appThread.title,
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts+73-3
......@@ -32,6 +32,7 @@ import { assert, describe, it } from "@effect/vitest";
3232import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";
3333import { SpawnExecutableResolution } from "@t3tools/shared/shell";
3434import * as CodexClient from "effect-codex-app-server/client";
35import * as CodexError from "effect-codex-app-server/errors";
3536import * as CodexReplay from "effect-codex-app-server/replay";
3637import * as DateTime from "effect/DateTime";
3738import * as Deferred from "effect/Deferred";
......@@ -1870,7 +1871,7 @@ describe("CodexAdapterV2 post-settle continuation", () => {
18701871 transcript: CodexReplay.CodexAppServerReplayTranscript,
18711872 onEvent: (event: ProviderAdapterV2Event) => Effect.Effect<unknown> = () => Effect.void,
18721873 onRequest: (method: string, params: unknown) => Effect.Effect<void> = () => Effect.void,
1873 readChildMetadata?: (threadId: string) => Effect.Effect<unknown>,
1874 readChildMetadata?: Parameters<typeof withCodexReplayChildMetadata>[2],
18741875 ) =>
18751876 Effect.gen(function* () {
18761877 const fileSystem = yield* FileSystem.FileSystem;
......@@ -6708,6 +6709,8 @@ describe("CodexAdapterV2 post-settle continuation", () => {
67086709 });
67096710
67106711 it.effect.each([
6712 { name: "current Codex Sol", model: "gpt-6-sol" },
6713 { name: "current Codex wrong child", model: null },
67116714 { name: "Sol", model: "gpt-5.6-sol" },
67126715 { name: "Fable", model: "gpt-5.6-fable" },
67136716 { name: "Astra", model: "gpt-6-astra" },
......@@ -6719,6 +6722,7 @@ describe("CodexAdapterV2 post-settle continuation", () => {
67196722 Effect.gen(function* () {
67206723 const metadataRead = yield* Deferred.make<void>();
67216724 const modelReported = yield* Deferred.make<void>();
6725 let metadataRequests = 0;
67226726 const harness = yield* makeCodexReplayHarness(
67236727 resumeSubagentTranscript,
67246728 (event) =>
......@@ -6727,14 +6731,22 @@ describe("CodexAdapterV2 post-settle continuation", () => {
67276731 : Effect.void,
67286732 undefined,
67296733 (threadId) => {
6734 metadataRequests++;
67306735 assert.equal(threadId, RESUME_CHILD_THREAD);
67316736 return Deferred.succeed(metadataRead, undefined).pipe(
67326737 Effect.as(
67336738 name === "invalid"
67346739 ? {}
67356740 : {
6736 thread: { id: name === "wrong child" ? "other-child" : threadId },
6737 model: name === "wrong child" ? "gpt-5.6-sol" : model,
6741 thread: {
6742 id: name.includes("wrong child") ? "other-child" : threadId,
6743 ...(name.startsWith("current Codex") ? { model: "gpt-6-sol" } : {}),
6744 },
6745 model: name.startsWith("current Codex")
6746 ? null
6747 : name === "wrong child"
6748 ? "gpt-5.6-sol"
6749 : model,
67386750 },
67396751 ),
67406752 );
......@@ -6754,10 +6766,68 @@ describe("CodexAdapterV2 post-settle continuation", () => {
67546766 yield* TestClock.adjust("100 millis");
67556767 yield* harness.firstTerminal;
67566768 assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model);
6769 assert.equal(metadataRequests, name === "current Codex Sol" ? 1 : 2);
67576770 }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),
67586771 ),
67596772 );
67606773
6774 it.effect.each(["failed", "malformed", "wrong child", "blank model"] as const)(
6775 "resumes child metadata after a %s read",
6776 (readResult) =>
6777 Effect.scoped(
6778 Effect.gen(function* () {
6779 const modelReported = yield* Deferred.make<void>();
6780 const model = "gpt-6-sol";
6781 const metadataRequests: Array<string> = [];
6782 const harness = yield* makeCodexReplayHarness(
6783 resumeSubagentTranscript,
6784 (event) =>
6785 event.type === "subagent.updated" && event.subagent.model === model
6786 ? Deferred.succeed(modelReported, undefined)
6787 : Effect.void,
6788 undefined,
6789 (threadId, method) => {
6790 assert.equal(threadId, RESUME_CHILD_THREAD);
6791 metadataRequests.push(method);
6792 if (method === "thread/resume") {
6793 return Effect.succeed({ thread: { id: threadId }, model });
6794 }
6795 switch (readResult) {
6796 case "failed":
6797 return Effect.fail(
6798 new CodexError.CodexAppServerRequestError({
6799 code: -32000,
6800 errorMessage: "Child metadata unavailable",
6801 method,
6802 }),
6803 );
6804 case "malformed":
6805 return Effect.succeed({});
6806 case "wrong child":
6807 return Effect.succeed({ thread: { id: "other-child", model: "gpt-6-astra" } });
6808 case "blank model":
6809 return Effect.succeed({ thread: { id: threadId, model: " \t " } });
6810 }
6811 },
6812 );
6813 yield* harness.runtime.startTurn(
6814 makeCodexTestTurnInput({
6815 threadId: harness.threadId,
6816 providerThread: harness.providerThread,
6817 now: yield* DateTime.now,
6818 attemptId: RunAttemptId.make("attempt-child-model-fallback"),
6819 text: RESUME_PROMPT,
6820 }),
6821 );
6822 yield* Deferred.await(modelReported);
6823 yield* TestClock.adjust("100 millis");
6824 yield* harness.firstTerminal;
6825 assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model);
6826 assert.deepEqual(metadataRequests, ["thread/read", "thread/resume"]);
6827 }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),
6828 ),
6829 );
6830
67616831 it.effect.each(["thread/settings/updated", "model/rerouted"] as const)(
67626832 "keeps %s child metadata when an older lookup finishes later",
67636833 (method) =>
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts+8-4
......@@ -3,6 +3,7 @@ import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability";
33import * as NodeServices from "@effect/platform-node/NodeServices";
44import { type ProviderReplayTranscript } from "@t3tools/contracts";
55import * as CodexClient from "effect-codex-app-server/client";
6import type * as CodexError from "effect-codex-app-server/errors";
67import * as CodexReplay from "effect-codex-app-server/replay";
78import * as Effect from "effect/Effect";
89import * as FileSystem from "effect/FileSystem";
......@@ -45,7 +46,10 @@ export type CodexOrchestratorReplayHarnessError = typeof CodexOrchestratorReplay
4546export function withCodexReplayChildMetadata(
4647 client: CodexClient.CodexAppServerClient["Service"],
4748 transcript: CodexReplay.CodexAppServerReplayTranscript,
48 readMetadata: (threadId: string) => Effect.Effect<unknown> = (threadId) =>
49 readMetadata: (
50 threadId: string,
51 method: "thread/read" | "thread/resume",
52 ) => Effect.Effect<unknown, CodexError.CodexAppServerError> = (threadId) =>
4953 Effect.succeed({ thread: { id: threadId }, model: null }),
5054): CodexClient.CodexAppServerClient["Service"] {
5155 const childThreadIds = new Set(
......@@ -67,12 +71,12 @@ export function withCodexReplayChildMetadata(
6771 raw: {
6872 ...client.raw,
6973 request: (method, params) =>
70 method === "thread/resume" &&
74 (method === "thread/read" || method === "thread/resume") &&
7175 Predicate.isObject(params) &&
72 params.excludeTurns === true &&
76 (method === "thread/read" ? params.includeTurns === false : params.excludeTurns === true) &&
7377 typeof params.threadId === "string" &&
7478 childThreadIds.has(params.threadId)
75 ? readMetadata(params.threadId)
79 ? readMetadata(params.threadId, method)
7680 : client.raw.request(method, params),
7781 },
7882 };
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts+27-2
......@@ -1320,6 +1320,15 @@ const decodeCodexChildModel = Schema.decodeUnknownEffect(
13201320 }),
13211321);
13221322
1323const decodeCodexChildThread = Schema.decodeUnknownEffect(
1324 Schema.Struct({
1325 thread: Schema.Struct({
1326 id: Schema.String,
1327 model: Schema.optional(Schema.NullOr(Schema.String)),
1328 }),
1329 }),
1330);
1331
13231332export const makeCodexAppServerSpawnCommand = Effect.fn(
13241333 "CodexAdapterV2.makeCodexAppServerSpawnCommand",
13251334)(function* (input: {
......@@ -2817,9 +2826,25 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
28172826 }
28182827 if (task.model === null) {
28192828 yield* client.raw
2820 .request("thread/resume", { threadId: input.nativeThreadId, excludeTurns: true })
2829 .request("thread/read", { threadId: input.nativeThreadId, includeTurns: false })
28212830 .pipe(
2822 Effect.flatMap(decodeCodexChildModel),
2831 Effect.flatMap(decodeCodexChildThread),
2832 Effect.map((response) =>
2833 response.thread.id === input.nativeThreadId && response.thread.model?.trim()
2834 ? { thread: response.thread, model: response.thread.model }
2835 : null,
2836 ),
2837 Effect.catch(() => Effect.succeed(null)),
2838 Effect.flatMap((response) =>
2839 response === null
2840 ? client.raw
2841 .request("thread/resume", {
2842 threadId: input.nativeThreadId,
2843 excludeTurns: true,
2844 })
2845 .pipe(Effect.flatMap(decodeCodexChildModel))
2846 : Effect.succeed(response),
2847 ),
28232848 Effect.timeout("5 seconds"),
28242849 Effect.flatMap((response) =>
28252850 response.thread.id === input.nativeThreadId &&
apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts+29-8
......@@ -40,12 +40,14 @@ const decodeCursorSettings = Schema.decodeEffect(CursorSettings);
4040
4141describe("CursorAdapterV2", () => {
4242 it.effect.each([
43 { status: "finished", model: undefined },
44 { status: "cancelled", model: "claude-opus-4-6" },
45 { status: "error", model: "custom-fable" },
43 { status: "finished", model: undefined, lateModel: undefined },
44 { status: "cancelled", model: "claude-opus-4-6", lateModel: undefined },
45 { status: "error", model: "custom-fable", lateModel: undefined },
46 { status: "finished", model: undefined, lateModel: "gpt-6-sol" },
47 { status: "finished", model: "gpt-6-sol", lateModel: null },
4648 ] as const)(
47 "settles missing task completions when the Cursor run is $status",
48 ({ status, model }) =>
49 "projects Cursor tasks: $status, late model $lateModel",
50 ({ status, model, lateModel }) =>
4951 Effect.gen(function* () {
5052 const fileSystem = yield* FileSystem.FileSystem;
5153 const path = yield* Path.Path;
......@@ -96,12 +98,24 @@ describe("CursorAdapterV2", () => {
9698 mode: "unspecified" as const,
9799 },
98100 };
99 for (const type of ["partial-tool-call", "tool-call-started"] as const) {
101 const updates =
102 lateModel !== undefined
103 ? (["tool-call-started", "tool-call-completed"] as const)
104 : (["partial-tool-call", "tool-call-started"] as const);
105 for (const type of updates) {
100106 yield* input.onDelta!({
101107 type,
102108 modelCallId: "model-call",
103109 callId: "task-call",
104 toolCall: taskToolCall,
110 toolCall: {
111 ...taskToolCall,
112 args: {
113 ...taskToolCall.args,
114 ...(type === "tool-call-completed"
115 ? { model: lateModel ?? undefined }
116 : {}),
117 },
118 },
105119 }).pipe(Effect.orDie);
106120 }
107121 return {
......@@ -180,9 +194,16 @@ describe("CursorAdapterV2", () => {
180194 const rows = events.filter((event) => event.type === "subagent.updated");
181195 assert.equal(rows[0]?.subagent.status, "running");
182196 assert.equal(rows[0]?.subagent.model, model ?? null);
197 assert.equal(rows.at(-1)?.subagent.model, lateModel ?? model ?? null);
183198 assert.equal(
184199 rows.at(-1)?.subagent.status,
185 status === "finished" ? "idle" : status === "cancelled" ? "cancelled" : "failed",
200 lateModel !== undefined
201 ? "completed"
202 : status === "finished"
203 ? "idle"
204 : status === "cancelled"
205 ? "cancelled"
206 : "failed",
186207 );
187208 assert.isNotNull(rows.at(-1)?.subagent.completedAt);
188209 }).pipe(Effect.scoped, Effect.provide(Layer.merge(NodeServices.layer, IdAllocator.layer))),
apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts+1-1
......@@ -1572,10 +1572,10 @@ export function makeCursorAdapterV2(
15721572 },
15731573 prompt: args.prompt,
15741574 title: args.description,
1575 model: args.model?.trim() || null,
15761575 result: null,
15771576 startedAt: now,
15781577 }),
1578 model: args.model?.trim() || existing?.task.model || null,
15791579 nativeTaskRef: {
15801580 driver: CursorAgentSdk.CURSOR_PROVIDER,
15811581 nativeId: input.callId,