mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-05 16:04:43 +00:00
fix(server): a closed agent's options include the provider defaults, like a live one's
The stored snapshot projected only the options the agent owns, so an agent that ran with a config.json default (for example Claude fast_mode: true) showed the option as missing, and its legacy Fast control as off, until it was resumed. The projection takes the same resolver the live agent uses (the provider's defaults under the agent's own options).
This commit is contained in:
@@ -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 ?? {});
|
||||
}
|
||||
|
||||
@@ -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<string, unknown> | undefined) => own ?? {};
|
||||
|
||||
type ManagedAgentOverrides = Omit<Partial<ManagedAgent>, "config" | "pendingPermissions"> & {
|
||||
config?: Partial<AgentSessionConfig>;
|
||||
pendingPermissions?: Map<string, AgentPermissionRequest>;
|
||||
@@ -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<string, unknown> | 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", () => {
|
||||
|
||||
@@ -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<AgentSnapshotPayload, "features" | "options" | "controls"> {
|
||||
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<AgentProvider>,
|
||||
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),
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
+2
@@ -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];
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -37,6 +37,7 @@ export function asAgentManager(stub: {
|
||||
return createStub<SessionOptions["agentManager"]>({
|
||||
listProviderSubagentActivity: () => [],
|
||||
getRetiredInternalAgent: () => null,
|
||||
effectiveOptionsFor: (_provider: string, own?: Record<string, unknown>) => own ?? {},
|
||||
...stub,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user