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", () => {...@@ -1049,6 +1049,19 @@ describe("AcpAdapterV2", () => {
1049 // Production thread 54aeb6d7 split after "(command". Metadata shapes1049 // Production thread 54aeb6d7 split after "(command". Metadata shapes
1050 // below were captured from live Devin sessions showy-mile/fragrant-chamomile.1050 // below were captured from live Devin sessions showy-mile/fragrant-chamomile.
1051 const updates = [1051 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 },
1052 {1065 {
1053 sessionUpdate: "tool_call_update",1066 sessionUpdate: "tool_call_update",
1054 toolCallId: "child-a",1067 toolCallId: "child-a",
...@@ -1235,6 +1248,18 @@ describe("AcpAdapterV2", () => {...@@ -1235,6 +1248,18 @@ describe("AcpAdapterV2", () => {
1235 event.type === "subagent.updated" ? [event.subagent] : [],1248 event.type === "subagent.updated" ? [event.subagent] : [],
1236 );1249 );
1237 const task = tasks.at(-1);1250 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");
1238 assert.equal(task?.status, "completed");1263 assert.equal(task?.status, "completed");
1239 assert.equal(task?.result, "Final report: ONE");1264 assert.equal(task?.result, "Final report: ONE");
1240 const childMessages = new Map(1265 const childMessages = new Map(
apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts+2-2
...@@ -2730,10 +2730,10 @@ export function makeAcpAdapterV2(...@@ -2730,10 +2730,10 @@ export function makeAcpAdapterV2(
2730 nativeTaskRef: nativeItemRef,2730 nativeTaskRef: nativeItemRef,
2731 prompt: update.prompt,2731 prompt: update.prompt,
2732 title: update.title,2732 title: update.title,
2733 model: update.model,
2734 result: null,2733 result: null,
2735 startedAt: now,2734 startedAt: now,
2736 }),2735 }),
2736 model: update.model?.trim() || existing?.task.model || null,
2737 status: taskStatus,2737 status: taskStatus,
2738 result: update.result ?? existing?.task.result ?? null,2738 result: update.result ?? existing?.task.result ?? null,
2739 completedAt: acpSubagentStatusIsTerminal(taskStatus) ? now : null,2739 completedAt: acpSubagentStatusIsTerminal(taskStatus) ? now : null,
...@@ -2769,7 +2769,7 @@ export function makeAcpAdapterV2(...@@ -2769,7 +2769,7 @@ export function makeAcpAdapterV2(
2769 providerInstanceId: context.input.modelSelection.instanceId,2769 providerInstanceId: context.input.modelSelection.instanceId,
2770 modelSelection: {2770 modelSelection: {
2771 ...context.input.modelSelection,2771 ...context.input.modelSelection,
2772 model: update.model ?? context.input.modelSelection.model,2772 model: task.model ?? context.input.modelSelection.model,
2773 },2773 },
2774 title: subagentThreadTitle({2774 title: subagentThreadTitle({
2775 parentTitle: context.input.appThread.title,2775 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";...@@ -32,6 +32,7 @@ import { assert, describe, it } from "@effect/vitest";
32import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";32import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";
33import { SpawnExecutableResolution } from "@t3tools/shared/shell";33import { SpawnExecutableResolution } from "@t3tools/shared/shell";
34import * as CodexClient from "effect-codex-app-server/client";34import * as CodexClient from "effect-codex-app-server/client";
35import * as CodexError from "effect-codex-app-server/errors";
35import * as CodexReplay from "effect-codex-app-server/replay";36import * as CodexReplay from "effect-codex-app-server/replay";
36import * as DateTime from "effect/DateTime";37import * as DateTime from "effect/DateTime";
37import * as Deferred from "effect/Deferred";38import * as Deferred from "effect/Deferred";
...@@ -1870,7 +1871,7 @@ describe("CodexAdapterV2 post-settle continuation", () => {...@@ -1870,7 +1871,7 @@ describe("CodexAdapterV2 post-settle continuation", () => {
1870 transcript: CodexReplay.CodexAppServerReplayTranscript,1871 transcript: CodexReplay.CodexAppServerReplayTranscript,
1871 onEvent: (event: ProviderAdapterV2Event) => Effect.Effect<unknown> = () => Effect.void,1872 onEvent: (event: ProviderAdapterV2Event) => Effect.Effect<unknown> = () => Effect.void,
1872 onRequest: (method: string, params: unknown) => Effect.Effect<void> = () => Effect.void,1873 onRequest: (method: string, params: unknown) => Effect.Effect<void> = () => Effect.void,
1873 readChildMetadata?: (threadId: string) => Effect.Effect<unknown>,1874 readChildMetadata?: Parameters<typeof withCodexReplayChildMetadata>[2],
1874 ) =>1875 ) =>
1875 Effect.gen(function* () {1876 Effect.gen(function* () {
1876 const fileSystem = yield* FileSystem.FileSystem;1877 const fileSystem = yield* FileSystem.FileSystem;
...@@ -6708,6 +6709,8 @@ describe("CodexAdapterV2 post-settle continuation", () => {...@@ -6708,6 +6709,8 @@ describe("CodexAdapterV2 post-settle continuation", () => {
6708 });6709 });
67096710
6710 it.effect.each([6711 it.effect.each([
6712 { name: "current Codex Sol", model: "gpt-6-sol" },
6713 { name: "current Codex wrong child", model: null },
6711 { name: "Sol", model: "gpt-5.6-sol" },6714 { name: "Sol", model: "gpt-5.6-sol" },
6712 { name: "Fable", model: "gpt-5.6-fable" },6715 { name: "Fable", model: "gpt-5.6-fable" },
6713 { name: "Astra", model: "gpt-6-astra" },6716 { name: "Astra", model: "gpt-6-astra" },
...@@ -6719,6 +6722,7 @@ describe("CodexAdapterV2 post-settle continuation", () => {...@@ -6719,6 +6722,7 @@ describe("CodexAdapterV2 post-settle continuation", () => {
6719 Effect.gen(function* () {6722 Effect.gen(function* () {
6720 const metadataRead = yield* Deferred.make<void>();6723 const metadataRead = yield* Deferred.make<void>();
6721 const modelReported = yield* Deferred.make<void>();6724 const modelReported = yield* Deferred.make<void>();
6725 let metadataRequests = 0;
6722 const harness = yield* makeCodexReplayHarness(6726 const harness = yield* makeCodexReplayHarness(
6723 resumeSubagentTranscript,6727 resumeSubagentTranscript,
6724 (event) =>6728 (event) =>
...@@ -6727,14 +6731,22 @@ describe("CodexAdapterV2 post-settle continuation", () => {...@@ -6727,14 +6731,22 @@ describe("CodexAdapterV2 post-settle continuation", () => {
6727 : Effect.void,6731 : Effect.void,
6728 undefined,6732 undefined,
6729 (threadId) => {6733 (threadId) => {
6734 metadataRequests++;
6730 assert.equal(threadId, RESUME_CHILD_THREAD);6735 assert.equal(threadId, RESUME_CHILD_THREAD);
6731 return Deferred.succeed(metadataRead, undefined).pipe(6736 return Deferred.succeed(metadataRead, undefined).pipe(
6732 Effect.as(6737 Effect.as(
6733 name === "invalid"6738 name === "invalid"
6734 ? {}6739 ? {}
6735 : {6740 : {
6736 thread: { id: name === "wrong child" ? "other-child" : threadId },6741 thread: {
6737 model: name === "wrong child" ? "gpt-5.6-sol" : model,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,
6738 },6750 },
6739 ),6751 ),
6740 );6752 );
...@@ -6754,10 +6766,68 @@ describe("CodexAdapterV2 post-settle continuation", () => {...@@ -6754,10 +6766,68 @@ describe("CodexAdapterV2 post-settle continuation", () => {
6754 yield* TestClock.adjust("100 millis");6766 yield* TestClock.adjust("100 millis");
6755 yield* harness.firstTerminal;6767 yield* harness.firstTerminal;
6756 assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model);6768 assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model);
6769 assert.equal(metadataRequests, name === "current Codex Sol" ? 1 : 2);
6757 }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),6770 }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),
6758 ),6771 ),
6759 );6772 );
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
6761 it.effect.each(["thread/settings/updated", "model/rerouted"] as const)(6831 it.effect.each(["thread/settings/updated", "model/rerouted"] as const)(
6762 "keeps %s child metadata when an older lookup finishes later",6832 "keeps %s child metadata when an older lookup finishes later",
6763 (method) =>6833 (method) =>
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts+8-4
...@@ -3,6 +3,7 @@ import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability";...@@ -3,6 +3,7 @@ import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability";
3import * as NodeServices from "@effect/platform-node/NodeServices";3import * as NodeServices from "@effect/platform-node/NodeServices";
4import { type ProviderReplayTranscript } from "@t3tools/contracts";4import { type ProviderReplayTranscript } from "@t3tools/contracts";
5import * as CodexClient from "effect-codex-app-server/client";5import * as CodexClient from "effect-codex-app-server/client";
6import type * as CodexError from "effect-codex-app-server/errors";
6import * as CodexReplay from "effect-codex-app-server/replay";7import * as CodexReplay from "effect-codex-app-server/replay";
7import * as Effect from "effect/Effect";8import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";9import * as FileSystem from "effect/FileSystem";
...@@ -45,7 +46,10 @@ export type CodexOrchestratorReplayHarnessError = typeof CodexOrchestratorReplay...@@ -45,7 +46,10 @@ export type CodexOrchestratorReplayHarnessError = typeof CodexOrchestratorReplay
45export function withCodexReplayChildMetadata(46export function withCodexReplayChildMetadata(
46 client: CodexClient.CodexAppServerClient["Service"],47 client: CodexClient.CodexAppServerClient["Service"],
47 transcript: CodexReplay.CodexAppServerReplayTranscript,48 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) =>
49 Effect.succeed({ thread: { id: threadId }, model: null }),53 Effect.succeed({ thread: { id: threadId }, model: null }),
50): CodexClient.CodexAppServerClient["Service"] {54): CodexClient.CodexAppServerClient["Service"] {
51 const childThreadIds = new Set(55 const childThreadIds = new Set(
...@@ -67,12 +71,12 @@ export function withCodexReplayChildMetadata(...@@ -67,12 +71,12 @@ export function withCodexReplayChildMetadata(
67 raw: {71 raw: {
68 ...client.raw,72 ...client.raw,
69 request: (method, params) =>73 request: (method, params) =>
70 method === "thread/resume" &&74 (method === "thread/read" || method === "thread/resume") &&
71 Predicate.isObject(params) &&75 Predicate.isObject(params) &&
72 params.excludeTurns === true &&76 (method === "thread/read" ? params.includeTurns === false : params.excludeTurns === true) &&
73 typeof params.threadId === "string" &&77 typeof params.threadId === "string" &&
74 childThreadIds.has(params.threadId)78 childThreadIds.has(params.threadId)
75 ? readMetadata(params.threadId)79 ? readMetadata(params.threadId, method)
76 : client.raw.request(method, params),80 : client.raw.request(method, params),
77 },81 },
78 };82 };
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts+27-2
...@@ -1320,6 +1320,15 @@ const decodeCodexChildModel = Schema.decodeUnknownEffect(...@@ -1320,6 +1320,15 @@ const decodeCodexChildModel = Schema.decodeUnknownEffect(
1320 }),1320 }),
1321);1321);
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
1323export const makeCodexAppServerSpawnCommand = Effect.fn(1332export const makeCodexAppServerSpawnCommand = Effect.fn(
1324 "CodexAdapterV2.makeCodexAppServerSpawnCommand",1333 "CodexAdapterV2.makeCodexAppServerSpawnCommand",
1325)(function* (input: {1334)(function* (input: {
...@@ -2817,9 +2826,25 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi...@@ -2817,9 +2826,25 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
2817 }2826 }
2818 if (task.model === null) {2827 if (task.model === null) {
2819 yield* client.raw2828 yield* client.raw
2820 .request("thread/resume", { threadId: input.nativeThreadId, excludeTurns: true })2829 .request("thread/read", { threadId: input.nativeThreadId, includeTurns: false })
2821 .pipe(2830 .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 ),
2823 Effect.timeout("5 seconds"),2848 Effect.timeout("5 seconds"),
2824 Effect.flatMap((response) =>2849 Effect.flatMap((response) =>
2825 response.thread.id === input.nativeThreadId &&2850 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);...@@ -40,12 +40,14 @@ const decodeCursorSettings = Schema.decodeEffect(CursorSettings);
4040
41describe("CursorAdapterV2", () => {41describe("CursorAdapterV2", () => {
42 it.effect.each([42 it.effect.each([
43 { status: "finished", model: undefined },43 { status: "finished", model: undefined, lateModel: undefined },
44 { status: "cancelled", model: "claude-opus-4-6" },44 { status: "cancelled", model: "claude-opus-4-6", lateModel: undefined },
45 { status: "error", model: "custom-fable" },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 },
46 ] as const)(48 ] as const)(
47 "settles missing task completions when the Cursor run is $status",49 "projects Cursor tasks: $status, late model $lateModel",
48 ({ status, model }) =>50 ({ status, model, lateModel }) =>
49 Effect.gen(function* () {51 Effect.gen(function* () {
50 const fileSystem = yield* FileSystem.FileSystem;52 const fileSystem = yield* FileSystem.FileSystem;
51 const path = yield* Path.Path;53 const path = yield* Path.Path;
...@@ -96,12 +98,24 @@ describe("CursorAdapterV2", () => {...@@ -96,12 +98,24 @@ describe("CursorAdapterV2", () => {
96 mode: "unspecified" as const,98 mode: "unspecified" as const,
97 },99 },
98 };100 };
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) {
100 yield* input.onDelta!({106 yield* input.onDelta!({
101 type,107 type,
102 modelCallId: "model-call",108 modelCallId: "model-call",
103 callId: "task-call",109 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 },
105 }).pipe(Effect.orDie);119 }).pipe(Effect.orDie);
106 }120 }
107 return {121 return {
...@@ -180,9 +194,16 @@ describe("CursorAdapterV2", () => {...@@ -180,9 +194,16 @@ describe("CursorAdapterV2", () => {
180 const rows = events.filter((event) => event.type === "subagent.updated");194 const rows = events.filter((event) => event.type === "subagent.updated");
181 assert.equal(rows[0]?.subagent.status, "running");195 assert.equal(rows[0]?.subagent.status, "running");
182 assert.equal(rows[0]?.subagent.model, model ?? null);196 assert.equal(rows[0]?.subagent.model, model ?? null);
197 assert.equal(rows.at(-1)?.subagent.model, lateModel ?? model ?? null);
183 assert.equal(198 assert.equal(
184 rows.at(-1)?.subagent.status,199 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",
186 );207 );
187 assert.isNotNull(rows.at(-1)?.subagent.completedAt);208 assert.isNotNull(rows.at(-1)?.subagent.completedAt);
188 }).pipe(Effect.scoped, Effect.provide(Layer.merge(NodeServices.layer, IdAllocator.layer))),209 }).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(...@@ -1572,10 +1572,10 @@ export function makeCursorAdapterV2(
1572 },1572 },
1573 prompt: args.prompt,1573 prompt: args.prompt,
1574 title: args.description,1574 title: args.description,
1575 model: args.model?.trim() || null,
1576 result: null,1575 result: null,
1577 startedAt: now,1576 startedAt: now,
1578 }),1577 }),
1578 model: args.model?.trim() || existing?.task.model || null,
1579 nativeTaskRef: {1579 nativeTaskRef: {
1580 driver: CursorAgentSdk.CURSOR_PROVIDER,1580 driver: CursorAgentSdk.CURSOR_PROVIDER,
1581 nativeId: input.callId,1581 nativeId: input.callId,