mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-01 22:23:07 +00:00
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.
This commit is contained in:
+12
-7
@@ -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.
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<ManagedAgent> {
|
||||
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<void> {
|
||||
async setAgentOptions(agentId: string, requested: ProviderOptionsPatch): Promise<void> {
|
||||
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<string, unknown>;
|
||||
/** Options sent with the values; they win over a value of the same name. */
|
||||
options?: Record<string, unknown>;
|
||||
}): Promise<void> {
|
||||
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<AgentProvider>();
|
||||
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<T extends ProviderOptions | undefined>(
|
||||
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<string, string>; purpose?: AgentResumePurpose } = {},
|
||||
): Promise<PreparedSessionConfig> {
|
||||
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 };
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,10 +221,7 @@ async function resolveSessionCreateAgent(
|
||||
input: CreateAgentFromSessionInput,
|
||||
): Promise<ResolvedCreateAgent> {
|
||||
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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string, unknown>) =>
|
||||
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",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,14 +73,10 @@ export function foldFeatureValues<
|
||||
providerOptions?: ProviderOptions | null;
|
||||
featureValues?: Record<string, unknown> | null;
|
||||
},
|
||||
>(input: { config: T; provider: string | undefined }): Omit<T, "featureValues"> {
|
||||
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<T, "featureValues"> {
|
||||
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<string, unknown>;
|
||||
},
|
||||
>(input: { returned: T; sent: T }): Omit<T, "featureValues"> {
|
||||
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<string, unknown>) => Record<string, unknown>;
|
||||
}): Omit<T, "featureValues"> {
|
||||
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<T extends ProfileOptionFields>(input: {
|
||||
source: "stored" | "client write";
|
||||
}): Omit<T, "featureValues"> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
|
||||
+62
-30
@@ -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<string, unknown> = {}) {
|
||||
describe("legacy Fast (fast_mode) saved or requested before service_tier", () => {
|
||||
function createLegacyFastHarness(
|
||||
rawDefaults: Record<string, unknown> = {},
|
||||
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<string, unknown>) {
|
||||
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<string, unknown>) {
|
||||
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");
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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 } },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"],
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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<string, unknown> }> = [];
|
||||
readonly featureValueCalls: Array<{
|
||||
agentId: string;
|
||||
values: Record<string, unknown>;
|
||||
options?: Record<string, unknown>;
|
||||
}> = [];
|
||||
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<string, unknown>;
|
||||
options?: Record<string, unknown>;
|
||||
}): Promise<void> {
|
||||
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" } },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -30,12 +30,6 @@ export interface AgentConfigOperations {
|
||||
setMode(agentId: string, modeId: string): Promise<AgentProviderNotice | null>;
|
||||
setModel(agentId: string, modelId: string | null): Promise<void>;
|
||||
setOptions(agentId: string, patch: ProviderOptionsPatch): Promise<void>;
|
||||
/** COMPAT(featureValues): added in v0.15.0, remove after 2027-05-01. Older clients' values. */
|
||||
setFeatureValues(input: {
|
||||
agentId: string;
|
||||
values: Record<string, unknown>;
|
||||
options?: ProviderOptionsPatch;
|
||||
}): Promise<void>;
|
||||
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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user