| 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,6 +1049,19 @@ describe("AcpAdapterV2", () => { |
| 1049 | // Production thread 54aeb6d7 split after "(command". Metadata shapes | 1049 | // 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"; |
| 32 | import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; | 32 | import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| 33 | import { SpawnExecutableResolution } from "@t3tools/shared/shell"; | 33 | import { SpawnExecutableResolution } from "@t3tools/shared/shell"; |
| 34 | import * as CodexClient from "effect-codex-app-server/client"; | 34 | import * as CodexClient from "effect-codex-app-server/client"; |
| 35 | import * as CodexError from "effect-codex-app-server/errors"; | ||
| 35 | import * as CodexReplay from "effect-codex-app-server/replay"; | 36 | import * as CodexReplay from "effect-codex-app-server/replay"; |
| 36 | import * as DateTime from "effect/DateTime"; | 37 | import * as DateTime from "effect/DateTime"; |
| 37 | import * as Deferred from "effect/Deferred"; | 38 | import * 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 | }); |
| 6709 | 6710 | ||
| 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 | ); |
| 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 | 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"; |
| 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 4 | import { type ProviderReplayTranscript } from "@t3tools/contracts"; | 4 | import { type ProviderReplayTranscript } from "@t3tools/contracts"; |
| 5 | import * as CodexClient from "effect-codex-app-server/client"; | 5 | import * as CodexClient from "effect-codex-app-server/client"; |
| 6 | import type * as CodexError from "effect-codex-app-server/errors"; | ||
| 6 | import * as CodexReplay from "effect-codex-app-server/replay"; | 7 | import * as CodexReplay from "effect-codex-app-server/replay"; |
| 7 | import * as Effect from "effect/Effect"; | 8 | import * as Effect from "effect/Effect"; |
| 8 | import * as FileSystem from "effect/FileSystem"; | 9 | import * as FileSystem from "effect/FileSystem"; |
| ... | @@ -45,7 +46,10 @@ export type CodexOrchestratorReplayHarnessError = typeof CodexOrchestratorReplay | ... | @@ -45,7 +46,10 @@ export type CodexOrchestratorReplayHarnessError = typeof CodexOrchestratorReplay |
| 45 | export function withCodexReplayChildMetadata( | 46 | export 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 | ); |
| 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 | export const makeCodexAppServerSpawnCommand = Effect.fn( | 1332 | export 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.raw | 2828 | 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); |
| 40 | 40 | ||
| 41 | describe("CursorAdapterV2", () => { | 41 | describe("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, |