From 5341b5c71ce48adb56f05e00f40e00f8d1d3e65a Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 3 Mar 2026 16:50:59 +1000 Subject: [PATCH] Diffs: Migrate tool usage guidance from before_prompt_build to a plugin skill (#32630) Merged via squash. Prepared head SHA: 585697a4e1556baa2cd79a7b449b120c4fd87e17 Co-authored-by: sircrumpet <4436535+sircrumpet@users.noreply.github.com> Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Reviewed-by: @gumadeiras --- CHANGELOG.md | 1 + docs/tools/diffs.md | 2 +- extensions/diffs/README.md | 2 + extensions/diffs/index.test.ts | 5 +- extensions/diffs/index.ts | 4 -- extensions/diffs/openclaw.plugin.json | 1 + extensions/diffs/skills/diffs/SKILL.md | 22 +++++++ extensions/diffs/src/prompt-guidance.ts | 11 ---- .../skills.loadworkspaceskillentries.test.ts | 62 +++++++++++++++++++ 9 files changed, 91 insertions(+), 19 deletions(-) create mode 100644 extensions/diffs/skills/diffs/SKILL.md delete mode 100644 extensions/diffs/src/prompt-guidance.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f1b9621533..55a11279eac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai ### Changes - Docs/Web search: remove outdated Brave free-tier wording and replace prescriptive AI ToS guidance with neutral compliance language in Brave setup docs. (#26860) Thanks @HenryLoenwind. +- Tools/Diffs guidance loading: move diffs usage guidance from unconditional prompt-hook injection to the plugin companion skill path, reducing unrelated-turn prompt noise while keeping diffs tool behavior unchanged. (#32630) thanks @sircrumpet. ### Fixes diff --git a/docs/tools/diffs.md b/docs/tools/diffs.md index 323374ac5a5..eb9706338f8 100644 --- a/docs/tools/diffs.md +++ b/docs/tools/diffs.md @@ -10,7 +10,7 @@ read_when: # Diffs -`diffs` is an optional plugin tool that turns change content into a read-only diff artifact for agents. +`diffs` is an optional plugin tool and companion skill that turns change content into a read-only diff artifact for agents. It accepts either: diff --git a/extensions/diffs/README.md b/extensions/diffs/README.md index a415a502f68..028835cf561 100644 --- a/extensions/diffs/README.md +++ b/extensions/diffs/README.md @@ -16,6 +16,8 @@ The tool can return: - `details.filePath`: a local rendered artifact path when file rendering is requested - `details.fileFormat`: the rendered file format (`png` or `pdf`) +When the plugin is enabled, it also ships a companion skill from `skills/` that guides when to use `diffs`. This guidance is delivered through normal skill loading, not unconditional prompt-hook injection on every turn. + This means an agent can: - call `diffs` with `mode=view`, then pass `details.viewerUrl` to `canvas present` diff --git a/extensions/diffs/index.test.ts b/extensions/diffs/index.test.ts index ea0d179787b..6c7e2555b58 100644 --- a/extensions/diffs/index.test.ts +++ b/extensions/diffs/index.test.ts @@ -4,7 +4,7 @@ import { createMockServerResponse } from "../../src/test-utils/mock-http-respons import plugin from "./index.js"; describe("diffs plugin registration", () => { - it("registers the tool, http route, and prompt guidance hook", () => { + it("registers the tool and http route", () => { const registerTool = vi.fn(); const registerHttpRoute = vi.fn(); const on = vi.fn(); @@ -43,8 +43,7 @@ describe("diffs plugin registration", () => { auth: "plugin", match: "prefix", }); - expect(on).toHaveBeenCalledTimes(1); - expect(on.mock.calls[0]?.[0]).toBe("before_prompt_build"); + expect(on).not.toHaveBeenCalled(); }); it("applies plugin-config defaults through registered tool and viewer handler", async () => { diff --git a/extensions/diffs/index.ts b/extensions/diffs/index.ts index bef57e83bd3..945448656e2 100644 --- a/extensions/diffs/index.ts +++ b/extensions/diffs/index.ts @@ -7,7 +7,6 @@ import { resolveDiffsPluginSecurity, } from "./src/config.js"; import { createDiffsHttpHandler } from "./src/http.js"; -import { DIFFS_AGENT_GUIDANCE } from "./src/prompt-guidance.js"; import { DiffArtifactStore } from "./src/store.js"; import { createDiffsTool } from "./src/tool.js"; @@ -35,9 +34,6 @@ const plugin = { allowRemoteViewer: security.allowRemoteViewer, }), }); - api.on("before_prompt_build", async () => ({ - prependContext: DIFFS_AGENT_GUIDANCE, - })); }, }; diff --git a/extensions/diffs/openclaw.plugin.json b/extensions/diffs/openclaw.plugin.json index 00db3002142..ef371e2b8c1 100644 --- a/extensions/diffs/openclaw.plugin.json +++ b/extensions/diffs/openclaw.plugin.json @@ -2,6 +2,7 @@ "id": "diffs", "name": "Diffs", "description": "Read-only diff viewer and file renderer for agents.", + "skills": ["./skills"], "uiHints": { "defaults.fontFamily": { "label": "Default Font", diff --git a/extensions/diffs/skills/diffs/SKILL.md b/extensions/diffs/skills/diffs/SKILL.md new file mode 100644 index 00000000000..8639a33ef90 --- /dev/null +++ b/extensions/diffs/skills/diffs/SKILL.md @@ -0,0 +1,22 @@ +--- +name: diffs +description: Use the diffs tool to produce real, shareable diffs (viewer URL, file artifact, or both) instead of manual edit summaries. +--- + +When you need to show edits as a real diff, prefer the `diffs` tool instead of writing a manual summary. + +The `diffs` tool accepts either `before` + `after` text, or a unified `patch` string. + +Use `mode=view` when you want an interactive gateway-hosted viewer. After the tool returns, use `details.viewerUrl` with the canvas tool via `canvas present` or `canvas navigate`. + +Use `mode=file` when you need a rendered file artifact. Set `fileFormat=png` (default) or `fileFormat=pdf`. The tool result includes `details.filePath`. + +For large or high-fidelity files, use `fileQuality` (`standard`|`hq`|`print`) and optionally override `fileScale`/`fileMaxWidth`. + +When you need to deliver the rendered file to a user or channel, do not rely on the raw tool-result renderer. Instead, call the `message` tool and pass `details.filePath` through `path` or `filePath`. + +Use `mode=both` when you want both the gateway viewer URL and the rendered artifact. + +If the user has configured diffs plugin defaults, prefer omitting `mode`, `theme`, `layout`, and related presentation options unless you need to override them for this specific diff. + +Include `path` for before/after text when you know the file name. diff --git a/extensions/diffs/src/prompt-guidance.ts b/extensions/diffs/src/prompt-guidance.ts deleted file mode 100644 index e70fa881ea8..00000000000 --- a/extensions/diffs/src/prompt-guidance.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const DIFFS_AGENT_GUIDANCE = [ - "When you need to show edits as a real diff, prefer the `diffs` tool instead of writing a manual summary.", - "The `diffs` tool accepts either `before` + `after` text, or a unified `patch` string.", - "Use `mode=view` when you want an interactive gateway-hosted viewer. After the tool returns, use `details.viewerUrl` with the canvas tool via `canvas present` or `canvas navigate`.", - "Use `mode=file` when you need a rendered file artifact. Set `fileFormat=png` (default) or `fileFormat=pdf`. The tool result includes `details.filePath`.", - "For large or high-fidelity files, use `fileQuality` (`standard`|`hq`|`print`) and optionally override `fileScale`/`fileMaxWidth`.", - "When you need to deliver the rendered file to a user or channel, do not rely on the raw tool-result renderer. Instead, call the `message` tool and pass `details.filePath` through `path` or `filePath`.", - "Use `mode=both` when you want both the gateway viewer URL and the rendered artifact.", - "If the user has configured diffs plugin defaults, prefer omitting `mode`, `theme`, `layout`, and related presentation options unless you need to override them for this specific diff.", - "Include `path` for before/after text when you know the file name.", -].join("\n"); diff --git a/src/agents/skills.loadworkspaceskillentries.test.ts b/src/agents/skills.loadworkspaceskillentries.test.ts index 501719fc7bd..456355e4ea7 100644 --- a/src/agents/skills.loadworkspaceskillentries.test.ts +++ b/src/agents/skills.loadworkspaceskillentries.test.ts @@ -48,6 +48,36 @@ async function setupWorkspaceWithProsePlugin() { return { workspaceDir, managedDir, bundledDir }; } +async function setupWorkspaceWithDiffsPlugin() { + const workspaceDir = await createTempWorkspaceDir(); + const managedDir = path.join(workspaceDir, ".managed"); + const bundledDir = path.join(workspaceDir, ".bundled"); + const pluginRoot = path.join(workspaceDir, ".openclaw", "extensions", "diffs"); + + await fs.mkdir(path.join(pluginRoot, "skills", "diffs"), { recursive: true }); + await fs.writeFile( + path.join(pluginRoot, "openclaw.plugin.json"), + JSON.stringify( + { + id: "diffs", + skills: ["./skills"], + configSchema: { type: "object", additionalProperties: false, properties: {} }, + }, + null, + 2, + ), + "utf-8", + ); + await fs.writeFile(path.join(pluginRoot, "index.ts"), "export {};\n", "utf-8"); + await fs.writeFile( + path.join(pluginRoot, "skills", "diffs", "SKILL.md"), + `---\nname: diffs\ndescription: test\n---\n`, + "utf-8", + ); + + return { workspaceDir, managedDir, bundledDir }; +} + describe("loadWorkspaceSkillEntries", () => { it("handles an empty managed skills dir without throwing", async () => { const workspaceDir = await createTempWorkspaceDir(); @@ -93,4 +123,36 @@ describe("loadWorkspaceSkillEntries", () => { expect(entries.map((entry) => entry.skill.name)).not.toContain("prose"); }); + + it("includes diffs plugin skill when the plugin is enabled", async () => { + const { workspaceDir, managedDir, bundledDir } = await setupWorkspaceWithDiffsPlugin(); + + const entries = loadWorkspaceSkillEntries(workspaceDir, { + config: { + plugins: { + entries: { diffs: { enabled: true } }, + }, + }, + managedSkillsDir: managedDir, + bundledSkillsDir: bundledDir, + }); + + expect(entries.map((entry) => entry.skill.name)).toContain("diffs"); + }); + + it("excludes diffs plugin skill when the plugin is disabled", async () => { + const { workspaceDir, managedDir, bundledDir } = await setupWorkspaceWithDiffsPlugin(); + + const entries = loadWorkspaceSkillEntries(workspaceDir, { + config: { + plugins: { + entries: { diffs: { enabled: false } }, + }, + }, + managedSkillsDir: managedDir, + bundledSkillsDir: bundledDir, + }); + + expect(entries.map((entry) => entry.skill.name)).not.toContain("diffs"); + }); });