fix: preserve agent instructions on adapter switch

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta
2026-03-22 07:05:08 -05:00
parent 75c7eb3868
commit a315838d43
2 changed files with 75 additions and 2 deletions

View File

@@ -197,4 +197,43 @@ describe("agent instructions bundle routes", () => {
expect.any(Object),
);
});
it("preserves managed instructions config when switching adapters", async () => {
mockAgentService.getById.mockResolvedValue({
...makeAgent(),
adapterType: "codex_local",
adapterConfig: {
instructionsBundleMode: "managed",
instructionsRootPath: "/tmp/agent-1",
instructionsEntryFile: "AGENTS.md",
instructionsFilePath: "/tmp/agent-1/AGENTS.md",
model: "gpt-5.4",
},
});
const res = await request(createApp())
.patch("/api/agents/11111111-1111-4111-8111-111111111111?companyId=company-1")
.send({
adapterType: "claude_local",
adapterConfig: {
model: "claude-sonnet-4",
},
});
expect(res.status, JSON.stringify(res.body)).toBe(200);
expect(mockAgentService.update).toHaveBeenCalledWith(
"11111111-1111-4111-8111-111111111111",
expect.objectContaining({
adapterType: "claude_local",
adapterConfig: expect.objectContaining({
model: "claude-sonnet-4",
instructionsBundleMode: "managed",
instructionsRootPath: "/tmp/agent-1",
instructionsEntryFile: "AGENTS.md",
instructionsFilePath: "/tmp/agent-1/AGENTS.md",
}),
}),
expect.any(Object),
);
});
});

View File

@@ -73,6 +73,13 @@ export function agentRoutes(db: Db) {
};
const DEFAULT_MANAGED_INSTRUCTIONS_ADAPTER_TYPES = new Set(Object.keys(DEFAULT_INSTRUCTIONS_PATH_KEYS));
const KNOWN_INSTRUCTIONS_PATH_KEYS = new Set(["instructionsFilePath", "agentsMdPath"]);
const KNOWN_INSTRUCTIONS_BUNDLE_KEYS = [
"instructionsBundleMode",
"instructionsRootPath",
"instructionsEntryFile",
"instructionsFilePath",
"agentsMdPath",
] as const;
const router = Router();
const svc = agentService(db);
@@ -303,6 +310,24 @@ export function agentRoutes(db: Db) {
return trimmed.length > 0 ? trimmed : null;
}
function preserveInstructionsBundleConfig(
existingAdapterConfig: Record<string, unknown>,
nextAdapterConfig: Record<string, unknown>,
) {
const nextKeys = new Set(Object.keys(nextAdapterConfig));
if (KNOWN_INSTRUCTIONS_BUNDLE_KEYS.some((key) => nextKeys.has(key))) {
return nextAdapterConfig;
}
const merged = { ...nextAdapterConfig };
for (const key of KNOWN_INSTRUCTIONS_BUNDLE_KEYS) {
if (merged[key] === undefined && existingAdapterConfig[key] !== undefined) {
merged[key] = existingAdapterConfig[key];
}
}
return merged;
}
function parseBooleanLike(value: unknown): boolean | null {
if (typeof value === "boolean") return value;
if (typeof value === "number") {
@@ -1710,9 +1735,18 @@ export function agentRoutes(db: Db) {
Object.prototype.hasOwnProperty.call(patchData, "adapterType") ||
Object.prototype.hasOwnProperty.call(patchData, "adapterConfig");
if (touchesAdapterConfiguration) {
const rawEffectiveAdapterConfig = Object.prototype.hasOwnProperty.call(patchData, "adapterConfig")
const existingAdapterConfig = asRecord(existing.adapterConfig) ?? {};
const changingAdapterType =
typeof patchData.adapterType === "string" && patchData.adapterType !== existing.adapterType;
let rawEffectiveAdapterConfig = Object.prototype.hasOwnProperty.call(patchData, "adapterConfig")
? (asRecord(patchData.adapterConfig) ?? {})
: (asRecord(existing.adapterConfig) ?? {});
: existingAdapterConfig;
if (changingAdapterType) {
rawEffectiveAdapterConfig = preserveInstructionsBundleConfig(
existingAdapterConfig,
rawEffectiveAdapterConfig,
);
}
const effectiveAdapterConfig = applyCreateDefaultsByAdapterType(
requestedAdapterType,
rawEffectiveAdapterConfig,