Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7029,13 +7029,17 @@ describe("CodexAdapterV2 post-settle continuation", () => {
: {
thread: {
id: name.includes("wrong child") ? "other-child" : threadId,
...(name.startsWith("current Codex") ? { model: "gpt-6-sol" } : {}),
...(name.startsWith("current Codex")
? { model: "gpt-6-sol", reasoningEffort: "high" }
: {}),
},
model: name.startsWith("current Codex")
? null
: name === "wrong child"
? "gpt-5.6-sol"
: model,
reasoningEffort: "high",
serviceTier: "priority",
},
),
);
Expand All @@ -7055,6 +7059,12 @@ describe("CodexAdapterV2 post-settle continuation", () => {
yield* TestClock.adjust("100 millis");
yield* harness.firstTerminal;
assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model);
if (model) {
assert.deepEqual(harness.subagentUpdates().at(-1)?.subagent.modelSelection?.options, [
{ id: "reasoningEffort", value: "high" },
...(name.startsWith("current Codex") ? [] : [{ id: "serviceTier", value: "priority" }]),
]);
}
assert.equal(metadataRequests, name === "current Codex Sol" ? 1 : 2);
}).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),
),
Expand Down Expand Up @@ -7142,6 +7152,8 @@ describe("CodexAdapterV2 post-settle continuation", () => {
threadId: RESUME_CHILD_THREAD,
threadSettings: {
model,
effort: "low",
serviceTier: "ultrafast",
modelProvider: "openai",
cwd: "/workspace",
approvalPolicy: "never",
Expand All @@ -7152,12 +7164,32 @@ describe("CodexAdapterV2 post-settle continuation", () => {
},
},
};
const initialSettings: CodexReplay.CodexAppServerReplayEntry = {
type: "emit_inbound",
frame: {
method: "thread/settings/updated",
params: {
threadId: RESUME_CHILD_THREAD,
threadSettings: {
model: "gpt-6-astra",
effort: "low",
serviceTier: "ultrafast",
modelProvider: "openai",
cwd: "/workspace",
approvalPolicy: "never",
approvalsReviewer: "auto_review",
collaborationMode: { mode: "default", settings: { model: "gpt-6-astra" } },
sandboxPolicy: { type: "dangerFullAccess" },
},
},
},
};
const harness = yield* makeCodexReplayHarness(
{
...resumeSubagentTranscript,
entries: resumeSubagentTranscript.entries.flatMap((entry) =>
entry.type === "emit_inbound" && entry.label === "turn/completed/root"
? [entry, notification]
? [entry, initialSettings, notification]
: [entry],
),
},
Expand Down Expand Up @@ -7186,6 +7218,10 @@ describe("CodexAdapterV2 post-settle continuation", () => {
yield* Deferred.succeed(releaseMetadata, undefined);
yield* TestClock.adjust("30 seconds");
assert.equal(harness.subagentUpdates().at(-1)?.subagent.model, model);
assert.deepEqual(harness.subagentUpdates().at(-1)?.subagent.modelSelection?.options, [
{ id: "reasoningEffort", value: "low" },
{ id: "serviceTier", value: "ultrafast" },
]);
}).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),
),
);
Expand Down
103 changes: 85 additions & 18 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1352,18 +1352,20 @@ const decodeCodexResumeMetadata = Schema.decodeUnknownEffect(
Schema.Struct({ thread: Schema.Struct({ id: Schema.String, updatedAt: Schema.Number }) }),
);

const decodeCodexChildModel = Schema.decodeUnknownEffect(
Schema.Struct({
thread: Schema.Struct({ id: Schema.String }),
model: Schema.NullOr(Schema.String),
}),
);
const CodexChildSelection = Schema.Struct({
thread: Schema.Struct({ id: Schema.String }),
model: Schema.NullOr(Schema.String),
reasoningEffort: Schema.optional(Schema.NullOr(Schema.String)),
serviceTier: Schema.optional(Schema.NullOr(Schema.String)),
});
const decodeCodexChildModel = Schema.decodeUnknownEffect(CodexChildSelection);

const decodeCodexChildThread = Schema.decodeUnknownEffect(
Schema.Struct({
thread: Schema.Struct({
id: Schema.String,
model: Schema.optional(Schema.NullOr(Schema.String)),
reasoningEffort: Schema.optional(Schema.NullOr(Schema.String)),
}),
}),
);
Expand Down Expand Up @@ -1834,7 +1836,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
const pendingRootTurns = yield* Ref.make(new Map<string, ProviderAdapterV2TurnInput>());
const turnWaiters = yield* Ref.make(new Map<string, Deferred.Deferred<void, never>>());
const subagentThreads = yield* Ref.make(new Map<string, CodexSubagentThreadContext>());
const subagentModels = new Map<string, string>();
const subagentSelections = new Map<string, Omit<ModelSelection, "instanceId">>();
const pendingSubagentTurns = yield* Ref.make(
new Map<string, ReadonlyArray<PendingCodexSubagentTurnStarted>>(),
);
Expand Down Expand Up @@ -2669,16 +2671,51 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
});
});

