fix(skills): omit content hashes from prompts (#126951)

This commit is contained in:
Bek
2026-08-21 01:13:42 -04:00
committed by GitHub
parent df8fedcc00
commit 76bb7ff2b6
13 changed files with 18 additions and 72 deletions
+1 -2
View File
@@ -143,7 +143,7 @@ See [Timezones](/concepts/timezone) and [Date & Time](/date-time) for full behav
## Skills
When eligible skills exist, OpenClaw injects a compact `<available_skills>` list (`formatSkillsForPrompt`) with the **file path** and a content-derived `<version>sha256:...</version>` marker per skill. The prompt instructs the model to use `read` to load the SKILL.md at the listed location (workspace, managed, or bundled), and to re-read a skill when its `<version>` differs from a previous turn. If no skills are eligible, the Skills section is omitted.
When eligible skills exist, OpenClaw injects a compact `<available_skills>` list (`formatSkillsForPrompt`) with the **file path** for each skill. The prompt instructs the model to use `read` to load the SKILL.md at the listed location (workspace, managed, or bundled). If no skills are eligible, the Skills section is omitted.
Native Codex turns receive this list as turn-scoped collaboration developer instructions instead of per-turn user input, except lightweight cron turns that preserve the exact scheduled prompt. Other harnesses keep the normal prompt section.
@@ -157,7 +157,6 @@ Eligibility includes skill metadata gates, runtime environment/config checks, an
<name>...</name>
<description>...</description>
<location>...</location>
<version>sha256:...</version>
</skill>
</available_skills>
```
-3
View File
@@ -378,7 +378,6 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).toContain("## Skills");
expect(prompt).toContain("<available_skills>");
expect(prompt).toContain("Changed <version>: re-read");
expect(prompt).toContain("External writes: batch safely");
});
@@ -922,7 +921,6 @@ describe("buildAgentSystemPrompt", () => {
"Scan <available_skills>. Clear match: read exact <location> with `Read`; obey.",
);
expect(prompt).not.toContain("<location>/SKILL.md");
expect(prompt).toContain("Changed <version>: re-read");
expect(prompt).toContain("Several: most specific");
expect(prompt).toContain("Docs: /tmp/openclaw/docs");
expect(prompt).toContain(
@@ -1186,7 +1184,6 @@ describe("buildAgentSystemPrompt", () => {
"Scan <available_skills>. Clear match: read exact <location> with `read`; obey.",
);
expect(prompt).not.toContain("<location>/SKILL.md");
expect(prompt).toContain("Changed <version>: re-read");
expect(prompt).toContain("Several: most specific");
});
+1 -1
View File
@@ -283,7 +283,7 @@ function buildSkillsSection(params: {
params.codeModeActive
? 'Scan <available_skills>. Clear match: use `skills.read("<name>")` inside `exec`; obey.'
: `Scan <available_skills>. Clear match: read exact <location> with \`${params.readToolName}\`; obey.`,
"Changed <version>: re-read. Several: most specific. None: read none.",
"Several: most specific. None: read none.",
"Up-front max one. Never invent paths.",
"External writes: batch safely; no tight loops; honor 429/Retry-After.",
trimmed,
-2
View File
@@ -13,7 +13,6 @@ import {
resolveSkillDisplayName,
type Skill,
} from "./skill-contract.js";
import { computeSkillPromptVersion } from "./skill-version.js";
type LoadedLocalSkill = {
skill: Skill;
@@ -112,7 +111,6 @@ function loadSingleSkillDirectory(params: {
description,
filePath,
baseDir,
promptVersion: computeSkillPromptVersion(raw),
source: params.source,
sourceInfo: createSyntheticSourceInfo(filePath, {
source: params.source,
+1 -2
View File
@@ -15,7 +15,6 @@ import { getArchivedSkillFiles } from "../workshop/curator.js";
import { parseSkillFrontmatter, resolveSkillInvocationPolicy } from "./frontmatter.js";
import { resolveSkillDisplayName } from "./skill-contract.js";
import { formatSkillsForPromptBounded } from "./skill-prompt-limits.js";
import { computeSkillPromptVersion } from "./skill-version.js";
/** Max name length per spec */
const MAX_NAME_LENGTH = 64;
@@ -30,6 +29,7 @@ export interface Skill {
description: string;
filePath: string;
baseDir: string;
/** @deprecated Ignored; retained for API compatibility until the next Plugin SDK major. */
promptVersion?: string;
source: string;
sourceInfo: SourceInfo;
@@ -254,7 +254,6 @@ function loadSkillFromFile(
description: frontmatter.description,
filePath,
baseDir: skillDir,
promptVersion: computeSkillPromptVersion(rawContent),
source,
sourceInfo: createSkillSourceInfo(filePath, skillDir, source),
disableModelInvocation: invocation.disableModelInvocation,
+10 -6
View File
@@ -42,7 +42,9 @@ describe("formatSkillsCompact", () => {
makeSkill("notes", "Summarize notes", "/tmp/notes/SKILL.md"),
{ ...makeSkill("weather", "Get weather <data> & forecasts"), promptVersion: "sha256:abc123" },
];
expect(formatSkillsForPromptCore(skills)).toBe(upstreamFormatSkillsForPrompt(skills));
const out = formatSkillsForPromptCore(skills);
expect(out).toBe(upstreamFormatSkillsForPrompt(skills));
expect(out).not.toContain("<version>");
});
it("renders all passed skills in the full formatter without reapplying visibility policy", () => {
@@ -56,14 +58,16 @@ describe("formatSkillsCompact", () => {
expect(formatSkillsCompact([])).toBe("");
});
it("keeps compact descriptions with name, location, and version", () => {
const out = formatSkillsCompact([
{ ...makeSkill("weather", "Get weather data"), promptVersion: "sha256:abc123" },
]);
it("keeps compact descriptions with name and location", () => {
const skill = {
...makeSkill("weather", "Get weather data"),
promptVersion: "sha256:abc123",
};
const out = formatSkillsCompact([skill]);
expect(out).toContain("<name>weather</name>");
expect(out).toContain("<description>Get weather data</description>");
expect(out).toContain("<location>/skills/weather/SKILL.md</location>");
expect(out).toContain("<version>sha256:abc123</version>");
expect(out).not.toContain("<version>");
});
it("omits descriptions when their compact budget is zero", () => {
-10
View File
@@ -13,8 +13,6 @@ export interface Skill {
readContent?: string;
filePath: string;
baseDir: string;
/** Deterministic marker for the SKILL.md content rendered as <version>. */
promptVersion?: string;
sourceInfo: SourceInfo;
disableModelInvocation: boolean;
// Preserve legacy source reads while keeping the canonical upstream shape.
@@ -75,7 +73,6 @@ export function formatSkillsForPromptCore(skills: Skill[]): string {
const lines = [
"\n\nThe following skills provide specialized instructions for specific tasks.",
"Use the read tool to load a skill's file when the task matches its description.",
"If a skill's <version> differs from a previous turn, re-read its SKILL.md before using it.",
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
"",
"<available_skills>",
@@ -88,9 +85,6 @@ export function formatSkillsForPromptCore(skills: Skill[]): string {
if (skill.locationNote) {
lines.push(` <location_note>${escapeSkillXml(skill.locationNote)}</location_note>`);
}
if (skill.promptVersion) {
lines.push(` <version>${escapeSkillXml(skill.promptVersion)}</version>`);
}
lines.push(" </skill>");
}
lines.push("</available_skills>");
@@ -114,7 +108,6 @@ export function formatSkillsCompactForPrompt(
descriptionMaxChars > 0
? "Use the read tool to load a skill's file when the task matches its name or description."
: "Use the read tool to load a skill's file when the task matches its name.",
"If a skill's <version> differs from a previous turn, re-read its SKILL.md before using it.",
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
"",
"<available_skills>",
@@ -132,9 +125,6 @@ export function formatSkillsCompactForPrompt(
if (skill.locationNote) {
lines.push(` <location_note>${escapeSkillXml(skill.locationNote)}</location_note>`);
}
if (skill.promptVersion) {
lines.push(` <version>${escapeSkillXml(skill.promptVersion)}</version>`);
}
lines.push(" </skill>");
}
lines.push("</available_skills>");
-6
View File
@@ -1,6 +0,0 @@
// Skill prompt versions are deterministic content markers for model-visible skill catalogs.
import { sha256HexPrefixCore } from "../../infra/crypto-digest.js";
export function computeSkillPromptVersion(content: string): string {
return `sha256:${sha256HexPrefixCore(content, 16)}`;
}
@@ -254,21 +254,17 @@ describe("applySkillsPromptLimits (via buildWorkspaceSkillsPrompt)", () => {
expect(prompt.length).toBeLessThanOrEqual(expected.length);
});
it("budgets the final rendered prompt including versions and limit notices", () => {
const skills = Array.from({ length: 24 }, (_, i) => ({
...makeSkill(`skill-${i}`, "A".repeat(160)),
promptVersion: `sha256:${String(i).padStart(16, "0")}`,
}));
it("budgets the final rendered prompt including limit notices", () => {
const skills = Array.from({ length: 24 }, (_, i) => makeSkill(`skill-${i}`, "A".repeat(160)));
const budget = 2_200;
const prompt = buildPrompt(skills, { maxChars: budget });
expect(prompt.length).toBeLessThanOrEqual(budget);
expect(prompt).toContain("<version>sha256:");
expect(prompt).toContain("included");
});
it("keeps no-skill catalogs empty instead of emitting version guidance", () => {
it("keeps no-skill catalogs empty", () => {
const prompt = buildWorkspaceSkillsPrompt("/fake", {
entries: [],
});
@@ -428,33 +428,6 @@ describe("buildSkillSnapshot", () => {
expect(snapshot.prompt).toBe(prompt);
});
it("renders a deterministic version that changes when SKILL.md content changes", async () => {
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
const skillDir = path.join(workspaceDir, "skills", "visible");
await writeSkill({
dir: skillDir,
name: "visible",
description: "Visible",
body: "# Visible\nfirst body\n",
});
const before = buildSnapshot(workspaceDir);
await writeSkill({
dir: skillDir,
name: "visible",
description: "Visible",
body: "# Visible\nsecond body\n",
});
const after = buildSnapshot(workspaceDir);
const beforeVersion = before.prompt.match(/<version>([^<]+)<\/version>/)?.[1];
const afterVersion = after.prompt.match(/<version>([^<]+)<\/version>/)?.[1];
expect(beforeVersion).toMatch(/^sha256:[a-f0-9]{16}$/);
expect(afterVersion).toMatch(/^sha256:[a-f0-9]{16}$/);
expect(afterVersion).not.toBe(beforeVersion);
expect(after.prompt).toContain("If a skill's <version> differs from a previous turn");
});
it("truncates the skills prompt when it exceeds the configured char budget", async () => {
const workspaceDir = await cloneTemplateDir(truncationWorkspaceTemplateDir, "workspace");
@@ -464,7 +437,7 @@ describe("buildSkillSnapshot", () => {
skills: {
limits: {
maxSkillsInPrompt: 100,
maxSkillsPromptChars: 500,
maxSkillsPromptChars: 700,
},
},
},
-2
View File
@@ -3,7 +3,6 @@ import { createSyntheticSourceInfo } from "../../agents/sessions/source-info.js"
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { resolveNodeIdFromNodeList } from "../../shared/node-resolve.js";
import { parseSkillFrontmatter, resolveSkillInvocationPolicy } from "../loading/frontmatter.js";
import { computeSkillPromptVersion } from "../loading/skill-version.js";
import type { ParsedSkillFrontmatter, SkillEntry } from "../types.js";
import { bumpSkillsSnapshotVersion } from "./refresh-state.js";
@@ -236,7 +235,6 @@ export function mergeRemoteNodeSkillEntries(
readContent: skill.content,
filePath,
baseDir: filePath.slice(0, -"/SKILL.md".length),
promptVersion: computeSkillPromptVersion(skill.content),
source: "openclaw-node",
sourceInfo: createSyntheticSourceInfo(filePath, {
source: "openclaw-node",
-2
View File
@@ -8,7 +8,6 @@ export function createCanonicalFixtureSkill(params: {
filePath: string;
baseDir: string;
source: string;
promptVersion?: string;
disableModelInvocation?: boolean;
}): Skill {
return {
@@ -16,7 +15,6 @@ export function createCanonicalFixtureSkill(params: {
description: params.description,
filePath: params.filePath,
baseDir: params.baseDir,
promptVersion: params.promptVersion,
source: params.source,
sourceInfo: createSyntheticSourceInfo(params.filePath, {
source: params.source,
+1 -1
View File
@@ -124,7 +124,7 @@ export type SkillEligibilityContext = {
};
};
export const WORKSPACE_SKILLS_PROMPT_FORMAT_VERSION = 3;
export const WORKSPACE_SKILLS_PROMPT_FORMAT_VERSION = 4;
export type SkillSnapshot = {
prompt: string;