From 43f7dc4ad1019b3840801af8599ec95ca93f7299 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 1 Nov 2026 19:47:11 +0100 Subject: [PATCH] refactor(server): the agent manager converts Codex's legacy fast_mode for the adapter behind the provider id The rename is adapter-specific, so it lives where the provider id resolves to its adapter: AgentManager converts an agent's own options on create, resume, import, reload, apply and draft controls, following derivedFromProviderId, so a custom provider extending codex keeps working with a saved or requested fast_mode. Storage, MCP, schedules, hub and profiles pass options through. The featureValues-to-options fold stays provider-agnostic at the legacy edges. --- docs/providers.md | 19 ++-- .../src/server/agent/agent-manager.test.ts | 84 ++++++++--------- .../server/src/server/agent/agent-manager.ts | 73 +++++++++------ .../src/server/agent/agent-storage.test.ts | 14 --- .../server/src/server/agent/agent-storage.ts | 2 +- .../src/server/agent/create-agent/create.ts | 5 +- .../src/server/agent/mcp-server.test.ts | 16 ++-- .../src/server/agent/option-controls.test.ts | 29 ++---- .../src/server/agent/option-controls.ts | 38 ++++---- .../server/agent/provider-registry.test.ts | 12 +-- .../codex-app-server-agent.features.test.ts | 92 +++++++++++++------ .../src/server/agent/tools/paseo-tools.ts | 19 +--- .../src/server/daemon-config-store.test.ts | 33 ++----- .../src/server/hub/execution-controller.ts | 3 +- .../server/src/server/schedule/service.ts | 2 +- packages/server/src/server/session.ts | 1 - .../agent-config/agent-config-session.test.ts | 30 +----- .../agent-config/agent-config-session.ts | 21 +---- .../provider/provider-catalog-session.ts | 5 +- 19 files changed, 224 insertions(+), 274 deletions(-) diff --git a/docs/providers.md b/docs/providers.md index 3bbb4f670d..ae2ccf04b7 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -59,13 +59,18 @@ or without a control. none of it is stored. Emit nothing from inside the call. If the provider's own state moved before it refused, it reports the moved values once as `options_changed` after the call has ended, because the stored options must match what runs. -- **Old data converts where it enters.** Codex used to take `fast_mode`; it takes - `service_tier` now. `convertLegacyOptions` (`option-controls.ts`) turns `fast_mode` into - `service_tier` as stored records, `config.json` defaults, profiles, older clients' requests, - MCP `settings.features` and plugin hooks come in (`true` is `"fast"`, `false` is `"default"`, - and a `service_tier` in the same input wins). Past that point nothing sees `fast_mode` for Codex. - Codex resolves `"fast"` to the model's Fast tier and reports the id it runs through - `openOptionsPatch` or the `applyOptions` result like any other value. +- **Old data converts in the agent manager.** Codex used to take `fast_mode`; it takes + `service_tier` now. `AgentManager` runs `convertLegacyOptions` (`option-controls.ts`) on an + agent's own options when it creates, resumes, imports, reloads or applies to it, and on draft + controls, before defaults merge and before anything is stored (`true` is `"fast"`, `false` is + `"default"`, and a `service_tier` in the same input wins). The manager converts for the adapter + behind the provider id, following `derivedFromProviderId`, so a custom provider with + `extends: "codex"` is converted too. Configured defaults convert in the provider registry. A + stored record keeps `fast_mode` until the agent is next opened or applied to, and is stored + converted from then on. Nothing past the manager sees `fast_mode` for Codex. Codex resolves + `"fast"` to the model's Fast tier and reports the id it runs through `openOptionsPatch` or the + `applyOptions` result like any other value. The `featureValues` to `options` fold is separate + and provider-agnostic; it stays at the protocol and legacy edges. - **Provider-initiated changes.** A setting the provider changes on its own is an `options_changed` event. The daemon stores and forwards it and does not push the old value back. diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 71ca6b0ea6..a630d58d2a 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -2448,7 +2448,7 @@ test("listDraftControls uses explicit model config without default model fetchin const workdir = mkdtempSync(join(tmpdir(), "agent-manager-draft-features-")); const storagePath = join(workdir, "agents"); const storage = new AgentStorage(storagePath, logger); - const draftControl = createControl({ key: "fast_mode", label: "Fast mode" }); + const draftControl = createControl({ key: "web_search", label: "Fast mode" }); class DraftFeatureClient extends TestAgentClient { fetchCatalogCalls = 0; createSessionCalls = 0; @@ -10061,7 +10061,7 @@ test("respondToPermission applies provider option changes and refreshes runtime get controls(): AgentOptionControl[] { return [ - createControl({ key: "fast_mode", label: "Fast" }), + createControl({ key: "web_search", label: "Fast" }), createControl({ key: "plan_mode", label: "Plan" }), ]; } @@ -10115,7 +10115,7 @@ test("respondToPermission applies provider option changes and refreshes runtime { provider: "codex", cwd: workdir, - providerOptions: { fast_mode: true, plan_mode: true }, + providerOptions: { web_search: true, plan_mode: true }, }, undefined, { workspaceId: undefined }, @@ -10140,14 +10140,14 @@ test("respondToPermission applies provider option changes and refreshes runtime const updated = manager.getAgent(snapshot.id); expect(updated?.pendingPermissions.size).toBe(0); - expect(updated?.effectiveOptions).toEqual({ fast_mode: true, plan_mode: false }); + expect(updated?.effectiveOptions).toEqual({ web_search: true, plan_mode: false }); expect(updated?.runtimeInfo).toMatchObject({ model: "gpt-5.4", extra: { collaborationMode: "Code" }, }); const persisted = await storage.get(snapshot.id); - expect(persisted?.config?.providerOptions).toEqual({ fast_mode: true, plan_mode: false }); + expect(persisted?.config?.providerOptions).toEqual({ web_search: true, plan_mode: false }); }); test("respondToPermission emits refreshed state before permission_resolved", async () => { @@ -10168,7 +10168,7 @@ test("respondToPermission emits refreshed state before permission_resolved", asy ]; get controls(): AgentOptionControl[] { - return [createControl({ key: "fast_mode", label: "Fast" })]; + return [createControl({ key: "web_search", label: "Fast" })]; } override async getRuntimeInfo() { @@ -10192,7 +10192,7 @@ test("respondToPermission emits refreshed state before permission_resolved", asy this.pushEvent({ type: "options_changed", provider: this.provider, - patch: { fast_mode: false }, + patch: { web_search: false }, }); this.pushEvent({ type: "permission_resolved", @@ -10224,7 +10224,7 @@ test("respondToPermission emits refreshed state before permission_resolved", asy { provider: "codex", cwd: workdir, - providerOptions: { fast_mode: true }, + providerOptions: { web_search: true }, }, undefined, { workspaceId: undefined }, @@ -10237,7 +10237,7 @@ test("respondToPermission emits refreshed state before permission_resolved", asy } if (event.type === "agent_state" && event.agent.id === snapshot.id) { seen.push( - `state:${event.agent.currentModeId}:${String(event.agent.effectiveOptions?.fast_mode)}`, + `state:${event.agent.currentModeId}:${String(event.agent.effectiveOptions?.web_search)}`, ); return; } @@ -12802,26 +12802,26 @@ describe("agent options", () => { } test("config.json defaults show as effective options without entering the agent's own options", async () => { - const { manager, agentId } = await createOptionsAgent({ fast_mode: true }); + const { manager, agentId } = await createOptionsAgent({ web_search: true }); const agent = manager.getAgent(agentId); - expect(agent?.config.providerOptions).toEqual({ fast_mode: true }); - expect(agent?.effectiveOptions).toEqual({ service_tier: "default", fast_mode: true }); - expect(toAgentPayload(agent!).options).toEqual({ service_tier: "default", fast_mode: true }); + expect(agent?.config.providerOptions).toEqual({ web_search: true }); + expect(agent?.effectiveOptions).toEqual({ service_tier: "default", web_search: true }); + expect(toAgentPayload(agent!).options).toEqual({ service_tier: "default", web_search: true }); }); test("setAgentOptions hands the provider only the keys the patch sets", async () => { - const { manager, client, storage, agentId } = await createOptionsAgent({ fast_mode: true }); + const { manager, client, storage, agentId } = await createOptionsAgent({ web_search: true }); await manager.setAgentOptions(agentId, { service_tier: "fast" }); expect(client.session?.applied).toEqual([{ service_tier: "fast" }]); const agent = manager.getAgent(agentId); - expect(agent?.config.providerOptions).toEqual({ fast_mode: true, service_tier: "fast" }); - expect(agent?.effectiveOptions).toEqual({ service_tier: "fast", fast_mode: true }); + expect(agent?.config.providerOptions).toEqual({ web_search: true, service_tier: "fast" }); + expect(agent?.effectiveOptions).toEqual({ service_tier: "fast", web_search: true }); await manager.flush(); expect((await storage.get(agentId))?.config?.providerOptions).toEqual({ - fast_mode: true, + web_search: true, service_tier: "fast", }); }); @@ -12879,23 +12879,23 @@ describe("agent options", () => { }); test("a removal with no default is sent as null", async () => { - const { manager, client, agentId } = await createOptionsAgent({ fast_mode: true }); + const { manager, client, agentId } = await createOptionsAgent({ web_search: true }); - await manager.setAgentOptions(agentId, { fast_mode: null }); + await manager.setAgentOptions(agentId, { web_search: null }); - expect(client.session?.applied).toEqual([{ fast_mode: null }]); + expect(client.session?.applied).toEqual([{ web_search: null }]); expect(manager.getAgent(agentId)?.config.providerOptions).toBeUndefined(); }); test("a value the provider kept through a removal is stored", async () => { - const { manager, client, agentId } = await createOptionsAgent({ fast_mode: true }); - client.session!.ended = { fast_mode: true }; + const { manager, client, agentId } = await createOptionsAgent({ web_search: true }); + client.session!.ended = { web_search: true }; - await manager.setAgentOptions(agentId, { fast_mode: null }); + await manager.setAgentOptions(agentId, { web_search: null }); const agent = manager.getAgent(agentId); - expect(agent?.config.providerOptions).toEqual({ fast_mode: true }); - expect(agent?.effectiveOptions).toEqual({ service_tier: "default", fast_mode: true }); + expect(agent?.config.providerOptions).toEqual({ web_search: true }); + expect(agent?.effectiveOptions).toEqual({ service_tier: "default", web_search: true }); }); test("what the provider ended up with is stored together with the patch", async () => { @@ -12913,7 +12913,7 @@ describe("agent options", () => { test("the difference a session opened with is stored before the agent is published", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-options-")); const client = new OptionsClient(); - client.openOptionsPatch = { service_tier: "slow", fast_mode: null }; + client.openOptionsPatch = { service_tier: "slow", web_search: null }; const manager = new AgentManager({ clients: { codex: client }, registry: new AgentStorage(join(workdir, "agents"), logger), @@ -12925,7 +12925,7 @@ describe("agent options", () => { }); const agent = await manager.createAgent( - { provider: "codex", cwd: workdir, providerOptions: { fast_mode: true } }, + { provider: "codex", cwd: workdir, providerOptions: { web_search: true } }, undefined, { workspaceId: undefined }, ); @@ -12996,18 +12996,18 @@ describe("agent options", () => { const slow = manager.setAgentOptions(agentId, { sandbox_mode: "read-only" }); client.session!.applyGate = null; - await manager.setAgentOptions(agentId, { fast_mode: true }); + await manager.setAgentOptions(agentId, { web_search: true }); release(); await slow; expect(manager.getAgent(agentId)?.config.providerOptions).toEqual({ sandbox_mode: "read-only", - fast_mode: true, + web_search: true, }); }); test("a stored agent that is not loaded still reports its effective options", async () => { - const { manager, agentId } = await createOptionsAgent({ fast_mode: true }); + const { manager, agentId } = await createOptionsAgent({ web_search: true }); await manager.closeAgent(agentId); const states: ManagedAgent[] = []; manager.subscribe( @@ -13019,14 +13019,14 @@ describe("agent options", () => { await manager.markAgentUnread(agentId); - expect(states.at(-1)?.effectiveOptions).toEqual({ service_tier: "default", fast_mode: true }); + expect(states.at(-1)?.effectiveOptions).toEqual({ service_tier: "default", web_search: true }); }); test("editing a provider default does not change the options of a loaded agent", async () => { - const { manager, client, agentId } = await createOptionsAgent({ fast_mode: true }); + const { manager, client, agentId } = await createOptionsAgent({ web_search: true }); expect(manager.getAgent(agentId)?.effectiveOptions).toEqual({ service_tier: "default", - fast_mode: true, + web_search: true, }); // The user edits the default in config.json while the agent is running. @@ -13034,19 +13034,19 @@ describe("agent options", () => { await manager.markAgentUnread(agentId); const agent = manager.getAgent(agentId); - expect(agent?.effectiveOptions).toEqual({ service_tier: "default", fast_mode: true }); - expect(toAgentPayload(agent!).options).toEqual({ service_tier: "default", fast_mode: true }); + expect(agent?.effectiveOptions).toEqual({ service_tier: "default", web_search: true }); + expect(toAgentPayload(agent!).options).toEqual({ service_tier: "default", web_search: true }); }); test("a provider that rejects an option leaves the stored options unchanged", async () => { - const { manager, client, agentId } = await createOptionsAgent({ fast_mode: false }); + const { manager, client, agentId } = await createOptionsAgent({ web_search: false }); client.session!.rejectWith = new Error("fast mode unavailable"); - await expect(manager.setAgentOptions(agentId, { fast_mode: true })).rejects.toThrow( + await expect(manager.setAgentOptions(agentId, { web_search: true })).rejects.toThrow( "fast mode unavailable", ); - expect(manager.getAgent(agentId)?.config.providerOptions).toEqual({ fast_mode: false }); + expect(manager.getAgent(agentId)?.config.providerOptions).toEqual({ web_search: false }); }); test("a provider whose session cannot apply options rejects a patch and stores nothing", async () => { @@ -13069,16 +13069,16 @@ describe("agent options", () => { logger, }); const agent = await manager.createAgent( - { provider: "codex", cwd: workdir, providerOptions: { fast_mode: false } }, + { provider: "codex", cwd: workdir, providerOptions: { web_search: false } }, undefined, { workspaceId: undefined }, ); - await expect(manager.setAgentOptions(agent.id, { fast_mode: true })).rejects.toThrow( - /fast_mode.*cannot change on a running session/, + await expect(manager.setAgentOptions(agent.id, { web_search: true })).rejects.toThrow( + /web_search.*cannot change on a running session/, ); - expect(manager.getAgent(agent.id)?.config.providerOptions).toEqual({ fast_mode: false }); + expect(manager.getAgent(agent.id)?.config.providerOptions).toEqual({ web_search: false }); }); test("agent.create.before hook featureValues fold into providerOptions", async () => { diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index f3beed5d27..4a7ab77c33 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -1256,7 +1256,10 @@ export class AgentManager { ): Promise<{ controls: AgentOptionControl[]; options: ProviderOptions }> { const normalizedConfig = await this.normalizeConfig(config, { resolveDefaultModel: false }); const client = this.requireClient(normalizedConfig.provider); - const options = this.effectiveOptionsFor(normalizedConfig.provider, config.providerOptions); + const options = this.effectiveOptionsFor( + normalizedConfig.provider, + this.convertLegacyOptions(normalizedConfig.provider, config.providerOptions), + ); if (!normalizedConfig.model && !client.listControls) { return { controls: [], options }; } @@ -1371,6 +1374,7 @@ export class AgentManager { ): Promise { this.assertAcceptingAgentRegistrations(); const resolvedAgentId = validateAgentId(agentId ?? this.idFactory(), "createAgent"); + config = this.withConvertedOptions(config); if (this.pluginLifecycle && !config.internal) { const sent = exposeFeatureValuesToHook(config); const request = await this.pluginLifecycle.before("agent.create.before", { @@ -1380,7 +1384,11 @@ export class AgentManager { // COMPAT(featureValues): added in v0.15.0, remove after 2027-05-01. A plugin written // against the previous SDK edits `config.featureValues`. config = { - ...foldHookFeatureValues({ returned: request.config, sent }), + ...foldHookFeatureValues({ + returned: request.config, + sent, + convert: (values) => this.convertLegacyOptions(request.config.provider, values), + }), internal: config.internal, }; options = { ...options, env: request.env }; @@ -2290,8 +2298,9 @@ export class AgentManager { * one step once the provider accepts, over the options as they are at that moment, so concurrent * writes to one agent resolve by arrival order. */ - async setAgentOptions(agentId: string, patch: ProviderOptionsPatch): Promise { + async setAgentOptions(agentId: string, requested: ProviderOptionsPatch): Promise { const agent = this.requireAgent(agentId); + const patch = this.convertLegacyOptions(agent.provider, requested); let ended: ProviderOptionsPatch = {}; let sent: ProviderOptionsPatch = {}; if (Object.keys(patch).length > 0) { @@ -2318,25 +2327,6 @@ export class AgentManager { this.emitState(agent); } - /** - * COMPAT(featureValues): added in v0.15.0, remove after 2027-05-01. An older client or the MCP - * `settings.features` sends composer values as `featureValues`; this is where they become an - * options patch for the agent's provider. - */ - async setAgentFeatureValues(input: { - agentId: string; - values: Record; - /** Options sent with the values; they win over a value of the same name. */ - options?: Record; - }): Promise { - const { agentId, values, options } = input; - const provider = this.requireAgent(agentId).provider; - await this.setAgentOptions(agentId, { - ...convertLegacyOptions({ provider, options: values }), - ...options, - }); - } - /** The config as the opened session left it: its own difference from the options it was given. */ private withOpenedOptions(input: { config: AgentSessionConfig; @@ -5699,15 +5689,46 @@ export class AgentManager { } } + /** The adapter a provider id runs on: a custom provider is named by the one it extends. */ + private adapterOf(provider: AgentProvider): AgentProvider { + const seen = new Set(); + let current = provider; + for (;;) { + seen.add(current); + const base = this.providerDefinitions.get(current)?.derivedFromProviderId; + if (!base || seen.has(base)) return current; + current = base; + } + } + + /** + * Options older data carries are converted for the adapter the provider runs on, before + * defaults merge and before anything is stored; nothing past the manager sees the old keys. + */ + private convertLegacyOptions( + provider: AgentProvider, + options: T, + ): T { + return convertLegacyOptions({ provider: this.adapterOf(provider), options }) as T; + } + + private withConvertedOptions(config: AgentSessionConfig): AgentSessionConfig { + const providerOptions = this.convertLegacyOptions(config.provider, config.providerOptions); + return providerOptions === config.providerOptions ? config : { ...config, providerOptions }; + } + private async prepareSessionConfig( config: AgentSessionConfig, agentId: string, options: { env?: Record; purpose?: AgentResumePurpose } = {}, ): Promise { - const storedConfig = await this.normalizeConfig(stripInternalPaseoMcpServer(config), { - env: options.env, - purpose: options.purpose, - }); + const storedConfig = await this.normalizeConfig( + this.withConvertedOptions(stripInternalPaseoMcpServer(config)), + { + env: options.env, + purpose: options.purpose, + }, + ); const paseoToolPolicy = this.paseoToolsEnabled ? this.resolvePaseoToolPolicy(storedConfig.provider) : { enabled: false }; diff --git a/packages/server/src/server/agent/agent-storage.test.ts b/packages/server/src/server/agent/agent-storage.test.ts index 9b24686f95..9c8296d0f6 100644 --- a/packages/server/src/server/agent/agent-storage.test.ts +++ b/packages/server/src/server/agent/agent-storage.test.ts @@ -218,20 +218,6 @@ describe("AgentStorage", () => { }); }); - test("a Codex record stored with fast_mode loads as service_tier", async () => { - await storage.applySnapshot( - createManagedAgent({ id: "agent-codex-legacy", provider: "codex" }), - ); - const file = findRecordFile(storagePath, "agent-codex-legacy"); - const stored = JSON.parse(await fs.readFile(file, "utf8")); - stored.config.providerOptions = { fast_mode: true, plan_mode: false }; - await fs.writeFile(file, JSON.stringify(stored)); - - const persisted = await new AgentStorage(storagePath, logger).get("agent-codex-legacy"); - - expect(persisted?.config?.providerOptions).toEqual({ service_tier: "fast", plan_mode: false }); - }); - test("a record stored with featureValues loads them in providerOptions", async () => { await storage.applySnapshot(createManagedAgent({ id: "agent-legacy" })); const file = findRecordFile(storagePath, "agent-legacy"); diff --git a/packages/server/src/server/agent/agent-storage.ts b/packages/server/src/server/agent/agent-storage.ts index 159be716c9..07dd93bfa3 100644 --- a/packages/server/src/server/agent/agent-storage.ts +++ b/packages/server/src/server/agent/agent-storage.ts @@ -107,7 +107,7 @@ function foldLegacyRecordFields(record: RawStoredAgentRecord): StoredAgentRecord const { features, config, controls, ...rest } = record; return { ...rest, - ...(config ? { config: foldFeatureValues({ config, provider: record.provider }) } : { config }), + ...(config ? { config: foldFeatureValues(config) } : { config }), ...((controls ?? features) ? { controls: controls ?? controlsFromFeatures(features) } : {}), }; } diff --git a/packages/server/src/server/agent/create-agent/create.ts b/packages/server/src/server/agent/create-agent/create.ts index 0db88fa7f8..3e47eb0978 100644 --- a/packages/server/src/server/agent/create-agent/create.ts +++ b/packages/server/src/server/agent/create-agent/create.ts @@ -221,10 +221,7 @@ async function resolveSessionCreateAgent( input: CreateAgentFromSessionInput, ): Promise { const trimmedPrompt = input.initialPrompt?.trim(); - const builtSessionConfig = foldFeatureValues({ - config: input.config, - provider: input.config.provider, - }); + const builtSessionConfig = foldFeatureValues(input.config); const resolvedCreateConfig = await dependencies.providerSnapshotManager.resolveCreateConfig({ cwd: builtSessionConfig.cwd, provider: builtSessionConfig.provider, diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts index de878bcde0..28b1914bf3 100644 --- a/packages/server/src/server/agent/mcp-server.test.ts +++ b/packages/server/src/server/agent/mcp-server.test.ts @@ -258,7 +258,6 @@ function buildAgentManagerSpies() { setAgentModel: vi.fn().mockResolvedValue(undefined), setAgentThinkingOption: vi.fn().mockResolvedValue(undefined), setAgentOptions: vi.fn().mockResolvedValue(undefined), - setAgentFeatureValues: vi.fn().mockResolvedValue(undefined), setLabels: vi.fn().mockResolvedValue(undefined), setTitle: vi.fn().mockResolvedValue(undefined), updateAgentMetadata: vi.fn().mockResolvedValue(undefined), @@ -1835,7 +1834,7 @@ describe("create_agent MCP tool", () => { expect.objectContaining({ provider: "codex", model: "gpt-5.4", - providerOptions: { service_tier: "fast" }, + providerOptions: { fast_mode: true }, }), undefined, { workspaceId: "workspace-created" }, @@ -3073,7 +3072,7 @@ describe("create_agent MCP tool", () => { expect.objectContaining({ provider: "codex", model: "gpt-5.4", - providerOptions: { service_tier: "fast" }, + providerOptions: { fast_mode: true }, }), undefined, { @@ -4718,10 +4717,7 @@ describe("update_agent MCP tool", () => { expect(spies.agentManager.setAgentMode).toHaveBeenCalledWith("agent-1", "full-access"); expect(spies.agentManager.setAgentModel).toHaveBeenCalledWith("agent-1", "gpt-5.4"); expect(spies.agentManager.setAgentThinkingOption).toHaveBeenCalledWith("agent-1", "high"); - expect(spies.agentManager.setAgentFeatureValues).toHaveBeenCalledWith({ - agentId: "agent-1", - values: { fast_mode: true }, - }); + expect(spies.agentManager.setAgentOptions).toHaveBeenCalledWith("agent-1", { fast_mode: true }); expect(spies.agentManager.updateAgentMetadata).toHaveBeenCalledWith("agent-1", { title: "Updated agent", labels: { role: "worker" }, @@ -4746,12 +4742,12 @@ describe("update_agent MCP tool", () => { expect(spies.agentManager.setAgentMode).not.toHaveBeenCalled(); expect(spies.agentManager.setAgentModel).not.toHaveBeenCalled(); expect(spies.agentManager.setAgentThinkingOption).not.toHaveBeenCalled(); - expect(spies.agentManager.setAgentFeatureValues).not.toHaveBeenCalled(); + expect(spies.agentManager.setAgentOptions).not.toHaveBeenCalled(); }); it("does not update metadata when runtime settings fail", async () => { const { agentManager, agentStorage, spies } = createTestDeps(); - spies.agentManager.setAgentFeatureValues.mockRejectedValue(new Error("unsupported feature")); + spies.agentManager.setAgentOptions.mockRejectedValue(new Error("unsupported feature")); const server = await createAgentMcpServer({ agentManager, agentStorage, @@ -5994,7 +5990,7 @@ describe("provider MCP tools", () => { modeId: "full-access", model: "gpt-5.4", thinkingOptionId: "high", - providerOptions: { service_tier: "fast" }, + providerOptions: { fast_mode: true }, }); expect(response.structuredContent).toEqual({ provider: "codex", diff --git a/packages/server/src/server/agent/option-controls.test.ts b/packages/server/src/server/agent/option-controls.test.ts index c9d7a88bbc..8ea79912e8 100644 --- a/packages/server/src/server/agent/option-controls.test.ts +++ b/packages/server/src/server/agent/option-controls.test.ts @@ -1,9 +1,5 @@ import { describe, expect, test } from "vitest"; -import { - convertLegacyOptions, - foldFeatureValues, - foldHookFeatureValues, -} from "./option-controls.js"; +import { convertLegacyOptions, foldHookFeatureValues } from "./option-controls.js"; describe("convertLegacyOptions", () => { test.each([ @@ -31,34 +27,27 @@ describe("convertLegacyOptions", () => { }); }); -describe("legacy Codex Fast at the other entry points", () => { - test("a stored record's fast_mode folds into service_tier", () => { - expect( - foldFeatureValues({ - config: { featureValues: { fast_mode: true }, providerOptions: { plan_mode: false } }, - provider: "codex", - }).providerOptions, - ).toEqual({ plan_mode: false, service_tier: "fast" }); - }); +describe("foldHookFeatureValues", () => { + const convert = (options: Record) => + convertLegacyOptions({ provider: "codex", options }); - test("a plugin hook that edits featureValues.fast_mode sets service_tier", () => { - const sent = { provider: "codex", providerOptions: {}, featureValues: {} }; - const returned = { ...sent, featureValues: { fast_mode: true } }; + test("a hook that edits featureValues.fast_mode sets service_tier", () => { + const sent = { providerOptions: {}, featureValues: {} }; + const returned = { providerOptions: {}, featureValues: { fast_mode: true } }; - expect(foldHookFeatureValues({ returned, sent }).providerOptions).toEqual({ + expect(foldHookFeatureValues({ returned, sent, convert }).providerOptions).toEqual({ service_tier: "fast", }); }); test("a hook that replaces the speed it was sent with fast_mode selects Fast", () => { const sent = { - provider: "codex", providerOptions: { service_tier: "default" }, featureValues: { service_tier: "default" }, }; const returned = { ...sent, featureValues: { fast_mode: true } }; - expect(foldHookFeatureValues({ returned, sent }).providerOptions).toEqual({ + expect(foldHookFeatureValues({ returned, sent, convert }).providerOptions).toEqual({ service_tier: "fast", }); }); diff --git a/packages/server/src/server/agent/option-controls.ts b/packages/server/src/server/agent/option-controls.ts index 2bbb1a1e6a..7d9e81d418 100644 --- a/packages/server/src/server/agent/option-controls.ts +++ b/packages/server/src/server/agent/option-controls.ts @@ -73,14 +73,10 @@ export function foldFeatureValues< providerOptions?: ProviderOptions | null; featureValues?: Record | null; }, ->(input: { config: T; provider: string | undefined }): Omit { - const { featureValues, ...rest } = input.config; - const merged = - featureValues && Object.keys(featureValues).length > 0 - ? { ...featureValues, ...rest.providerOptions } - : (rest.providerOptions ?? undefined); - const providerOptions = convertLegacyOptions({ provider: input.provider, options: merged }); - return providerOptions === rest.providerOptions ? rest : { ...rest, providerOptions }; +>(config: T): Omit { + const { featureValues, ...rest } = config; + if (!featureValues || Object.keys(featureValues).length === 0) return rest; + return { ...rest, providerOptions: { ...featureValues, ...rest.providerOptions } }; } /** @@ -99,21 +95,25 @@ export function exposeFeatureValuesToHook< export function foldHookFeatureValues< T extends { - provider?: string; providerOptions?: ProviderOptions | null; featureValues?: Record; }, ->(input: { returned: T; sent: T }): Omit { - const { returned, sent } = input; +>(input: { + returned: T; + sent: T; + /** Brings what the hook returned to the form of the copy it was sent. */ + convert: (options: Record) => Record; +}): Omit { + const { returned, sent, convert } = input; const { featureValues, ...rest } = returned; const before = sent.featureValues; if (!before) { // No copy was sent, so everything the hook put in `featureValues` is new. - return foldFeatureValues({ config: returned, provider: returned.provider }); + return foldFeatureValues(returned); } - // The copy the hook was sent is canonical; what it returns may name the old key, so both - // sides are compared in the same form and a replaced speed is not read as a deletion. - const after = convertLegacyOptions({ provider: returned.provider, options: featureValues ?? {} }); + // A hook may return an older key where it was sent the current one; comparing in the same + // form keeps a replaced value from reading as a deletion. + const after = convert(featureValues ?? {}); const patch: ProviderOptionsPatch = {}; for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) { if (isDeepStrictEqual(after[key], before[key])) continue; @@ -122,10 +122,7 @@ export function foldHookFeatureValues< return { ...rest, providerOptions: applyProviderOptionsPatch({ - options: convertLegacyOptions({ - provider: returned.provider, - options: rest.providerOptions ?? undefined, - }), + options: rest.providerOptions ?? undefined, patch, }), }; @@ -155,8 +152,7 @@ export function foldProfileFeatureValues(input: { source: "stored" | "client write"; }): Omit { const { profile, stored, source } = input; - const { featureValues: legacyValues, ...rest } = profile; - const featureValues = convertLegacyOptions({ provider: profile.provider, options: legacyValues }); + const { featureValues, ...rest } = profile; if (source === "stored") { return featureValues ? { ...rest, options: { ...featureValues, ...rest.options } } : rest; } diff --git a/packages/server/src/server/agent/provider-registry.test.ts b/packages/server/src/server/agent/provider-registry.test.ts index 2f5b3b0e61..04c7effc78 100644 --- a/packages/server/src/server/agent/provider-registry.test.ts +++ b/packages/server/src/server/agent/provider-registry.test.ts @@ -509,7 +509,6 @@ import { createAllClients, } from "./provider-registry.js"; import { FakeOmp } from "./providers/omp/test-utils/fake-omp.js"; -import { foldFeatureValues } from "./option-controls.js"; const logger = createTestLogger(); @@ -797,13 +796,12 @@ test("a saved legacy Codex Fast preference wins over a configured service_tier d providerOverrides: { codex: { options: { service_tier: "default", plan_mode: false } } }, }); const client = registry.codex.createClient(logger); - // A pre-v0.10 record is converted as it is read, before the defaults merge under it. - const { providerOptions: saved } = foldFeatureValues({ - config: { providerOptions: { fast_mode: true } }, - provider: "codex", - }); - expect(client.effectiveOptions?.(saved)).toEqual({ service_tier: "fast", plan_mode: false }); + // The manager hands the registry a pre-v0.10 record's `fast_mode` already converted. + expect(client.effectiveOptions?.({ service_tier: "fast" })).toEqual({ + service_tier: "fast", + plan_mode: false, + }); expect(client.effectiveOptions?.({})).toEqual({ service_tier: "default", plan_mode: false }); }); diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.features.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.features.test.ts index 1857fa7ba5..35e4c2113d 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.features.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.features.test.ts @@ -7,6 +7,7 @@ import { describe, expect, test, vi } from "vitest"; import { AgentManager } from "../agent-manager.js"; import { toAgentPayload } from "../agent-projections.js"; import { AgentStorage } from "../agent-storage.js"; +import { convertLegacyOptions } from "../option-controls.js"; import type { AgentClient, AgentSession, @@ -654,8 +655,16 @@ describe("Codex app-server provider controls", () => { await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ serviceTier: "default" }); }); - describe("Fast chosen as `fast`", () => { - function createLegacyFastHarness(configDefaults: Record = {}) { + describe("legacy Fast (fast_mode) saved or requested before service_tier", () => { + function createLegacyFastHarness( + rawDefaults: Record = {}, + providerId = CODEX_PROVIDER, + ) { + // The provider registry converts configured defaults before it merges them. + const configDefaults = convertLegacyOptions({ + provider: CODEX_PROVIDER, + options: rawDefaults, + }); const appServer = createFakeCodexAppServer({ "collaborationMode/list": () => ({ data: TEST_COLLABORATION_MODES }), "model/list": () => ({ data: TEST_SPEED_MODELS }), @@ -680,7 +689,7 @@ describe("Codex app-server provider controls", () => { return session; }; const client: AgentClient = { - provider: CODEX_PROVIDER, + provider: providerId, capabilities: { supportsStreaming: true, supportsSessionPersistence: true, @@ -702,7 +711,16 @@ describe("Codex app-server provider controls", () => { }; const workdir = mkdtempSync(join(tmpdir(), "codex-legacy-fast-")); const storage = new AgentStorage(join(workdir, "agents"), logger); - const manager = new AgentManager({ clients: { codex: client }, registry: storage, logger }); + const manager = new AgentManager({ + clients: { [providerId]: client }, + // A custom provider that extends Codex runs on the Codex adapter. + providerDefinitions: + providerId === CODEX_PROVIDER + ? {} + : { [providerId]: { enabled: true, derivedFromProviderId: CODEX_PROVIDER } }, + registry: storage, + logger, + }); const turnStarts = () => appServer .requests() @@ -712,7 +730,7 @@ describe("Codex app-server provider controls", () => { async function create(providerOptions?: Record) { const agent = await manager.createAgent( { - provider: CODEX_PROVIDER, + provider: providerId, cwd: workdir, model: "gpt-5.4", ...(providerOptions ? { providerOptions } : {}), @@ -724,7 +742,7 @@ describe("Codex app-server provider controls", () => { } async function resume(providerOptions: Record) { const agent = await manager.resumeAgentFromPersistence({ - provider: CODEX_PROVIDER, + provider: providerId, sessionId: "thread-1", metadata: { cwd: workdir, model: "gpt-5.4", providerOptions }, }); @@ -762,9 +780,9 @@ describe("Codex app-server provider controls", () => { const FAST = { selected: "priority", feature: "priority" }; - test("opens as the model's Fast tier and is stored as that tier", async () => { + test("a saved fast_mode opens as the model's Fast tier and is stored as that tier", async () => { const { manager, create, shownSpeed, runTurn } = createLegacyFastHarness(); - const agentId = await create({ service_tier: "fast" }); + const agentId = await create({ fast_mode: true }); await manager.flush(); expect(shownSpeed(agentId)).toMatchObject({ @@ -777,7 +795,7 @@ describe("Codex app-server provider controls", () => { test("is shown as Fast after a stored record is resumed", async () => { const { manager, resume, shownSpeed, runTurn } = createLegacyFastHarness(); - const agentId = await resume({ service_tier: "fast" }); + const agentId = await resume({ fast_mode: true }); await manager.flush(); expect(shownSpeed(agentId)).toMatchObject({ own: { service_tier: "priority" }, ...FAST }); @@ -785,19 +803,50 @@ describe("Codex app-server provider controls", () => { }); test("from config.json defaults runs Fast and is shown as Fast", async () => { - const { create, shownSpeed, runTurn } = createLegacyFastHarness({ service_tier: "fast" }); + const { create, shownSpeed, runTurn } = createLegacyFastHarness({ fast_mode: true }); const agentId = await create(); expect(shownSpeed(agentId)).toMatchObject(FAST); await runTurn(agentId, "priority"); }); + test("a saved fast_mode beats a configured service_tier default", async () => { + const { manager, resume, shownSpeed, runTurn } = createLegacyFastHarness({ + service_tier: "default", + }); + const agentId = await resume({ fast_mode: true }); + await manager.flush(); + + expect(shownSpeed(agentId)).toMatchObject({ own: { service_tier: "priority" }, ...FAST }); + await runTurn(agentId, "priority"); + }); + + test("a provider extending Codex opens a saved fast_mode as Fast", async () => { + const { manager, resume, shownSpeed, runTurn } = createLegacyFastHarness({}, "my-codex"); + const agentId = await resume({ fast_mode: true }); + await manager.flush(); + + expect(shownSpeed(agentId)).toMatchObject({ own: { service_tier: "priority" }, ...FAST }); + await runTurn(agentId, "priority"); + }); + + test("a provider extending Codex creates with a requested fast_mode and applies a later one", async () => { + const { manager, create, shownSpeed, runTurn } = createLegacyFastHarness({}, "my-codex"); + const agentId = await create({ fast_mode: false }); + expect(shownSpeed(agentId)).toMatchObject({ own: { service_tier: "default" } }); + + await manager.setAgentOptions(agentId, { fast_mode: true }); + + expect(shownSpeed(agentId)).toMatchObject({ own: { service_tier: "priority" }, ...FAST }); + await runTurn(agentId, "priority"); + }); + test("an older client's fast_mode runs Fast at once, and a later service_tier replaces it", async () => { const { manager, create, shownSpeed, runTurn } = createLegacyFastHarness(); const agentId = await create(); expect(shownSpeed(agentId).selected).toBe("default"); - await manager.setAgentFeatureValues({ agentId, values: { fast_mode: true } }); + await manager.setAgentOptions(agentId, { fast_mode: true }); expect(shownSpeed(agentId)).toMatchObject({ own: { service_tier: "priority" }, ...FAST }); await runTurn(agentId, "priority"); @@ -818,24 +867,7 @@ describe("Codex app-server provider controls", () => { const { manager, create, shownSpeed, runTurn } = createLegacyFastHarness(); const agentId = await create({ service_tier: "priority" }); - await manager.setAgentFeatureValues({ - agentId, - values: { service_tier: "default", fast_mode: true }, - }); - - expect(shownSpeed(agentId)).toMatchObject({ own: { service_tier: "default" } }); - await runTurn(agentId, "default"); - }); - - test("options sent with the older values replace them before anything is applied", async () => { - const { manager, create, shownSpeed, runTurn } = createLegacyFastHarness(); - const agentId = await create({ service_tier: "priority" }); - - await manager.setAgentFeatureValues({ - agentId, - values: { service_tier: "retired-tier" }, - options: { service_tier: "default" }, - }); + await manager.setAgentOptions(agentId, { service_tier: "default", fast_mode: true }); expect(shownSpeed(agentId)).toMatchObject({ own: { service_tier: "default" } }); await runTurn(agentId, "default"); @@ -845,7 +877,7 @@ describe("Codex app-server provider controls", () => { const { manager, create, shownSpeed, runTurn } = createLegacyFastHarness(); const agentId = await create({ service_tier: "priority" }); - await manager.setAgentFeatureValues({ agentId, values: { fast_mode: false } }); + await manager.setAgentOptions(agentId, { fast_mode: false }); expect(shownSpeed(agentId)).toMatchObject({ own: { service_tier: "default" } }); await runTurn(agentId, "default"); diff --git a/packages/server/src/server/agent/tools/paseo-tools.ts b/packages/server/src/server/agent/tools/paseo-tools.ts index f83d982ac8..8a0409b188 100644 --- a/packages/server/src/server/agent/tools/paseo-tools.ts +++ b/packages/server/src/server/agent/tools/paseo-tools.ts @@ -18,7 +18,7 @@ import type { AgentMode, AgentProvider, AgentSessionConfig } from "../agent-sdk- import type { AgentManager } from "../agent-manager.js"; import { ActiveTurnBehaviorSchema, AgentProfileSchema } from "@getpaseo/protocol/messages"; import type { DaemonConfigStore } from "../../daemon-config-store.js"; -import { convertLegacyOptions, featuresFromControls } from "../option-controls.js"; +import { featuresFromControls } from "../option-controls.js"; import { AgentFeatureSchema, AgentPermissionRequestPayloadSchema, @@ -1636,11 +1636,7 @@ function createToolCatalog( cwd: resolvedArgs.cwd, workspaceId: resolvedArgs.workspaceId, thinking: parsedArgs.settings?.thinkingOptionId, - // COMPAT(featureValues): added in v0.15.0, remove after 2027-05-01. `settings.features` is an options map. - providerOptions: convertLegacyOptions({ - provider: selectedProvider, - options: parsedArgs.settings?.features, - }), + providerOptions: parsedArgs.settings?.features, labels: { ...parsedArgs.labels, ...(callerAgentId && !resolvedArgs.detached && options.serverId @@ -2292,7 +2288,7 @@ function createToolCatalog( await agentManager.setAgentThinkingOption(agentId, settings.thinkingOptionId); } if (settings?.features) { - await agentManager.setAgentFeatureValues({ agentId, values: settings.features }); + await agentManager.setAgentOptions(agentId, settings.features); } await updateAgentCommand({ agentManager }, { agentId, name, labels }); @@ -3130,14 +3126,7 @@ function createToolCatalog( ...(settings?.modeId ? { modeId: settings.modeId } : {}), ...(selectedModel ? { model: selectedModel } : {}), ...(settings?.thinkingOptionId ? { thinkingOptionId: settings.thinkingOptionId } : {}), - ...(settings?.features - ? { - providerOptions: convertLegacyOptions({ - provider: providerId, - options: settings.features, - }), - } - : {}), + ...(settings?.features ? { providerOptions: settings.features } : {}), }); return { content: [], diff --git a/packages/server/src/server/daemon-config-store.test.ts b/packages/server/src/server/daemon-config-store.test.ts index 7a7f567850..02e64236fd 100644 --- a/packages/server/src/server/daemon-config-store.test.ts +++ b/packages/server/src/server/daemon-config-store.test.ts @@ -268,7 +268,7 @@ describe("DaemonConfigStore", () => { id: "p", name: "P", provider: "codex", - options: { sandbox_mode: "read-only", plan_mode: false }, + options: { sandbox_mode: "read-only", fast_mode: false }, }, ], }); @@ -279,7 +279,7 @@ describe("DaemonConfigStore", () => { await store.patch({ agentProfiles: [ - { id: "p", name: "Renamed", provider: "codex", featureValues: { plan_mode: true } }, + { id: "p", name: "Renamed", provider: "codex", featureValues: { fast_mode: true } }, ], }); @@ -288,7 +288,7 @@ describe("DaemonConfigStore", () => { id: "p", name: "Renamed", provider: "codex", - options: { sandbox_mode: "read-only", plan_mode: true }, + options: { sandbox_mode: "read-only", fast_mode: true }, }, ]); }); @@ -303,7 +303,7 @@ describe("DaemonConfigStore", () => { id: "p", name: "Renamed", provider: "codex", - options: { sandbox_mode: "read-only", plan_mode: false }, + options: { sandbox_mode: "read-only", fast_mode: false }, }, ]); }); @@ -340,7 +340,7 @@ describe("DaemonConfigStore", () => { name: "P", provider: "codex", options: { sandbox_mode: "workspace-write" }, - featureValues: { plan_mode: true }, + featureValues: { fast_mode: true }, }, ], }); @@ -350,26 +350,7 @@ describe("DaemonConfigStore", () => { id: "p", name: "P", provider: "codex", - options: { sandbox_mode: "workspace-write", plan_mode: true }, - }, - ]); - }); - - test("converts a Codex profile's legacy fast_mode to service_tier", async () => { - const store = storeWithProfile(); - - await store.patch({ - agentProfiles: [ - { id: "p", name: "P", provider: "codex", featureValues: { fast_mode: true } }, - ], - }); - - expect(store.get().agentProfiles).toEqual([ - { - id: "p", - name: "P", - provider: "codex", - options: { sandbox_mode: "read-only", plan_mode: false, service_tier: "fast" }, + options: { sandbox_mode: "workspace-write", fast_mode: true }, }, ]); }); @@ -379,7 +360,7 @@ describe("DaemonConfigStore", () => { await store.patch({ agentProfiles: [ - { id: "p", name: "P", provider: "codex", featureValues: { plan_mode: null } }, + { id: "p", name: "P", provider: "codex", featureValues: { fast_mode: null } }, ], }); diff --git a/packages/server/src/server/hub/execution-controller.ts b/packages/server/src/server/hub/execution-controller.ts index 4f0843c8a1..23343b4e61 100644 --- a/packages/server/src/server/hub/execution-controller.ts +++ b/packages/server/src/server/hub/execution-controller.ts @@ -169,8 +169,7 @@ export class HubExecutionController { modeId: message.modeId, thinkingOptionId: message.thinkingOptionId, // COMPAT(featureValues): added in v0.15.0, remove after 2027-05-01. - providerOptions: foldFeatureValues({ config: message, provider: message.provider }) - .providerOptions, + providerOptions: foldFeatureValues(message).providerOptions, toolPolicy: message.toolPolicy, env: message.env, mcpServers: message.mcpServers, diff --git a/packages/server/src/server/schedule/service.ts b/packages/server/src/server/schedule/service.ts index c167a4839a..c8c9db2263 100644 --- a/packages/server/src/server/schedule/service.ts +++ b/packages/server/src/server/schedule/service.ts @@ -1099,7 +1099,7 @@ function buildScheduleAgentConfig( thinkingOptionId: config.thinkingOptionId, title: config.title, // COMPAT(featureValues): added in v0.15.0, remove after 2027-05-01. - providerOptions: foldFeatureValues({ config, provider: config.provider }).providerOptions, + providerOptions: foldFeatureValues(config).providerOptions, systemPrompt: config.systemPrompt, mcpServers: config.mcpServers as AgentSessionConfig["mcpServers"], }; diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 927db78077..7c4f3405a2 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -1067,7 +1067,6 @@ export class Session { (await setAgentModeCommand({ agentManager }, { agentId, modeId })).notice, setModel: (agentId, modelId) => agentManager.setAgentModel(agentId, modelId), setOptions: (agentId, patch) => agentManager.setAgentOptions(agentId, patch), - setFeatureValues: (input) => agentManager.setAgentFeatureValues(input), setThinking: (agentId, thinkingOptionId) => agentManager.setAgentThinkingOption(agentId, thinkingOptionId), }, diff --git a/packages/server/src/server/session/agent-config/agent-config-session.test.ts b/packages/server/src/server/session/agent-config/agent-config-session.test.ts index a9869f0118..d6589683a7 100644 --- a/packages/server/src/server/session/agent-config/agent-config-session.test.ts +++ b/packages/server/src/server/session/agent-config/agent-config-session.test.ts @@ -13,11 +13,6 @@ class FakeAgentConfigOperations implements AgentConfigOperations { readonly modeCalls: Array<{ agentId: string; modeId: string }> = []; readonly modelCalls: Array<{ agentId: string; modelId: string | null }> = []; readonly optionsCalls: Array<{ agentId: string; patch: Record }> = []; - readonly featureValueCalls: Array<{ - agentId: string; - values: Record; - options?: Record; - }> = []; readonly thinkingCalls: Array<{ agentId: string; thinkingOptionId: string | null }> = []; /** Cross-operation ordering, which the per-operation arrays above cannot show. */ readonly callLog: string[] = []; @@ -50,16 +45,6 @@ class FakeAgentConfigOperations implements AgentConfigOperations { if (this.failWith) throw this.failWith; } - async setFeatureValues(input: { - agentId: string; - values: Record; - options?: Record; - }): Promise { - this.featureValueCalls.push(input); - this.callLog.push("featureValues"); - if (this.failWith) throw this.failWith; - } - async setThinking( agentId: string, thinkingOptionId: string | null, @@ -232,9 +217,7 @@ describe("AgentConfigSession", () => { requestId: "req-1", }); - expect(operations.featureValueCalls).toEqual([ - { agentId: "agent-1", values: { web_search: true } }, - ]); + expect(operations.optionsCalls).toEqual([{ agentId: "agent-1", patch: { web_search: true } }]); expect(emitted).toEqual([ { type: "set_agent_feature_response", @@ -374,7 +357,7 @@ describe("AgentConfigSession", () => { ]); }); - test("apply: legacy featureValues and options go in one call, options winning", async () => { + test("apply: folds legacy featureValues into the options patch, options winning", async () => { const { subsystem, operations } = makeSubsystem(); await subsystem.handleAgentConfigApplyRequest({ @@ -387,13 +370,8 @@ describe("AgentConfigSession", () => { requestId: "req-1", }); - expect(operations.callLog).toEqual(["featureValues"]); - expect(operations.featureValueCalls).toEqual([ - { - agentId: "agent-1", - values: { fast_mode: true, service_tier: "fast" }, - options: { fast_mode: false }, - }, + expect(operations.optionsCalls).toEqual([ + { agentId: "agent-1", patch: { fast_mode: false, service_tier: "fast" } }, ]); }); diff --git a/packages/server/src/server/session/agent-config/agent-config-session.ts b/packages/server/src/server/session/agent-config/agent-config-session.ts index db471c111f..b7ba697670 100644 --- a/packages/server/src/server/session/agent-config/agent-config-session.ts +++ b/packages/server/src/server/session/agent-config/agent-config-session.ts @@ -30,12 +30,6 @@ export interface AgentConfigOperations { setMode(agentId: string, modeId: string): Promise; setModel(agentId: string, modelId: string | null): Promise; setOptions(agentId: string, patch: ProviderOptionsPatch): Promise; - /** COMPAT(featureValues): added in v0.15.0, remove after 2027-05-01. Older clients' values. */ - setFeatureValues(input: { - agentId: string; - values: Record; - options?: ProviderOptionsPatch; - }): Promise; setThinking( agentId: string, thinkingOptionId: string | null, @@ -121,7 +115,7 @@ export class AgentConfigSession { failureText: "Failed to set agent feature", // COMPAT(featureValues): added in v0.15.0, remove after 2027-05-01. A feature is a one-key options patch. run: async () => { - await this.operations.setFeatureValues({ agentId, values: { [featureId]: value } }); + await this.operations.setOptions(agentId, { [featureId]: value }); return undefined; }, emitResponse: (payload) => this.host.emit({ type: "set_agent_feature_response", payload }), @@ -198,16 +192,9 @@ export class AgentConfigSession { notice ??= thinkingNotice; } // COMPAT(featureValues): added in v0.15.0, remove after 2027-05-01. `featureValues` is an options patch. - const hasValues = config.featureValues && Object.keys(config.featureValues).length > 0; - const hasOptions = config.options && Object.keys(config.options).length > 0; - if (hasValues) { - await this.operations.setFeatureValues({ - agentId, - values: config.featureValues ?? {}, - options: config.options, - }); - } else if (hasOptions) { - await this.operations.setOptions(agentId, config.options ?? {}); + const optionsPatch = { ...config.featureValues, ...config.options }; + if (Object.keys(optionsPatch).length > 0) { + await this.operations.setOptions(agentId, optionsPatch); } return notice; diff --git a/packages/server/src/server/session/provider/provider-catalog-session.ts b/packages/server/src/server/session/provider/provider-catalog-session.ts index ee4f46b3c6..041d7182fc 100644 --- a/packages/server/src/server/session/provider/provider-catalog-session.ts +++ b/packages/server/src/server/session/provider/provider-catalog-session.ts @@ -347,10 +347,7 @@ export class ProviderCatalogSession { providerOptions?: ProviderOptions; }): AgentSessionConfig { // COMPAT(featureValues): added in v0.15.0, remove after 2027-05-01. - const { providerOptions } = foldFeatureValues({ - config: draftConfig, - provider: draftConfig.provider, - }); + const { providerOptions } = foldFeatureValues(draftConfig); return { provider: draftConfig.provider, cwd: expandTilde(draftConfig.cwd),