const updateSubagentModel = Effect.fnUntraced(function* (
const updateSubagentSelection = Effect.fnUntraced(function* (
nativeThreadId: string,
value: string | null,
effort?: string | null,
tier?: string | null,
) {
const model = value?.trim();
if (!model) return;
subagentModels.set(nativeThreadId, model);
const previous = subagentSelections.get(nativeThreadId);
const previousOptions = previous?.options;
const options =
effort === undefined && tier === undefined
? previousOptions
: [
...(previousOptions ?? []).filter(({ id }) =>
id === "reasoningEffort"
? effort === undefined
: id === "serviceTier" && tier === undefined,
),
...[
{ id: "reasoningEffort", value: effort },
{ id: "serviceTier", value: tier },
].flatMap(({ id, value }) =>
value?.trim() ? [{ id, value: value.trim() }] : [],
),
];
const selection = {
model,
...(options === undefined ? {} : { options }),
};
subagentSelections.set(nativeThreadId, selection);
const subagent = (yield* Ref.get(subagentThreads)).get(nativeThreadId);
if (subagent === undefined || subagent.task.model === model) return;
subagent.task = { ...subagent.task, model, updatedAt: yield* DateTime.now };
if (subagent === undefined) return;
const modelSelection = { instanceId: subagent.task.providerInstanceId, ...selection };
if (
subagent.task.modelSelection &&
modelSelectionsEqual(subagent.task.modelSelection, modelSelection)
)
return;
subagent.task = {
...subagent.task,
model,
modelSelection,
updatedAt: yield* DateTime.now,
};
yield* emitProviderEvent({
type: "subagent.updated",
driver: CODEX_PROVIDER,
Expand All @@ -2694,6 +2731,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
readonly prompt: string;
readonly title: string | null;
readonly model: string | null;
readonly reasoningEffort?: string | null | undefined;
readonly ordinal: number;
readonly emitInitialPrompt: boolean;
}) =>
Expand All @@ -2702,6 +2740,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
if (registeredSubagents.has(input.nativeThreadId)) {
return;
}
if (input.model && !subagentSelections.has(input.nativeThreadId)) {
yield* updateSubagentSelection(
input.nativeThreadId,
input.model,
input.reasoningEffort,
);
}

const now = yield* DateTime.now;
const subagentNodeId = idAllocator.derive.nodeFromProviderItem({
Expand Down Expand Up @@ -2744,6 +2789,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
createdAt: now,
updatedAt: now,
} satisfies OrchestrationV2ProviderThread;
const reportedSelection = subagentSelections.get(input.nativeThreadId);
const task = {
id: subagentNodeId,
threadId: input.context.projectionThreadId,
Expand All @@ -2758,7 +2804,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
nativeTaskRef: codexNativeItemRef(input.nativeItemId),
prompt: input.prompt,
title: input.title,
model: subagentModels.get(input.nativeThreadId) ?? input.model,
model: reportedSelection?.model ?? input.model,
modelSelection: reportedSelection
? {
instanceId: input.context.input.modelSelection.instanceId,
...reportedSelection,
}
: undefined,
status: "running",
result: null,
startedAt: now,
Expand Down Expand Up @@ -2913,9 +2965,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
.request("thread/read", { threadId: input.nativeThreadId, includeTurns: false })
.pipe(
Effect.flatMap(decodeCodexChildThread),
Effect.map((response) =>
Effect.map((response): typeof CodexChildSelection.Type | null =>
response.thread.id === input.nativeThreadId && response.thread.model?.trim()
? { thread: response.thread, model: response.thread.model }
? {
thread: response.thread,
model: response.thread.model,
reasoningEffort: response.thread.reasoningEffort,
}
: null,
),
Effect.catch(() => Effect.succeed(null)),
Expand All @@ -2932,8 +2988,13 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
Effect.timeout("5 seconds"),
Effect.flatMap((response) =>
response.thread.id === input.nativeThreadId &&
!subagentModels.has(input.nativeThreadId)
? updateSubagentModel(input.nativeThreadId, response.model)
!subagentSelections.has(input.nativeThreadId)
? updateSubagentSelection(
input.nativeThreadId,
response.model,
response.reasoningEffort,
response.serviceTier,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
: Effect.void,
),
Effect.catch(() => Effect.void),
Expand Down Expand Up @@ -2964,6 +3025,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
prompt: input.item.prompt ?? "",
title: null,
model,
reasoningEffort: input.item.reasoningEffort,
ordinal: index + 1,
emitInitialPrompt: true,
});
Expand Down Expand Up @@ -4296,10 +4358,15 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
);

yield* client.handleServerNotification("thread/settings/updated", (payload) =>
updateSubagentModel(payload.threadId, payload.threadSettings.model),
updateSubagentSelection(
payload.threadId,
payload.threadSettings.model,
payload.threadSettings.effort,
payload.threadSettings.serviceTier,
),
);
yield* client.handleServerNotification("model/rerouted", (payload) =>
updateSubagentModel(payload.threadId, payload.toModel),
updateSubagentSelection(payload.threadId, payload.toModel),
);

yield* client.handleServerNotification("turn/started", (payload) =>
Expand Down
9 changes: 5 additions & 4 deletions apps/web/src/components/chat/SubagentTooltipContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function SubagentTooltipContent(props: {
model: string | null;
providerInstanceId: ProviderInstanceId;
origin: OrchestrationV2Subagent["origin"];
modelSelection?: OrchestrationV2Subagent["modelSelection"];
provider?: ServerProvider | undefined;
/** The environment's instances; with several accounts on one provider, the card names this one. */
providers?: ReadonlyArray<ServerProvider> | undefined;
Expand All @@ -57,7 +58,9 @@ export function SubagentTooltipContent(props: {
? resolveSelectableModel(props.provider.driver, model, props.provider.models)
: model;
const providerModel = props.provider?.models.find((candidate) => candidate.slug === modelSlug);
const childSelection = props.childThread?.modelSelection;
const childSelection =
props.modelSelection ??
(props.origin === "app_owned" ? props.childThread?.modelSelection : undefined);
const childModel = props.provider
? (resolveSelectableModel(
props.provider.driver,
Expand All @@ -67,9 +70,7 @@ export function SubagentTooltipContent(props: {
: childSelection?.model.trim();
const provider = props.provider;
const matchingSelection =
props.origin === "app_owned" &&
childModel === (modelSlug ?? model) &&
childSelection?.instanceId === props.providerInstanceId
childModel === (modelSlug ?? model) && childSelection?.instanceId === props.providerInstanceId
? childSelection
: undefined;
const effort = ["reasoningEffort", "effort", "reasoning", "variant"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,39 @@ it("shows readable models and only differing workspace details in agent tooltips
expect(text(true)).toContain("My GPT · Work account · high");
expect(text()).toContain("My GPT · Work account · Fast mode onhigh");
expect(text()).not.toContain("Personal account");
state.shells = [];
for (const status of ["running", "completed", "failed"] as const) {
state.projection = {
...projection,
subagents: [
{
...projection.subagents[0],
origin: "provider_native",
status,
modelSelection: {
instanceId: "codex",
model: "gpt-5.4",
options: [
{ id: "reasoningEffort", value: "low" },
{ id: "serviceTier", value: "ultrafast" },
],
},
},
],
};
await act(async () => renderer.update(cloneElement(panel)));
if (status === "completed") {
await act(async () =>
renderer.root.findByProps({ type: "button", "aria-expanded": false }).props.onClick(),
);
}
expect(text()).toContain("My GPT · Work account · Ultrafast mode onlow");
expect(text()).not.toContain(" · high");
}
await act(async () =>
renderer.root.findByProps({ type: "button", "aria-expanded": true }).props.onClick(),
);
state.projection = projection;
state.configs.set("test", speedConfig);
child.modelSelection.options = [{ id: "reasoningEffort", value: "high" }];
state.shells = [
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/chat/ThreadRelationshipsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ export function ThreadRelationshipsPanel(props: {
driver: subagent.driver,
providerInstanceId: subagent.providerInstanceId,
origin: subagent.origin,
modelSelection: subagent.modelSelection,
},
]),
),
Expand Down Expand Up @@ -412,6 +413,7 @@ export function ThreadRelationshipsPanel(props: {
model={agent.model}
providerInstanceId={agent.providerInstanceId}
origin={agent.origin}
modelSelection={agent.modelSelection}
provider={provider}
providers={providers}
driver={providerDriver}
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/components/chat/V2LifecycleRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@ function SubagentTimelineLink(props: {
{...props}
elapsed={agent ? <AgentElapsed agent={projectedSubagentsToRuntime([agent])[0]!} /> : null}
model={agent?.model ?? null}
modelSelection={agent?.modelSelection}
status={liveStatus}
result={agent?.result ?? props.result}
progress={agent?.progress ?? props.progress}
Expand All @@ -532,7 +533,11 @@ function SubagentTimelineLink(props: {
}

function SubagentTimelineTooltip(
props: Parameters<typeof SubagentTimelineLink>[0] & { model: string | null; elapsed: ReactNode },
props: Parameters<typeof SubagentTimelineLink>[0] & {
model: string | null;
modelSelection: OrchestrationV2Subagent["modelSelection"];
elapsed: ReactNode;
},
) {
const environmentId = props.parentRef.environmentId;
const parent = useThreadShell(props.parentRef)?.source;
Expand All @@ -549,6 +554,7 @@ function SubagentTimelineTooltip(
model={props.model}
providerInstanceId={props.providerInstanceId}
origin={props.origin}
modelSelection={props.modelSelection}
provider={props.provider}
providers={props.providers}
driver={props.driver}
Expand Down
Loading
Loading