diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 03f6de1be8..b43b3cc259 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -2427,10 +2427,8 @@ export class AgentManager { return input.client.effectiveOptions?.(input.own) ?? input.own ?? {}; } - private effectiveOptionsFor( - provider: AgentProvider, - own: ProviderOptions | undefined, - ): ProviderOptions { + /** What an agent of this provider runs with, given its own options: the defaults under them. */ + effectiveOptionsFor(provider: AgentProvider, own: ProviderOptions | undefined): ProviderOptions { const client = this.clients.get(provider); return client ? this.launchOptionsFor({ client, own }) : (own ?? {}); } diff --git a/packages/server/src/server/agent/agent-projections.test.ts b/packages/server/src/server/agent/agent-projections.test.ts index 38ba8bf3d1..7f5e62dc85 100644 --- a/packages/server/src/server/agent/agent-projections.test.ts +++ b/packages/server/src/server/agent/agent-projections.test.ts @@ -17,6 +17,9 @@ import type { AgentSessionConfig, } from "./agent-sdk-types.js"; +/** A provider with no defaults of its own: the effective options are the agent's own. */ +const ownOptionsOnly = (_provider: string, own: Record | undefined) => own ?? {}; + type ManagedAgentOverrides = Omit, "config" | "pendingPermissions"> & { config?: Partial; pendingPermissions?: Map; @@ -381,7 +384,7 @@ describe("toAgentPayload", () => { }), ); - const payload = buildStoredAgentPayload(record, ["codex"]); + const payload = buildStoredAgentPayload(record, ["codex"], ownOptionsOnly); expect(record.persistence?.metadata).toEqual({ conversationId: "conversation-stored", @@ -422,7 +425,7 @@ describe("toAgentPayload", () => { }), ); - const payload = buildStoredAgentPayload(record, ["codex"]); + const payload = buildStoredAgentPayload(record, ["codex"], ownOptionsOnly); expect(payload.status).toBe("closed"); expect(payload.controls).toEqual(controls); @@ -441,15 +444,37 @@ describe("toAgentPayload", () => { ]); }); + it("projects a closed agent's options with the provider defaults under its own, like a live one", () => { + const record = toStoredAgentRecord( + createManagedAgent({ + provider: "claude", + lifecycle: "closed", + controls: [{ type: "toggle", key: "fast_mode", label: "Fast" }], + config: { provider: "claude", providerOptions: {} }, + }), + ); + const withConfigDefaults = (_provider: string, own: Record | undefined) => ({ + fast_mode: true, + ...own, + }); + + const payload = buildStoredAgentPayload(record, ["claude"], withConfigDefaults); + + expect(payload.options).toEqual({ fast_mode: true }); + expect(payload.features).toEqual([ + { type: "toggle", id: "fast_mode", label: "Fast", value: true }, + ]); + }); + it("carries a stored lastError onto the payload", () => { const record = toStoredAgentRecord( createManagedAgent({ provider: "codex", config: { provider: "codex" }, lastError: "boom" }), ); - expect(buildStoredAgentPayload(record, ["codex"]).lastError).toBe("boom"); - expect(buildStoredAgentPayload({ ...record, lastError: null }, ["codex"])).not.toHaveProperty( - "lastError", - ); + expect(buildStoredAgentPayload(record, ["codex"], ownOptionsOnly).lastError).toBe("boom"); + expect( + buildStoredAgentPayload({ ...record, lastError: null }, ["codex"], ownOptionsOnly), + ).not.toHaveProperty("lastError"); }); it("omits lastUsage when not available", () => { diff --git a/packages/server/src/server/agent/agent-projections.ts b/packages/server/src/server/agent/agent-projections.ts index 135a9fedf0..1f64b9965c 100644 --- a/packages/server/src/server/agent/agent-projections.ts +++ b/packages/server/src/server/agent/agent-projections.ts @@ -225,17 +225,24 @@ function visibilityFlags(agent: { }; } +/** The options an agent runs with, given the options it owns; see `toAgentPayload`. */ +export type EffectiveOptionsResolver = ( + provider: AgentProvider, + own: ProviderOptions | undefined, +) => ProviderOptions; + function buildStoredOptionControls( record: StoredAgentRecord, + effectiveOptionsFor: EffectiveOptionsResolver, ): Pick { const controls = normalizeControls(record.controls); - const options = record.config?.providerOptions - ? sanitizeEffectiveOptions(record.config.providerOptions) - : undefined; + const options = sanitizeEffectiveOptions( + effectiveOptionsFor(record.provider, record.config?.providerOptions ?? undefined), + ); return { // COMPAT(featureValues): added in v0.15.0, remove after 2027-05-01. features: featuresFromControls({ controls, options }), - ...(options ? { options } : {}), + options, controls, }; } @@ -243,6 +250,7 @@ function buildStoredOptionControls( export function buildStoredAgentPayload( record: StoredAgentRecord, validProviders: Iterable, + effectiveOptionsFor: EffectiveOptionsResolver, ): AgentSnapshotPayload { const defaultCapabilities = { supportsStreaming: false, @@ -261,7 +269,7 @@ export function buildStoredAgentPayload( const lastUserMessageAt = record.lastUserMessageAt ? new Date(record.lastUserMessageAt) : null; const runtimeInfo = buildStoredRuntimeInfo(record); - const optionControls = buildStoredOptionControls(record); + const optionControls = buildStoredOptionControls(record, effectiveOptionsFor); const providerAvailable = isStoredAgentProviderAvailable(record, validProviders); const persistence = projectPersistenceHandleForWire( buildStoredPersistenceHandle(record, validProviders), diff --git a/packages/server/src/server/agent/directory.test.ts b/packages/server/src/server/agent/directory.test.ts index e5767de4f7..647db9aef9 100644 --- a/packages/server/src/server/agent/directory.test.ts +++ b/packages/server/src/server/agent/directory.test.ts @@ -55,7 +55,8 @@ beforeEach(async () => { projectLive: () => { throw new Error("No live agents in this suite"); }, - projectStored: (record) => buildStoredAgentPayload(record, ["codex"]), + projectStored: (record) => + buildStoredAgentPayload(record, ["codex"], (_provider, own) => own ?? {}), isProviderVisible: () => true, isStoredProviderAvailable: () => true, }); diff --git a/packages/server/src/server/agent/finish-subscriptions/finish-subscriptions.test.ts b/packages/server/src/server/agent/finish-subscriptions/finish-subscriptions.test.ts index 323e4be48f..9e18f87948 100644 --- a/packages/server/src/server/agent/finish-subscriptions/finish-subscriptions.test.ts +++ b/packages/server/src/server/agent/finish-subscriptions/finish-subscriptions.test.ts @@ -123,6 +123,7 @@ function outcome(status: "idle" | "running" | "closed" | "error" = "closed"): Wa lastStatus: status, }, ["codex"], + (_provider, own) => own ?? {}, ), lastMessage: "Partial work saved", error: null, diff --git a/packages/server/src/server/agent/finish-subscriptions/internal/observation-stream.test.ts b/packages/server/src/server/agent/finish-subscriptions/internal/observation-stream.test.ts index 1d15a890df..5bc0b3f7d7 100644 --- a/packages/server/src/server/agent/finish-subscriptions/internal/observation-stream.test.ts +++ b/packages/server/src/server/agent/finish-subscriptions/internal/observation-stream.test.ts @@ -43,6 +43,7 @@ class Host implements HostObservation { updatedAt: "2026-01-01", }, ["codex"], + (_provider, own) => own ?? {}, ), event, "Last response", @@ -133,6 +134,7 @@ test("late initial permission snapshot cannot resurrect an already resolved requ updatedAt: "2026-01-01", }, ["codex"], + (_provider, own) => own ?? {}, ); if (requestEvent.type !== "permission_requested") throw new Error("Expected permission"); final.pendingPermissions = [requestEvent.request]; diff --git a/packages/server/src/server/agent/finish-subscriptions/internal/observation.ts b/packages/server/src/server/agent/finish-subscriptions/internal/observation.ts index 7abe483cf4..bfe77db3f2 100644 --- a/packages/server/src/server/agent/finish-subscriptions/internal/observation.ts +++ b/packages/server/src/server/agent/finish-subscriptions/internal/observation.ts @@ -34,7 +34,11 @@ export function createFinishObservation(input: { const stored = await agentStorage.get(agentId); return { status: stored?.lastStatus === "error" ? "error" : "idle", - final: stored ? buildStoredAgentPayload(stored, [stored.provider]) : null, + final: stored + ? buildStoredAgentPayload(stored, [stored.provider], (provider, own) => + agentManager.effectiveOptionsFor(provider, own), + ) + : null, lastMessage: await agentManager.getLastAssistantMessage(agentId), error: stored?.lastError ?? null, }; @@ -93,7 +97,14 @@ export function createFinishObservation(input: { type: "turn_started", provider: current.provider, }); - else publish(stored ? buildStoredAgentPayload(stored, [stored.provider]) : null); + else + publish( + stored + ? buildStoredAgentPayload(stored, [stored.provider], (provider, own) => + agentManager.effectiveOptionsFor(provider, own), + ) + : null, + ); const stop = () => unsubscribe(); signal.addEventListener("abort", stop, { once: true }); return async () => { diff --git a/packages/server/src/server/agent/tools/paseo-tools.ts b/packages/server/src/server/agent/tools/paseo-tools.ts index 1ac0882d82..53c2e90d06 100644 --- a/packages/server/src/server/agent/tools/paseo-tools.ts +++ b/packages/server/src/server/agent/tools/paseo-tools.ts @@ -2133,6 +2133,7 @@ function createToolCatalog( const structuredSnapshot = buildStoredAgentPayload( record, new Set(providerSnapshotManager.listRegisteredProviderIds()), + (provider, own) => agentManager.effectiveOptionsFor(provider, own), ); return { content: [], @@ -2188,6 +2189,7 @@ function createToolCatalog( buildStoredAgentPayload( record, new Set(providerSnapshotManager.listRegisteredProviderIds()), + (provider, own) => agentManager.effectiveOptionsFor(provider, own), ), isProviderVisible: () => true, isStoredProviderAvailable: (record) => diff --git a/packages/server/src/server/hub/daemon-executions.ts b/packages/server/src/server/hub/daemon-executions.ts index c55b6e3258..029e2090c7 100644 --- a/packages/server/src/server/hub/daemon-executions.ts +++ b/packages/server/src/server/hub/daemon-executions.ts @@ -288,7 +288,11 @@ export class DaemonExecutions implements HubExecutionAgents { agent: live ? serializeAgentSnapshot(live) : { - ...buildStoredAgentPayload(record, this.agentManager.getRegisteredProviderIds()), + ...buildStoredAgentPayload( + record, + this.agentManager.getRegisteredProviderIds(), + (provider, own) => this.agentManager.effectiveOptionsFor(provider, own), + ), status: "closed", }, }; diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index b31a55210c..e1b5d61a23 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -2183,7 +2183,9 @@ export class Session { record: StoredAgentRecord, registeredProviderIds = new Set(this.providerSnapshotManager.listRegisteredProviderIds()), ): AgentSnapshotPayload { - return buildStoredAgentPayload(record, registeredProviderIds); + return buildStoredAgentPayload(record, registeredProviderIds, (provider, own) => + this.agentManager.effectiveOptionsFor(provider, own), + ); } private isProviderVisibleToClient(provider: string): boolean { diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts index 6b1db34449..caff1b1b3d 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -2097,7 +2097,8 @@ test("workspace mark unread selects a finished cross-workspace child with scoped projectLive: async () => { throw new Error("No live agents"); }, - projectStored: (record) => buildStoredAgentPayload(record, ["codex"]), + projectStored: (record) => + buildStoredAgentPayload(record, ["codex"], (_provider, own) => own ?? {}), isProviderVisible: () => true, isStoredProviderAvailable: () => true, }); diff --git a/packages/server/src/server/test-utils/session-stubs.ts b/packages/server/src/server/test-utils/session-stubs.ts index a6ceb24ed8..ca702dc36d 100644 --- a/packages/server/src/server/test-utils/session-stubs.ts +++ b/packages/server/src/server/test-utils/session-stubs.ts @@ -37,6 +37,7 @@ export function asAgentManager(stub: { return createStub({ listProviderSubagentActivity: () => [], getRetiredInternalAgent: () => null, + effectiveOptionsFor: (_provider: string, own?: Record) => own ?? {}, ...stub, }); } diff --git a/packages/server/src/server/workspace-directory.test.ts b/packages/server/src/server/workspace-directory.test.ts index ab2ead15aa..e291007db1 100644 --- a/packages/server/src/server/workspace-directory.test.ts +++ b/packages/server/src/server/workspace-directory.test.ts @@ -921,7 +921,8 @@ test("scoped descriptor builds allocate independently of archived history", asyn projectLive: async () => { throw new Error("Unexpected live projection"); }, - projectStored: (record) => buildStoredAgentPayload(record, ["codex"]), + projectStored: (record) => + buildStoredAgentPayload(record, ["codex"], (_provider, own) => own ?? {}), isProviderVisible: () => true, isStoredProviderAvailable: () => true, });