Recover agent instructions from disk
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
3b2cb3a699
commit
0bb1ee3caa
4 changed files with 169 additions and 5 deletions
|
|
@ -236,4 +236,42 @@ describe("agent instructions bundle routes", () => {
|
|||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("merges same-adapter config patches so instructions metadata is not dropped", 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({
|
||||
adapterConfig: {
|
||||
command: "codex --profile engineer",
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(200);
|
||||
expect(mockAgentService.update).toHaveBeenCalledWith(
|
||||
"11111111-1111-4111-8111-111111111111",
|
||||
expect.objectContaining({
|
||||
adapterConfig: expect.objectContaining({
|
||||
command: "codex --profile engineer",
|
||||
model: "gpt-5.4",
|
||||
instructionsBundleMode: "managed",
|
||||
instructionsRootPath: "/tmp/agent-1",
|
||||
instructionsEntryFile: "AGENTS.md",
|
||||
instructionsFilePath: "/tmp/agent-1/AGENTS.md",
|
||||
}),
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -192,4 +192,88 @@ describe("agent instructions service", () => {
|
|||
expect(bundle.files.map((file) => file.path)).toEqual(["AGENTS.md"]);
|
||||
expect(exported.files).toEqual({ "AGENTS.md": "# Recovered Agent\n" });
|
||||
});
|
||||
|
||||
it("prefers the managed bundle on disk when managed metadata points at a stale root", async () => {
|
||||
const paperclipHome = await makeTempDir("paperclip-agent-instructions-stale-managed-");
|
||||
const staleRoot = await makeTempDir("paperclip-agent-instructions-stale-root-");
|
||||
cleanupDirs.add(paperclipHome);
|
||||
cleanupDirs.add(staleRoot);
|
||||
process.env.PAPERCLIP_HOME = paperclipHome;
|
||||
process.env.PAPERCLIP_INSTANCE_ID = "test-instance";
|
||||
|
||||
const managedRoot = path.join(
|
||||
paperclipHome,
|
||||
"instances",
|
||||
"test-instance",
|
||||
"companies",
|
||||
"company-1",
|
||||
"agents",
|
||||
"agent-1",
|
||||
"instructions",
|
||||
);
|
||||
await fs.mkdir(managedRoot, { recursive: true });
|
||||
await fs.writeFile(path.join(managedRoot, "AGENTS.md"), "# Managed Agent\n", "utf8");
|
||||
|
||||
const svc = agentInstructionsService();
|
||||
const agent = makeAgent({
|
||||
instructionsBundleMode: "managed",
|
||||
instructionsRootPath: staleRoot,
|
||||
instructionsEntryFile: "docs/MISSING.md",
|
||||
instructionsFilePath: path.join(staleRoot, "docs", "MISSING.md"),
|
||||
});
|
||||
|
||||
const bundle = await svc.getBundle(agent);
|
||||
const exported = await svc.exportFiles(agent);
|
||||
|
||||
expect(bundle.mode).toBe("managed");
|
||||
expect(bundle.rootPath).toBe(managedRoot);
|
||||
expect(bundle.entryFile).toBe("AGENTS.md");
|
||||
expect(bundle.files.map((file) => file.path)).toEqual(["AGENTS.md"]);
|
||||
expect(bundle.warnings).toEqual([
|
||||
`Recovered managed instructions from disk at ${managedRoot}; ignoring stale configured root ${staleRoot}.`,
|
||||
"Recovered managed instructions entry file from disk as AGENTS.md; previous entry docs/MISSING.md was missing.",
|
||||
]);
|
||||
expect(exported.files).toEqual({ "AGENTS.md": "# Managed Agent\n" });
|
||||
});
|
||||
|
||||
it("recovers the managed bundle when stale root metadata is present but mode is missing", async () => {
|
||||
const paperclipHome = await makeTempDir("paperclip-agent-instructions-partial-managed-");
|
||||
const staleRoot = await makeTempDir("paperclip-agent-instructions-partial-root-");
|
||||
cleanupDirs.add(paperclipHome);
|
||||
cleanupDirs.add(staleRoot);
|
||||
process.env.PAPERCLIP_HOME = paperclipHome;
|
||||
process.env.PAPERCLIP_INSTANCE_ID = "test-instance";
|
||||
|
||||
const managedRoot = path.join(
|
||||
paperclipHome,
|
||||
"instances",
|
||||
"test-instance",
|
||||
"companies",
|
||||
"company-1",
|
||||
"agents",
|
||||
"agent-1",
|
||||
"instructions",
|
||||
);
|
||||
await fs.mkdir(managedRoot, { recursive: true });
|
||||
await fs.writeFile(path.join(managedRoot, "AGENTS.md"), "# Managed Agent\n", "utf8");
|
||||
|
||||
const svc = agentInstructionsService();
|
||||
const agent = makeAgent({
|
||||
instructionsRootPath: staleRoot,
|
||||
instructionsEntryFile: "docs/MISSING.md",
|
||||
});
|
||||
|
||||
const bundle = await svc.getBundle(agent);
|
||||
const exported = await svc.exportFiles(agent);
|
||||
|
||||
expect(bundle.mode).toBe("managed");
|
||||
expect(bundle.rootPath).toBe(managedRoot);
|
||||
expect(bundle.entryFile).toBe("AGENTS.md");
|
||||
expect(bundle.files.map((file) => file.path)).toEqual(["AGENTS.md"]);
|
||||
expect(bundle.warnings).toEqual([
|
||||
`Recovered managed instructions from disk at ${managedRoot}; ignoring stale configured root ${staleRoot}.`,
|
||||
"Recovered managed instructions entry file from disk as AGENTS.md; previous entry docs/MISSING.md was missing.",
|
||||
]);
|
||||
expect(exported.files).toEqual({ "AGENTS.md": "# Managed Agent\n" });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1727,9 +1727,13 @@ export function agentRoutes(db: Db) {
|
|||
const existingAdapterConfig = asRecord(existing.adapterConfig) ?? {};
|
||||
const changingAdapterType =
|
||||
typeof patchData.adapterType === "string" && patchData.adapterType !== existing.adapterType;
|
||||
let rawEffectiveAdapterConfig = Object.prototype.hasOwnProperty.call(patchData, "adapterConfig")
|
||||
const requestedAdapterConfig = Object.prototype.hasOwnProperty.call(patchData, "adapterConfig")
|
||||
? (asRecord(patchData.adapterConfig) ?? {})
|
||||
: existingAdapterConfig;
|
||||
: null;
|
||||
let rawEffectiveAdapterConfig = requestedAdapterConfig ?? existingAdapterConfig;
|
||||
if (requestedAdapterConfig && !changingAdapterType) {
|
||||
rawEffectiveAdapterConfig = { ...existingAdapterConfig, ...requestedAdapterConfig };
|
||||
}
|
||||
if (changingAdapterType) {
|
||||
rawEffectiveAdapterConfig = preserveInstructionsBundleConfig(
|
||||
existingAdapterConfig,
|
||||
|
|
|
|||
|
|
@ -273,8 +273,6 @@ function deriveBundleState(agent: AgentLike): BundleState {
|
|||
}
|
||||
|
||||
async function recoverManagedBundleState(agent: AgentLike, state: BundleState): Promise<BundleState> {
|
||||
if (state.rootPath) return state;
|
||||
|
||||
const managedRootPath = resolveManagedInstructionsRoot(agent);
|
||||
const stat = await statIfExists(managedRootPath);
|
||||
if (!stat?.isDirectory()) return state;
|
||||
|
|
@ -282,11 +280,51 @@ async function recoverManagedBundleState(agent: AgentLike, state: BundleState):
|
|||
const files = await listFilesRecursive(managedRootPath);
|
||||
if (files.length === 0) return state;
|
||||
|
||||
const recoveredEntryFile = files.includes(state.entryFile)
|
||||
? state.entryFile
|
||||
: files.includes(ENTRY_FILE_DEFAULT)
|
||||
? ENTRY_FILE_DEFAULT
|
||||
: files[0]!;
|
||||
|
||||
if (!state.rootPath) {
|
||||
return {
|
||||
...state,
|
||||
mode: "managed",
|
||||
rootPath: managedRootPath,
|
||||
entryFile: recoveredEntryFile,
|
||||
resolvedEntryPath: path.resolve(managedRootPath, recoveredEntryFile),
|
||||
};
|
||||
}
|
||||
|
||||
if (state.mode === "external") return state;
|
||||
|
||||
const resolvedConfiguredRoot = path.resolve(state.rootPath);
|
||||
const configuredRootMatchesManaged = resolvedConfiguredRoot === managedRootPath;
|
||||
const hasEntryMismatch = recoveredEntryFile !== state.entryFile;
|
||||
|
||||
if (configuredRootMatchesManaged && !hasEntryMismatch) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const warnings = [...state.warnings];
|
||||
if (!configuredRootMatchesManaged) {
|
||||
warnings.push(
|
||||
`Recovered managed instructions from disk at ${managedRootPath}; ignoring stale configured root ${state.rootPath}.`,
|
||||
);
|
||||
}
|
||||
if (hasEntryMismatch) {
|
||||
warnings.push(
|
||||
`Recovered managed instructions entry file from disk as ${recoveredEntryFile}; previous entry ${state.entryFile} was missing.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
mode: "managed",
|
||||
rootPath: managedRootPath,
|
||||
resolvedEntryPath: path.resolve(managedRootPath, state.entryFile),
|
||||
entryFile: recoveredEntryFile,
|
||||
resolvedEntryPath: path.resolve(managedRootPath, recoveredEntryFile),
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue