fix: recover managed agent instructions from disk
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
a315838d43
commit
1adfd30b3b
2 changed files with 68 additions and 7 deletions
|
|
@ -161,4 +161,35 @@ describe("agent instructions service", () => {
|
||||||
"docs/TOOLS.md",
|
"docs/TOOLS.md",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("recovers a managed bundle from disk when bundle config metadata is missing", async () => {
|
||||||
|
const paperclipHome = await makeTempDir("paperclip-agent-instructions-recover-");
|
||||||
|
cleanupDirs.add(paperclipHome);
|
||||||
|
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"), "# Recovered Agent\n", "utf8");
|
||||||
|
|
||||||
|
const svc = agentInstructionsService();
|
||||||
|
const agent = makeAgent({});
|
||||||
|
|
||||||
|
const bundle = await svc.getBundle(agent);
|
||||||
|
const exported = await svc.exportFiles(agent);
|
||||||
|
|
||||||
|
expect(bundle.mode).toBe("managed");
|
||||||
|
expect(bundle.rootPath).toBe(managedRoot);
|
||||||
|
expect(bundle.files.map((file) => file.path)).toEqual(["AGENTS.md"]);
|
||||||
|
expect(exported.files).toEqual({ "AGENTS.md": "# Recovered Agent\n" });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -272,6 +272,24 @@ 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;
|
||||||
|
|
||||||
|
const files = await listFilesRecursive(managedRootPath);
|
||||||
|
if (files.length === 0) return state;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
mode: "managed",
|
||||||
|
rootPath: managedRootPath,
|
||||||
|
resolvedEntryPath: path.resolve(managedRootPath, state.entryFile),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function toBundle(agent: AgentLike, state: BundleState, files: AgentInstructionsFileSummary[]): AgentInstructionsBundle {
|
function toBundle(agent: AgentLike, state: BundleState, files: AgentInstructionsFileSummary[]): AgentInstructionsBundle {
|
||||||
const nextFiles = [...files];
|
const nextFiles = [...files];
|
||||||
if (state.legacyPromptTemplateActive && !nextFiles.some((file) => file.path === LEGACY_PROMPT_TEMPLATE_PATH)) {
|
if (state.legacyPromptTemplateActive && !nextFiles.some((file) => file.path === LEGACY_PROMPT_TEMPLATE_PATH)) {
|
||||||
|
|
@ -366,7 +384,7 @@ export function syncInstructionsBundleConfigFromFilePath(
|
||||||
|
|
||||||
export function agentInstructionsService() {
|
export function agentInstructionsService() {
|
||||||
async function getBundle(agent: AgentLike): Promise<AgentInstructionsBundle> {
|
async function getBundle(agent: AgentLike): Promise<AgentInstructionsBundle> {
|
||||||
const state = deriveBundleState(agent);
|
const state = await recoverManagedBundleState(agent, deriveBundleState(agent));
|
||||||
if (!state.rootPath) return toBundle(agent, state, []);
|
if (!state.rootPath) return toBundle(agent, state, []);
|
||||||
const stat = await statIfExists(state.rootPath);
|
const stat = await statIfExists(state.rootPath);
|
||||||
if (!stat?.isDirectory()) {
|
if (!stat?.isDirectory()) {
|
||||||
|
|
@ -381,7 +399,7 @@ export function agentInstructionsService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readFile(agent: AgentLike, relativePath: string): Promise<AgentInstructionsFileDetail> {
|
async function readFile(agent: AgentLike, relativePath: string): Promise<AgentInstructionsFileDetail> {
|
||||||
const state = deriveBundleState(agent);
|
const state = await recoverManagedBundleState(agent, deriveBundleState(agent));
|
||||||
if (relativePath === LEGACY_PROMPT_TEMPLATE_PATH) {
|
if (relativePath === LEGACY_PROMPT_TEMPLATE_PATH) {
|
||||||
const content = asString(state.config[PROMPT_KEY]);
|
const content = asString(state.config[PROMPT_KEY]);
|
||||||
if (content === null) throw notFound("Instructions file not found");
|
if (content === null) throw notFound("Instructions file not found");
|
||||||
|
|
@ -422,9 +440,21 @@ export function agentInstructionsService() {
|
||||||
agent: AgentLike,
|
agent: AgentLike,
|
||||||
options?: { clearLegacyPromptTemplate?: boolean },
|
options?: { clearLegacyPromptTemplate?: boolean },
|
||||||
): Promise<{ adapterConfig: Record<string, unknown>; state: BundleState }> {
|
): Promise<{ adapterConfig: Record<string, unknown>; state: BundleState }> {
|
||||||
const current = deriveBundleState(agent);
|
const derived = deriveBundleState(agent);
|
||||||
|
const current = await recoverManagedBundleState(agent, derived);
|
||||||
if (current.rootPath && current.mode) {
|
if (current.rootPath && current.mode) {
|
||||||
return { adapterConfig: current.config, state: current };
|
const adapterConfig = derived.rootPath
|
||||||
|
? current.config
|
||||||
|
: applyBundleConfig(current.config, {
|
||||||
|
mode: current.mode,
|
||||||
|
rootPath: current.rootPath,
|
||||||
|
entryFile: current.entryFile,
|
||||||
|
clearLegacyPromptTemplate: options?.clearLegacyPromptTemplate,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
adapterConfig,
|
||||||
|
state: deriveBundleState({ ...agent, adapterConfig }),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const managedRoot = resolveManagedInstructionsRoot(agent);
|
const managedRoot = resolveManagedInstructionsRoot(agent);
|
||||||
|
|
@ -462,7 +492,7 @@ export function agentInstructionsService() {
|
||||||
clearLegacyPromptTemplate?: boolean;
|
clearLegacyPromptTemplate?: boolean;
|
||||||
},
|
},
|
||||||
): Promise<{ bundle: AgentInstructionsBundle; adapterConfig: Record<string, unknown> }> {
|
): Promise<{ bundle: AgentInstructionsBundle; adapterConfig: Record<string, unknown> }> {
|
||||||
const state = deriveBundleState(agent);
|
const state = await recoverManagedBundleState(agent, deriveBundleState(agent));
|
||||||
const nextMode = input.mode ?? state.mode ?? "managed";
|
const nextMode = input.mode ?? state.mode ?? "managed";
|
||||||
const nextEntryFile = input.entryFile ? normalizeRelativeFilePath(input.entryFile) : state.entryFile;
|
const nextEntryFile = input.entryFile ? normalizeRelativeFilePath(input.entryFile) : state.entryFile;
|
||||||
let nextRootPath: string;
|
let nextRootPath: string;
|
||||||
|
|
@ -544,7 +574,7 @@ export function agentInstructionsService() {
|
||||||
bundle: AgentInstructionsBundle;
|
bundle: AgentInstructionsBundle;
|
||||||
adapterConfig: Record<string, unknown>;
|
adapterConfig: Record<string, unknown>;
|
||||||
}> {
|
}> {
|
||||||
const state = deriveBundleState(agent);
|
const state = await recoverManagedBundleState(agent, deriveBundleState(agent));
|
||||||
if (relativePath === LEGACY_PROMPT_TEMPLATE_PATH) {
|
if (relativePath === LEGACY_PROMPT_TEMPLATE_PATH) {
|
||||||
throw unprocessable("Cannot delete the legacy promptTemplate pseudo-file");
|
throw unprocessable("Cannot delete the legacy promptTemplate pseudo-file");
|
||||||
}
|
}
|
||||||
|
|
@ -564,7 +594,7 @@ export function agentInstructionsService() {
|
||||||
entryFile: string;
|
entryFile: string;
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
}> {
|
}> {
|
||||||
const state = deriveBundleState(agent);
|
const state = await recoverManagedBundleState(agent, deriveBundleState(agent));
|
||||||
if (state.rootPath) {
|
if (state.rootPath) {
|
||||||
const stat = await statIfExists(state.rootPath);
|
const stat = await statIfExists(state.rootPath);
|
||||||
if (stat?.isDirectory()) {
|
if (stat?.isDirectory()) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue