mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
fix(runner): return the edited agent's link and effect in instruction save receipts (#19940)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - In chat, a person can ask the CEO agent to change another agent's saved instructions > - The native instruction tools save the change, but their receipts contain no link to the edited agent > - Completion feedback tells the agent to use only hrefs that tools returned and not to guess routes > - So the agent can not link the agent it changed, and it has no grounded fact about when the change applies > - This pull request adds the edited agent's real profile href and the save's effect to the save and restore receipts > - The benefit is a chat reply that lets the person open the changed agent and know which runs use the new rule ## Linked Issues or Issue Description Refs #19344 **What happened** A person asked in chat: "Add a rule for Morgan to run the relevant tests before handing work back." The agent saved the rule correctly and kept concurrent human guidance. The reply did not link Morgan and did not say when the rule applies. The run's tool events show the cause: `update_agent_instructions` returned the revision but no href, and the accepted `paperclip_finish` feedback said "Do not guess routes or invent links when no resource href was returned." **Expected behavior** After an instruction save or restore, the agent has a real profile href for the edited agent and a statement of when the saved instructions take effect. The reply links the agent and explains that later runs use the change. **Steps to reproduce** 1. In a chat with the CEO agent, ask it to add a rule to another agent's instructions. 2. Let the agent call `update_agent_instructions`. 3. Read the tool receipt. It contains no href for the edited agent and no effect statement. ## What Changed - `update_agent_instructions` and `restore_agent_instructions` receipts now include `resourceLinks` with the edited agent's company-scoped profile href (`/<prefix>/agents/<agentId>`) and name. - The same receipts include `appliesTo`. It states that runs that start after the save use the new instructions and that runs in progress keep their snapshot. This matches `materializeInstructionBundle`, which reads the committed head when it builds the run context. - The link uses the agent id, which is unique. It does not use the name-derived URL key. ## Verification - `cd server && pnpm exec vitest run src/services/native-runtime/agent-instruction-tools.integration.test.ts src/services/native-runtime/agent-instruction-tools.test.ts`: 14 passed. - Without the source change, the new assertions fail in 3 tests (update receipt and both lost-acknowledgement recovery receipts). - The recovery test confirms that the persisted receipt, which recovery replays, also carries the link. - The end-to-end chat eval `agents.edit-instructions` runs this change on its case branch. ## Risks - Low risk. The change adds fields to a tool receipt. Existing fields do not change. - Receipts that were saved before this change replay without the new fields. That is acceptable: the agent then has no link, as before. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code execution in Claude Code. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
196108acb9
commit
83dec71a8f
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