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