authorgravatar for 62337003+Bil0000@users.noreply.github.comBilal Bakr <62337003+Bil0000@users.noreply.github.com> 2026-10-08 09:08:14+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-07 23:08:14-07:00
log57f96e69d39669f47e7310062222cd67adbb4912
tree49c2dd93c6ded17485b797e04baf8e89e7cc251c
parentff9eb8bd0c8e71f98969a58d5789889e07b57c8b
signature Signed by PGP key B5690EEEBB952194

fix(lineage): keep agent effort and speed after completion (#16925)


7 files changed, 171 insertions(+), 25 deletions(-)

apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts+38-2
...@@ -7029,13 +7029,17 @@ describe("CodexAdapterV2 post-settle continuation", () => {...@@ -7029,13 +7029,17 @@ describe("CodexAdapterV2 post-settle continuation", () => {
7029 : {7029 : {
7030 thread: {7030 thread: {
7031 id: name.includes("wrong child") ? "other-child" : threadId,7031 id: name.includes("wrong child") ? "other-child" : threadId,
7032 ...(name.startsWith("current Codex") ? { model: "gpt-6-sol" } : {}),7032 ...(name.startsWith("current Codex")
7033 ? { model: "gpt-6-sol", reasoningEffort: "high" }
7034 : {}),
7033 },7035 },
7034 model: name.startsWith("current Codex")7036 model: name.startsWith("current Codex")
7035 ? null7037 ? null
7036 : name === "wrong child"7038 : name === "wrong child"
7037 ? "gpt-5.6-sol"7039 ? "gpt-5.6-sol"
7038 : model,7040 : model,
7041 reasoningEffort: "high",
7042 serviceTier: "priority",
7039 },7043 },
7040 ),7044 ),
7041 );7045 );
...@@ -7055,6 +7059,12 @@ describe("CodexAdapterV2 post-settle continuation", () => {...@@ -7055,6 +7059,12 @@ describe("CodexAdapterV2 post-settle continuation", () => {
7055 yield* TestClock.adjust("100 millis");7059 yield* TestClock.adjust("100 millis");
7056 yield* harness.firstTerminal;7060 yield* harness.firstTerminal;
7057 assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model);7061 assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model);
7062 if (model) {
7063 assert.deepEqual(harness.subagentUpdates().at(-1)?.subagent.modelSelection?.options, [
7064 { id: "reasoningEffort", value: "high" },
7065 ...(name.startsWith("current Codex") ? [] : [{ id: "serviceTier", value: "priority" }]),
7066 ]);
7067 }
7058 assert.equal(metadataRequests, name === "current Codex Sol" ? 1 : 2);7068 assert.equal(metadataRequests, name === "current Codex Sol" ? 1 : 2);
7059 }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),7069 }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),
7060 ),7070 ),
...@@ -7142,6 +7152,8 @@ describe("CodexAdapterV2 post-settle continuation", () => {...@@ -7142,6 +7152,8 @@ describe("CodexAdapterV2 post-settle continuation", () => {
7142 threadId: RESUME_CHILD_THREAD,7152 threadId: RESUME_CHILD_THREAD,
7143 threadSettings: {7153 threadSettings: {
7144 model,7154 model,
7155 effort: "low",
7156 serviceTier: "ultrafast",
7145 modelProvider: "openai",7157 modelProvider: "openai",
7146 cwd: "/workspace",7158 cwd: "/workspace",
7147 approvalPolicy: "never",7159 approvalPolicy: "never",
...@@ -7152,12 +7164,32 @@ describe("CodexAdapterV2 post-settle continuation", () => {...@@ -7152,12 +7164,32 @@ describe("CodexAdapterV2 post-settle continuation", () => {
7152 },7164 },
7153 },7165 },
7154 };7166 };
7167 const initialSettings: CodexReplay.CodexAppServerReplayEntry = {
7168 type: "emit_inbound",
7169 frame: {
7170 method: "thread/settings/updated",
7171 params: {
7172 threadId: RESUME_CHILD_THREAD,
7173 threadSettings: {
7174 model: "gpt-6-astra",
7175 effort: "low",
7176 serviceTier: "ultrafast",
7177 modelProvider: "openai",
7178 cwd: "/workspace",
7179 approvalPolicy: "never",
7180 approvalsReviewer: "auto_review",
7181 collaborationMode: { mode: "default", settings: { model: "gpt-6-astra" } },
7182 sandboxPolicy: { type: "dangerFullAccess" },
7183 },
7184 },
7185 },
7186 };
7155 const harness = yield* makeCodexReplayHarness(7187 const harness = yield* makeCodexReplayHarness(
7156 {7188 {
7157 ...resumeSubagentTranscript,7189 ...resumeSubagentTranscript,
7158 entries: resumeSubagentTranscript.entries.flatMap((entry) =>7190 entries: resumeSubagentTranscript.entries.flatMap((entry) =>
7159 entry.type === "emit_inbound" && entry.label === "turn/completed/root"7191 entry.type === "emit_inbound" && entry.label === "turn/completed/root"
7160 ? [entry, notification]7192 ? [entry, initialSettings, notification]
7161 : [entry],7193 : [entry],
7162 ),7194 ),
7163 },7195 },
...@@ -7186,6 +7218,10 @@ describe("CodexAdapterV2 post-settle continuation", () => {...@@ -7186,6 +7218,10 @@ describe("CodexAdapterV2 post-settle continuation", () => {
7186 yield* Deferred.succeed(releaseMetadata, undefined);7218 yield* Deferred.succeed(releaseMetadata, undefined);
7187 yield* TestClock.adjust("30 seconds");7219 yield* TestClock.adjust("30 seconds");
7188 assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model);7220 assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model);
7221 assert.deepEqual(harness.subagentUpdates().at(-1)?.subagent.modelSelection?.options, [
7222 { id: "reasoningEffort", value: "low" },
7223 { id: "serviceTier", value: "ultrafast" },
7224 ]);
7189 }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),7225 }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),
7190 ),7226 ),
7191 );7227 );
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts+85-18
...@@ -1352,18 +1352,20 @@ const decodeCodexResumeMetadata = Schema.decodeUnknownEffect(...@@ -1352,18 +1352,20 @@ const decodeCodexResumeMetadata = Schema.decodeUnknownEffect(
1352 Schema.Struct({ thread: Schema.Struct({ id: Schema.String, updatedAt: Schema.Number }) }),1352 Schema.Struct({ thread: Schema.Struct({ id: Schema.String, updatedAt: Schema.Number }) }),
1353);1353);
13541354
1355const decodeCodexChildModel = Schema.decodeUnknownEffect(1355const CodexChildSelection = Schema.Struct({
1356 Schema.Struct({1356 thread: Schema.Struct({ id: Schema.String }),
1357 thread: Schema.Struct({ id: Schema.String }),1357 model: Schema.NullOr(Schema.String),
1358 model: Schema.NullOr(Schema.String),1358 reasoningEffort: Schema.optional(Schema.NullOr(Schema.String)),
1359 }),1359 serviceTier: Schema.optional(Schema.NullOr(Schema.String)),
1360);1360});
1361const decodeCodexChildModel = Schema.decodeUnknownEffect(CodexChildSelection);
13611362
1362const decodeCodexChildThread = Schema.decodeUnknownEffect(1363const decodeCodexChildThread = Schema.decodeUnknownEffect(
1363 Schema.Struct({1364 Schema.Struct({
1364 thread: Schema.Struct({1365 thread: Schema.Struct({
1365 id: Schema.String,1366 id: Schema.String,
1366 model: Schema.optional(Schema.NullOr(Schema.String)),1367 model: Schema.optional(Schema.NullOr(Schema.String)),
1368 reasoningEffort: Schema.optional(Schema.NullOr(Schema.String)),
1367 }),1369 }),
1368 }),1370 }),
1369);1371);
...@@ -1834,7 +1836,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi...@@ -1834,7 +1836,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
1834 const pendingRootTurns = yield* Ref.make(new Map<string, ProviderAdapterV2TurnInput>());1836 const pendingRootTurns = yield* Ref.make(new Map<string, ProviderAdapterV2TurnInput>());
1835 const turnWaiters = yield* Ref.make(new Map<string, Deferred.Deferred<void, never>>());1837 const turnWaiters = yield* Ref.make(new Map<string, Deferred.Deferred<void, never>>());
1836 const subagentThreads = yield* Ref.make(new Map<string, CodexSubagentThreadContext>());1838 const subagentThreads = yield* Ref.make(new Map<string, CodexSubagentThreadContext>());
1837 const subagentModels = new Map<string, string>();1839 const subagentSelections = new Map<string, Omit<ModelSelection, "instanceId">>();
1838 const pendingSubagentTurns = yield* Ref.make(1840 const pendingSubagentTurns = yield* Ref.make(
1839 new Map<string, ReadonlyArray<PendingCodexSubagentTurnStarted>>(),1841 new Map<string, ReadonlyArray<PendingCodexSubagentTurnStarted>>(),
1840 );1842 );
...@@ -2669,16 +2671,51 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi...@@ -2669,16 +2671,51 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
2669 });2671 });
2670 });2672 });
26712673
2672 const updateSubagentModel = Effect.fnUntraced(function* (2674 const updateSubagentSelection = Effect.fnUntraced(function* (
2673 nativeThreadId: string,2675 nativeThreadId: string,
2674 value: string | null,2676 value: string | null,
2677 effort?: string | null,
2678 tier?: string | null,
2675 ) {2679 ) {
2676 const model = value?.trim();2680 const model = value?.trim();
2677 if (!model) return;2681 if (!model) return;
2678 subagentModels.set(nativeThreadId, model);2682 const previous = subagentSelections.get(nativeThreadId);
2683 const previousOptions = previous?.options;
2684 const options =
2685 effort === undefined && tier === undefined
2686 ? previousOptions
2687 : [
2688 ...(previousOptions ?? []).filter(({ id }) =>
2689 id === "reasoningEffort"
2690 ? effort === undefined
2691 : id === "serviceTier" && tier === undefined,
2692 ),
2693 ...[
2694 { id: "reasoningEffort", value: effort },
2695 { id: "serviceTier", value: tier },
2696 ].flatMap(({ id, value }) =>
2697 value?.trim() ? [{ id, value: value.trim() }] : [],
2698 ),
2699 ];
2700 const selection = {
2701 model,
2702 ...(options === undefined ? {} : { options }),
2703 };
2704 subagentSelections.set(nativeThreadId, selection);
2679 const subagent = (yield* Ref.get(subagentThreads)).get(nativeThreadId);2705 const subagent = (yield* Ref.get(subagentThreads)).get(nativeThreadId);
2680 if (subagent === undefined || subagent.task.model === model) return;2706 if (subagent === undefined) return;
2681 subagent.task = { ...subagent.task, model, updatedAt: yield* DateTime.now };2707 const modelSelection = { instanceId: subagent.task.providerInstanceId, ...selection };
2708 if (
2709 subagent.task.modelSelection &&
2710 modelSelectionsEqual(subagent.task.modelSelection, modelSelection)
2711 )
2712 return;
2713 subagent.task = {
2714 ...subagent.task,
2715 model,
2716 modelSelection,
2717 updatedAt: yield* DateTime.now,
2718 };
2682 yield* emitProviderEvent({2719 yield* emitProviderEvent({
2683 type: "subagent.updated",2720 type: "subagent.updated",
2684 driver: CODEX_PROVIDER,2721 driver: CODEX_PROVIDER,
...@@ -2694,6 +2731,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi...@@ -2694,6 +2731,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
2694 readonly prompt: string;2731 readonly prompt: string;
2695 readonly title: string | null;2732 readonly title: string | null;
2696 readonly model: string | null;2733 readonly model: string | null;
2734 readonly reasoningEffort?: string | null | undefined;
2697 readonly ordinal: number;2735 readonly ordinal: number;
2698 readonly emitInitialPrompt: boolean;2736 readonly emitInitialPrompt: boolean;
2699 }) =>2737 }) =>
...@@ -2702,6 +2740,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi...@@ -2702,6 +2740,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
2702 if (registeredSubagents.has(input.nativeThreadId)) {2740 if (registeredSubagents.has(input.nativeThreadId)) {
2703 return;2741 return;
2704 }2742 }
2743 if (input.model && !subagentSelections.has(input.nativeThreadId)) {
2744 yield* updateSubagentSelection(
2745 input.nativeThreadId,
2746 input.model,
2747 input.reasoningEffort,
2748 );
2749 }
27052750
2706 const now = yield* DateTime.now;2751 const now = yield* DateTime.now;
2707 const subagentNodeId = idAllocator.derive.nodeFromProviderItem({2752 const subagentNodeId = idAllocator.derive.nodeFromProviderItem({
...@@ -2744,6 +2789,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi...@@ -2744,6 +2789,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
2744 createdAt: now,2789 createdAt: now,
2745 updatedAt: now,2790 updatedAt: now,
2746 } satisfies OrchestrationV2ProviderThread;2791 } satisfies OrchestrationV2ProviderThread;
2792 const reportedSelection = subagentSelections.get(input.nativeThreadId);
2747 const task = {2793 const task = {
2748 id: subagentNodeId,2794 id: subagentNodeId,
2749 threadId: input.context.projectionThreadId,2795 threadId: input.context.projectionThreadId,
...@@ -2758,7 +2804,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi...@@ -2758,7 +2804,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
2758 nativeTaskRef: codexNativeItemRef(input.nativeItemId),2804 nativeTaskRef: codexNativeItemRef(input.nativeItemId),
2759 prompt: input.prompt,2805 prompt: input.prompt,
2760 title: input.title,2806 title: input.title,
2761 model: subagentModels.get(input.nativeThreadId) ?? input.model,2807 model: reportedSelection?.model ?? input.model,
2808 modelSelection: reportedSelection
2809 ? {
2810 instanceId: input.context.input.modelSelection.instanceId,
2811 ...reportedSelection,
2812 }
2813 : undefined,
2762 status: "running",2814 status: "running",
2763 result: null,2815 result: null,
2764 startedAt: now,2816 startedAt: now,
...@@ -2913,9 +2965,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi...@@ -2913,9 +2965,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
2913 .request("thread/read", { threadId: input.nativeThreadId, includeTurns: false })2965 .request("thread/read", { threadId: input.nativeThreadId, includeTurns: false })
2914 .pipe(2966 .pipe(
2915 Effect.flatMap(decodeCodexChildThread),2967 Effect.flatMap(decodeCodexChildThread),
2916 Effect.map((response) =>2968 Effect.map((response): typeof CodexChildSelection.Type | null =>
2917 response.thread.id === input.nativeThreadId && response.thread.model?.trim()2969 response.thread.id === input.nativeThreadId && response.thread.model?.trim()
2918 ? { thread: response.thread, model: response.thread.model }2970 ? {
2971 thread: response.thread,
2972 model: response.thread.model,
2973 reasoningEffort: response.thread.reasoningEffort,
2974 }
2919 : null,2975 : null,
2920 ),2976 ),
2921 Effect.catch(() => Effect.succeed(null)),2977 Effect.catch(() => Effect.succeed(null)),
...@@ -2932,8 +2988,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi...@@ -2932,8 +2988,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
2932 Effect.timeout("5 seconds"),2988 Effect.timeout("5 seconds"),
2933 Effect.flatMap((response) =>2989 Effect.flatMap((response) =>
2934 response.thread.id === input.nativeThreadId &&2990 response.thread.id === input.nativeThreadId &&
2935 !subagentModels.has(input.nativeThreadId)2991 !subagentSelections.has(input.nativeThreadId)
2936 ? updateSubagentModel(input.nativeThreadId, response.model)2992 ? updateSubagentSelection(
2993 input.nativeThreadId,
2994 response.model,
2995 response.reasoningEffort,
2996 response.serviceTier,
2997 )
2937 : Effect.void,2998 : Effect.void,
2938 ),2999 ),
2939 Effect.catch(() => Effect.void),3000 Effect.catch(() => Effect.void),
...@@ -2964,6 +3025,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi...@@ -2964,6 +3025,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
2964 prompt: input.item.prompt ?? "",3025 prompt: input.item.prompt ?? "",
2965 title: null,3026 title: null,
2966 model,3027 model,
3028 reasoningEffort: input.item.reasoningEffort,
2967 ordinal: index + 1,3029 ordinal: index + 1,
2968 emitInitialPrompt: true,3030 emitInitialPrompt: true,
2969 });3031 });
...@@ -4296,10 +4358,15 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi...@@ -4296,10 +4358,15 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
4296 );4358 );
42974359
4298 yield* client.handleServerNotification("thread/settings/updated", (payload) =>4360 yield* client.handleServerNotification("thread/settings/updated", (payload) =>
4299 updateSubagentModel(payload.threadId, payload.threadSettings.model),4361 updateSubagentSelection(
4362 payload.threadId,
4363 payload.threadSettings.model,
4364 payload.threadSettings.effort,
4365 payload.threadSettings.serviceTier,
4366 ),
4300 );4367 );
4301 yield* client.handleServerNotification("model/rerouted", (payload) =>4368 yield* client.handleServerNotification("model/rerouted", (payload) =>
4302 updateSubagentModel(payload.threadId, payload.toModel),4369 updateSubagentSelection(payload.threadId, payload.toModel),
4303 );4370 );
43044371
4305 yield* client.handleServerNotification("turn/started", (payload) =>4372 yield* client.handleServerNotification("turn/started", (payload) =>
apps/web/src/components/chat/SubagentTooltipContent.tsx+5-4
...@@ -35,6 +35,7 @@ export function SubagentTooltipContent(props: {...@@ -35,6 +35,7 @@ export function SubagentTooltipContent(props: {
35 model: string | null;35 model: string | null;
36 providerInstanceId: ProviderInstanceId;36 providerInstanceId: ProviderInstanceId;
37 origin: OrchestrationV2Subagent["origin"];37 origin: OrchestrationV2Subagent["origin"];
38 modelSelection?: OrchestrationV2Subagent["modelSelection"];
38 provider?: ServerProvider | undefined;39 provider?: ServerProvider | undefined;
39 /** The environment's instances; with several accounts on one provider, the card names this one. */40 /** The environment's instances; with several accounts on one provider, the card names this one. */
40 providers?: ReadonlyArray<ServerProvider> | undefined;41 providers?: ReadonlyArray<ServerProvider> | undefined;
...@@ -57,7 +58,9 @@ export function SubagentTooltipContent(props: {...@@ -57,7 +58,9 @@ export function SubagentTooltipContent(props: {
57 ? resolveSelectableModel(props.provider.driver, model, props.provider.models)58 ? resolveSelectableModel(props.provider.driver, model, props.provider.models)
58 : model;59 : model;
59 const providerModel = props.provider?.models.find((candidate) => candidate.slug === modelSlug);60 const providerModel = props.provider?.models.find((candidate) => candidate.slug === modelSlug);
60 const childSelection = props.childThread?.modelSelection;61 const childSelection =
62 props.modelSelection ??
63 (props.origin === "app_owned" ? props.childThread?.modelSelection : undefined);
61 const childModel = props.provider64 const childModel = props.provider
62 ? (resolveSelectableModel(65 ? (resolveSelectableModel(
63 props.provider.driver,66 props.provider.driver,
...@@ -67,9 +70,7 @@ export function SubagentTooltipContent(props: {...@@ -67,9 +70,7 @@ export function SubagentTooltipContent(props: {
67 : childSelection?.model.trim();70 : childSelection?.model.trim();
68 const provider = props.provider;71 const provider = props.provider;
69 const matchingSelection =72 const matchingSelection =
70 props.origin === "app_owned" &&73 childModel === (modelSlug ?? model) && childSelection?.instanceId === props.providerInstanceId
71 childModel === (modelSlug ?? model) &&
72 childSelection?.instanceId === props.providerInstanceId
73 ? childSelection74 ? childSelection
74 : undefined;75 : undefined;
75 const effort = ["reasoningEffort", "effort", "reasoning", "variant"]76 const effort = ["reasoningEffort", "effort", "reasoning", "variant"]
apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx+33
...@@ -579,6 +579,39 @@ it("shows readable models and only differing workspace details in agent tooltips...@@ -579,6 +579,39 @@ it("shows readable models and only differing workspace details in agent tooltips
579 expect(text(true)).toContain("My GPT · Work account · high");579 expect(text(true)).toContain("My GPT · Work account · high");
580 expect(text()).toContain("My GPT · Work account · Fast mode onhigh");580 expect(text()).toContain("My GPT · Work account · Fast mode onhigh");
581 expect(text()).not.toContain("Personal account");581 expect(text()).not.toContain("Personal account");
582 state.shells = [];
583 for (const status of ["running", "completed", "failed"] as const) {
584 state.projection = {
585 ...projection,
586 subagents: [
587 {
588 ...projection.subagents[0],
589 origin: "provider_native",
590 status,
591 modelSelection: {
592 instanceId: "codex",
593 model: "gpt-5.4",
594 options: [
595 { id: "reasoningEffort", value: "low" },
596 { id: "serviceTier", value: "ultrafast" },
597 ],
598 },
599 },
600 ],
601 };
602 await act(async () => renderer.update(cloneElement(panel)));
603 if (status === "completed") {
604 await act(async () =>
605 renderer.root.findByProps({ type: "button", "aria-expanded": false }).props.onClick(),
606 );
607 }
608 expect(text()).toContain("My GPT · Work account · Ultrafast mode onlow");
609 expect(text()).not.toContain(" · high");
610 }
611 await act(async () =>
612 renderer.root.findByProps({ type: "button", "aria-expanded": true }).props.onClick(),
613 );
614 state.projection = projection;
582 state.configs.set("test", speedConfig);615 state.configs.set("test", speedConfig);
583 child.modelSelection.options = [{ id: "reasoningEffort", value: "high" }];616 child.modelSelection.options = [{ id: "reasoningEffort", value: "high" }];
584 state.shells = [617 state.shells = [
apps/web/src/components/chat/ThreadRelationshipsControl.tsx+2
...@@ -212,6 +212,7 @@ export function ThreadRelationshipsPanel(props: {...@@ -212,6 +212,7 @@ export function ThreadRelationshipsPanel(props: {
212 driver: subagent.driver,212 driver: subagent.driver,
213 providerInstanceId: subagent.providerInstanceId,213 providerInstanceId: subagent.providerInstanceId,
214 origin: subagent.origin,214 origin: subagent.origin,
215 modelSelection: subagent.modelSelection,
215 },216 },
216 ]),217 ]),
217 ),218 ),
...@@ -412,6 +413,7 @@ export function ThreadRelationshipsPanel(props: {...@@ -412,6 +413,7 @@ export function ThreadRelationshipsPanel(props: {
412 model={agent.model}413 model={agent.model}
413 providerInstanceId={agent.providerInstanceId}414 providerInstanceId={agent.providerInstanceId}
414 origin={agent.origin}415 origin={agent.origin}
416 modelSelection={agent.modelSelection}
415 provider={provider}417 provider={provider}
416 providers={providers}418 providers={providers}
417 driver={providerDriver}419 driver={providerDriver}
apps/web/src/components/chat/V2LifecycleRow.tsx+7-1
...@@ -522,6 +522,7 @@ function SubagentTimelineLink(props: {...@@ -522,6 +522,7 @@ function SubagentTimelineLink(props: {
522 {...props}522 {...props}
523 elapsed={agent ? <AgentElapsed agent={projectedSubagentsToRuntime([agent])[0]!} /> : null}523 elapsed={agent ? <AgentElapsed agent={projectedSubagentsToRuntime([agent])[0]!} /> : null}
524 model={agent?.model ?? null}524 model={agent?.model ?? null}
525 modelSelection={agent?.modelSelection}
525 status={liveStatus}526 status={liveStatus}
526 result={agent?.result ?? props.result}527 result={agent?.result ?? props.result}
527 progress={agent?.progress ?? props.progress}528 progress={agent?.progress ?? props.progress}
...@@ -532,7 +533,11 @@ function SubagentTimelineLink(props: {...@@ -532,7 +533,11 @@ function SubagentTimelineLink(props: {
532}533}
533534
534function SubagentTimelineTooltip(535function SubagentTimelineTooltip(
535 props: Parameters<typeof SubagentTimelineLink>[0] & { model: string | null; elapsed: ReactNode },536 props: Parameters<typeof SubagentTimelineLink>[0] & {
537 model: string | null;
538 modelSelection: OrchestrationV2Subagent["modelSelection"];
539 elapsed: ReactNode;
540 },
536) {541) {
537 const environmentId = props.parentRef.environmentId;542 const environmentId = props.parentRef.environmentId;
538 const parent = useThreadShell(props.parentRef)?.source;543 const parent = useThreadShell(props.parentRef)?.source;
...@@ -549,6 +554,7 @@ function SubagentTimelineTooltip(...@@ -549,6 +554,7 @@ function SubagentTimelineTooltip(
549 model={props.model}554 model={props.model}
550 providerInstanceId={props.providerInstanceId}555 providerInstanceId={props.providerInstanceId}
551 origin={props.origin}556 origin={props.origin}
557 modelSelection={props.modelSelection}
552 provider={props.provider}558 provider={props.provider}
553 providers={props.providers}559 providers={props.providers}
554 driver={props.driver}560 driver={props.driver}
packages/contracts/src/orchestrationV2.ts+1
...@@ -675,6 +675,7 @@ export const OrchestrationV2Subagent = Schema.Struct({...@@ -675,6 +675,7 @@ export const OrchestrationV2Subagent = Schema.Struct({
675 prompt: Schema.String,675 prompt: Schema.String,
676 title: Schema.NullOr(Schema.String),676 title: Schema.NullOr(Schema.String),
677 model: Schema.NullOr(Schema.String),677 model: Schema.NullOr(Schema.String),
678 modelSelection: Schema.optional(ModelSelection),
678 // Parent-wake policy for app-owned tasks: "always" offers a continuation on679 // Parent-wake policy for app-owned tasks: "always" offers a continuation on
679 // every terminal (async delegations; queue_after_active sequences it behind680 // every terminal (async delegations; queue_after_active sequences it behind
680 // a live parent run), "settled_only" offers only when the parent has no681 // a live parent run), "settled_only" offers only when the parent has no