fix: inject skills prompt list

This commit is contained in:
Peter Steinberger
2026-01-09 21:20:38 +01:00
parent 0297b38ce0
commit 4861f09f78
6 changed files with 96 additions and 4 deletions

View File

@@ -38,6 +38,7 @@
- Config: support inline env vars in config (`env.*` / `env.vars`) and document env precedence. - Config: support inline env vars in config (`env.*` / `env.vars`) and document env precedence.
- Agent: enable adaptive context pruning by default for tool-result trimming. - Agent: enable adaptive context pruning by default for tool-result trimming.
- Agent: drop empty error assistant messages when sanitizing session history. (#591) — thanks @steipete - Agent: drop empty error assistant messages when sanitizing session history. (#591) — thanks @steipete
- Agent: inject eligible skills list into the system prompt so bundled skills load from their actual locations. (#551) — thanks @gabriel-trigo
- Doctor: check config/state permissions and offer to tighten them. — thanks @steipete - Doctor: check config/state permissions and offer to tighten them. — thanks @steipete
- Doctor/Daemon: audit supervisor configs, add --repair/--force flows, surface service config audits in daemon status, and document user vs system services. — thanks @steipete - Doctor/Daemon: audit supervisor configs, add --repair/--force flows, surface service config audits in daemon status, and document user vs system services. — thanks @steipete
- Doctor: repair gateway service entrypoint when switching between npm and git installs; add Docker e2e coverage. — thanks @steipete - Doctor: repair gateway service entrypoint when switching between npm and git installs; add Docker e2e coverage. — thanks @steipete

View File

@@ -49,10 +49,19 @@ Use `agents.defaults.userTimezone` in `~/.clawdbot/clawdbot.json` to change the
## Skills ## Skills
Skills are **not** auto-injected. Instead, the prompt instructs the model to use `read` to load skill instructions on demand: When eligible skills exist, Clawdbot injects a compact **available skills list**
(`formatSkillsForPrompt`) that includes 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).
``` ```
<workspace>/skills/<name>/SKILL.md <available_skills>
<skill>
<name>...</name>
<description>...</description>
<location>...</location>
</skill>
</available_skills>
``` ```
This keeps the base prompt small while still enabling targeted skill usage. This keeps the base prompt small while still enabling targeted skill usage.

View File

@@ -6,9 +6,11 @@ import {
applyGoogleTurnOrderingFix, applyGoogleTurnOrderingFix,
buildEmbeddedSandboxInfo, buildEmbeddedSandboxInfo,
createSystemPromptOverride, createSystemPromptOverride,
resolveSkillsPrompt,
splitSdkTools, splitSdkTools,
} from "./pi-embedded-runner.js"; } from "./pi-embedded-runner.js";
import type { SandboxContext } from "./sandbox.js"; import type { SandboxContext } from "./sandbox.js";
import type { SkillEntry } from "./skills.js";
describe("buildEmbeddedSandboxInfo", () => { describe("buildEmbeddedSandboxInfo", () => {
it("returns undefined when sandbox is missing", () => { it("returns undefined when sandbox is missing", () => {
@@ -122,6 +124,35 @@ describe("createSystemPromptOverride", () => {
}); });
}); });
describe("resolveSkillsPrompt", () => {
it("prefers snapshot prompt when available", () => {
const prompt = resolveSkillsPrompt({
skillsSnapshot: { prompt: "SNAPSHOT", skills: [] },
workspaceDir: "/tmp/clawd",
});
expect(prompt).toBe("SNAPSHOT");
});
it("builds prompt from entries when snapshot is missing", () => {
const entry: SkillEntry = {
skill: {
name: "demo-skill",
description: "Demo",
filePath: "/app/skills/demo-skill/SKILL.md",
baseDir: "/app/skills/demo-skill",
source: "clawdbot-bundled",
},
frontmatter: {},
};
const prompt = resolveSkillsPrompt({
skillEntries: [entry],
workspaceDir: "/tmp/clawd",
});
expect(prompt).toContain("<available_skills>");
expect(prompt).toContain("/app/skills/demo-skill/SKILL.md");
});
});
describe("applyGoogleTurnOrderingFix", () => { describe("applyGoogleTurnOrderingFix", () => {
const makeAssistantFirst = () => const makeAssistantFirst = () =>
[ [

View File

@@ -89,7 +89,9 @@ import { resolveSandboxContext } from "./sandbox.js";
import { import {
applySkillEnvOverrides, applySkillEnvOverrides,
applySkillEnvOverridesFromSnapshot, applySkillEnvOverridesFromSnapshot,
buildWorkspaceSkillsPrompt,
loadWorkspaceSkillEntries, loadWorkspaceSkillEntries,
type SkillEntry,
type SkillSnapshot, type SkillSnapshot,
} from "./skills.js"; } from "./skills.js";
import { buildAgentSystemPrompt } from "./system-prompt.js"; import { buildAgentSystemPrompt } from "./system-prompt.js";
@@ -578,6 +580,7 @@ function buildEmbeddedSystemPrompt(params: {
ownerNumbers?: string[]; ownerNumbers?: string[];
reasoningTagHint: boolean; reasoningTagHint: boolean;
heartbeatPrompt?: string; heartbeatPrompt?: string;
skillsPrompt?: string;
runtimeInfo: { runtimeInfo: {
host: string; host: string;
os: string; os: string;
@@ -601,6 +604,7 @@ function buildEmbeddedSystemPrompt(params: {
ownerNumbers: params.ownerNumbers, ownerNumbers: params.ownerNumbers,
reasoningTagHint: params.reasoningTagHint, reasoningTagHint: params.reasoningTagHint,
heartbeatPrompt: params.heartbeatPrompt, heartbeatPrompt: params.heartbeatPrompt,
skillsPrompt: params.skillsPrompt,
runtimeInfo: params.runtimeInfo, runtimeInfo: params.runtimeInfo,
sandboxInfo: params.sandboxInfo, sandboxInfo: params.sandboxInfo,
toolNames: params.tools.map((tool) => tool.name), toolNames: params.tools.map((tool) => tool.name),
@@ -618,6 +622,24 @@ export function createSystemPromptOverride(
return () => trimmed; return () => trimmed;
} }
export function resolveSkillsPrompt(params: {
skillsSnapshot?: SkillSnapshot;
skillEntries?: SkillEntry[];
config?: ClawdbotConfig;
workspaceDir: string;
}): string {
const snapshotPrompt = params.skillsSnapshot?.prompt?.trim();
if (snapshotPrompt) return snapshotPrompt;
if (params.skillEntries && params.skillEntries.length > 0) {
const prompt = buildWorkspaceSkillsPrompt(params.workspaceDir, {
entries: params.skillEntries,
config: params.config,
});
return prompt.trim() ? prompt : "";
}
return "";
}
// Tool names are now capitalized (Bash, Read, Write, Edit) to bypass Anthropic's // Tool names are now capitalized (Bash, Read, Write, Edit) to bypass Anthropic's
// OAuth token blocking of lowercase names. However, pi-coding-agent's SDK has // OAuth token blocking of lowercase names. However, pi-coding-agent's SDK has
// hardcoded lowercase names in its built-in tool registry, so we must pass ALL // hardcoded lowercase names in its built-in tool registry, so we must pass ALL
@@ -844,6 +866,12 @@ export async function compactEmbeddedPiSession(params: {
skills: skillEntries ?? [], skills: skillEntries ?? [],
config: params.config, config: params.config,
}); });
const skillsPrompt = resolveSkillsPrompt({
skillsSnapshot: params.skillsSnapshot,
skillEntries: shouldLoadSkillEntries ? skillEntries : undefined,
config: params.config,
workspaceDir: effectiveWorkspace,
});
const bootstrapFiles = const bootstrapFiles =
await loadWorkspaceBootstrapFiles(effectiveWorkspace); await loadWorkspaceBootstrapFiles(effectiveWorkspace);
@@ -895,6 +923,7 @@ export async function compactEmbeddedPiSession(params: {
heartbeatPrompt: resolveHeartbeatPrompt( heartbeatPrompt: resolveHeartbeatPrompt(
params.config?.agents?.defaults?.heartbeat?.prompt, params.config?.agents?.defaults?.heartbeat?.prompt,
), ),
skillsPrompt,
runtimeInfo, runtimeInfo,
sandboxInfo, sandboxInfo,
tools, tools,
@@ -1167,6 +1196,12 @@ export async function runEmbeddedPiAgent(params: {
skills: skillEntries ?? [], skills: skillEntries ?? [],
config: params.config, config: params.config,
}); });
const skillsPrompt = resolveSkillsPrompt({
skillsSnapshot: params.skillsSnapshot,
skillEntries: shouldLoadSkillEntries ? skillEntries : undefined,
config: params.config,
workspaceDir: effectiveWorkspace,
});
const bootstrapFiles = const bootstrapFiles =
await loadWorkspaceBootstrapFiles(effectiveWorkspace); await loadWorkspaceBootstrapFiles(effectiveWorkspace);
@@ -1208,6 +1243,7 @@ export async function runEmbeddedPiAgent(params: {
heartbeatPrompt: resolveHeartbeatPrompt( heartbeatPrompt: resolveHeartbeatPrompt(
params.config?.agents?.defaults?.heartbeat?.prompt, params.config?.agents?.defaults?.heartbeat?.prompt,
), ),
skillsPrompt,
runtimeInfo, runtimeInfo,
sandboxInfo, sandboxInfo,
tools, tools,

View File

@@ -90,10 +90,21 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).toContain("## Skills"); expect(prompt).toContain("## Skills");
expect(prompt).toContain( expect(prompt).toContain(
"Use `read` to load from /tmp/clawd/skills/<name>/SKILL.md", "Use `read` to load the SKILL.md at the location listed for that skill.",
); );
}); });
it("appends available skills when provided", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd",
skillsPrompt:
"<available_skills>\n <skill>\n <name>demo</name>\n </skill>\n</available_skills>",
});
expect(prompt).toContain("<available_skills>");
expect(prompt).toContain("<name>demo</name>");
});
it("renders project context files when provided", () => { it("renders project context files when provided", () => {
const prompt = buildAgentSystemPrompt({ const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd", workspaceDir: "/tmp/clawd",

View File

@@ -12,6 +12,7 @@ export function buildAgentSystemPrompt(params: {
userTimezone?: string; userTimezone?: string;
userTime?: string; userTime?: string;
contextFiles?: EmbeddedContextFile[]; contextFiles?: EmbeddedContextFile[];
skillsPrompt?: string;
heartbeatPrompt?: string; heartbeatPrompt?: string;
runtimeInfo?: { runtimeInfo?: {
host?: string; host?: string;
@@ -121,6 +122,7 @@ export function buildAgentSystemPrompt(params: {
: undefined; : undefined;
const userTimezone = params.userTimezone?.trim(); const userTimezone = params.userTimezone?.trim();
const userTime = params.userTime?.trim(); const userTime = params.userTime?.trim();
const skillsPrompt = params.skillsPrompt?.trim();
const heartbeatPrompt = params.heartbeatPrompt?.trim(); const heartbeatPrompt = params.heartbeatPrompt?.trim();
const heartbeatPromptLine = heartbeatPrompt const heartbeatPromptLine = heartbeatPrompt
? `Heartbeat prompt: ${heartbeatPrompt}` ? `Heartbeat prompt: ${heartbeatPrompt}`
@@ -136,6 +138,7 @@ export function buildAgentSystemPrompt(params: {
const telegramInlineButtonsEnabled = const telegramInlineButtonsEnabled =
runtimeProvider === "telegram" && runtimeProvider === "telegram" &&
runtimeCapabilitiesLower.has("inlinebuttons"); runtimeCapabilitiesLower.has("inlinebuttons");
const skillsLines = skillsPrompt ? [skillsPrompt, ""] : [];
const lines = [ const lines = [
"You are a personal assistant running inside Clawdbot.", "You are a personal assistant running inside Clawdbot.",
@@ -164,7 +167,8 @@ export function buildAgentSystemPrompt(params: {
"If a task is more complex or takes longer, spawn a sub-agent. It will do the work for you and ping you when it's done. You can always check up on it.", "If a task is more complex or takes longer, spawn a sub-agent. It will do the work for you and ping you when it's done. You can always check up on it.",
"", "",
"## Skills", "## Skills",
`Skills provide task-specific instructions. Use \`read\` to load from ${params.workspaceDir}/skills/<name>/SKILL.md when needed.`, "Skills provide task-specific instructions. Use `read` to load the SKILL.md at the location listed for that skill.",
...skillsLines,
"", "",
hasGateway ? "## Clawdbot Self-Update" : "", hasGateway ? "## Clawdbot Self-Update" : "",
hasGateway hasGateway