mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 05:11:49 +01:00
fix(runner): return the edited agent's link and effect in instruction save receipts
Completion feedback forbids guessing routes when no href was returned, and the instruction tools returned none, so chat replies after an instruction edit could not link the edited agent. Save and restore receipts now carry a resourceLinks entry with the agent's real profile href and state that runs starting after the save use the new instructions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4b367425e6
commit
988094c256
2 files changed
+23
-3
No files matched your search
@@ -59,6 +59,12 @@ describe("canonical instruction tools through native authority", () => {
|
||||
expect(first).toMatchObject({ entryFile, content: original });
|
||||
const changed = await call("update_agent_instructions", { entryFile, content: "changed\r\n", baseRevisionId: first.revision.id });
|
||||
expect(changed.revision).toMatchObject({ responsibleUserId: userId, actorAgentId: agentId, sourceRunId: runId });
|
||||
// The chat reply must not guess routes, so the receipt carries the edited
|
||||
// agent's real profile href and when the saved instructions take effect.
|
||||
const [company] = await db.select({ issuePrefix: companies.issuePrefix }).from(companies).where(eq(companies.id, companyId));
|
||||
expect(changed.resourceLinks).toEqual([{ kind: "agent", id: targetAgentId, name: "Target",
|
||||
href: `/${encodeURIComponent(company!.issuePrefix)}/agents/${targetAgentId}` }]);
|
||||
expect(changed.appliesTo).toMatch(/start after this save/);
|
||||
expect((await call("get_agent_instruction_history", { entryFile })).revisions).toHaveLength(0);
|
||||
expect(await fs.readFile(path.join(root, entryFile), "utf8")).toBe("changed\r\n");
|
||||
await expect(call("update_agent_instructions", { entryFile, content: "stale", baseRevisionId: first.revision.id })).rejects.toMatchObject({ status: 409 });
|
||||
@@ -200,7 +206,7 @@ describe("canonical instruction tools through native authority", () => {
|
||||
await expect(authority.execute(request)).rejects.toThrow("injected lost commit acknowledgement");
|
||||
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId));
|
||||
const receipt = (run!.resultJson as any).semanticToolReceipts[`instruction:${request.callId}`].result;
|
||||
expect(receipt).toMatchObject({ changed: true, content });
|
||||
expect(receipt).toMatchObject({ changed: true, content, resourceLinks: [{ kind: "agent", id: targetAgentId, name: "Target" }] });
|
||||
authority = new PaperclipRunnerToolAuthority(db, { companyId, agentId, issueId, runId });
|
||||
expect(await authority.recover(request)).toEqual({ result: receipt });
|
||||
expect(await authority.recover({ ...request, callId: randomUUID() })).toBeNull();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AgentInstructionCommitReceipt } from "@paperclipai/shared";
|
||||
import { z } from "zod";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { agents, companies, type Db } from "@paperclipai/db";
|
||||
import { badRequest, notFound } from "../../errors.js";
|
||||
import { agentInstructionRevisionService } from "../agent-instruction-revisions.js";
|
||||
import { agentInstructionWorkingCopyService } from "../agent-instruction-working-copies.js";
|
||||
@@ -44,7 +45,7 @@ export async function executeAgentInstructionTool(input: {
|
||||
...scope(targetAgentId), runId: input.binding.runId, entryFile: receipt.revision.entryFile,
|
||||
revisionId: receipt.revision.id, contentHash: receipt.revision.contentHash,
|
||||
}).catch(() => undefined);
|
||||
return receipt;
|
||||
return { ...receipt, ...(await savedAgentResource(input.db, scope(targetAgentId))) };
|
||||
};
|
||||
switch (input.tool) {
|
||||
case "read_agent_instructions": {
|
||||
@@ -74,3 +75,16 @@ export async function executeAgentInstructionTool(input: {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Runs snapshot instructions when their runtime context is built, so a save
|
||||
* reaches only runs that start afterward. The chat reply needs a real profile
|
||||
* href for the edited agent; completion guidance forbids guessing routes. */
|
||||
const INSTRUCTION_SAVE_EFFECT = "Runs that start after this save use these instructions. Runs already in progress keep the instructions they started with.";
|
||||
async function savedAgentResource(db: Db, target: { companyId: string; agentId: string }) {
|
||||
const [row] = await db.select({ name: agents.name, issuePrefix: companies.issuePrefix }).from(agents)
|
||||
.innerJoin(companies, eq(companies.id, agents.companyId))
|
||||
.where(and(eq(agents.id, target.agentId), eq(agents.companyId, target.companyId)));
|
||||
if (!row) return { appliesTo: INSTRUCTION_SAVE_EFFECT };
|
||||
const href = `/${encodeURIComponent(row.issuePrefix)}/agents/${encodeURIComponent(target.agentId)}`;
|
||||
return { appliesTo: INSTRUCTION_SAVE_EFFECT, resourceLinks: [{ kind: "agent", id: target.agentId, name: row.name, href }] };
|
||||
}
|
||||
Reference in new issue
Block a user