mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-22 02:15:26 -06:00
fix(skills): omit content hashes from prompts (#126951)
This commit is contained in:
@@ -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>
|
||||
```
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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>");
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user