refactor: centralize skills prompt resolution

This commit is contained in:
Peter Steinberger
2026-01-09 21:27:11 +01:00
parent cf8d7139e1
commit 24605379b9
7 changed files with 78 additions and 62 deletions

View File

@@ -15,7 +15,7 @@ The prompt is assembled by Clawdbot and injected into each agent run.
The prompt is intentionally compact and uses fixed sections:
- **Tooling**: current tool list + short descriptions.
- **Skills**: tells the model how to load skill instructions on demand.
- **Skills** (when available): tells the model how to load skill instructions on demand.
- **Clawdbot Self-Update**: how to run `config.apply` and `update.run`.
- **Workspace**: working directory (`agents.defaults.workspace`).
- **Workspace Files (injected)**: indicates bootstrap files are included below.
@@ -52,7 +52,8 @@ Use `agents.defaults.userTimezone` in `~/.clawdbot/clawdbot.json` to change the
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).
location (workspace, managed, or bundled). If no skills are eligible, the
Skills section is omitted.
```
<available_skills>

View File

@@ -6,11 +6,9 @@ import {
applyGoogleTurnOrderingFix,
buildEmbeddedSandboxInfo,
createSystemPromptOverride,
resolveSkillsPrompt,
splitSdkTools,
} from "./pi-embedded-runner.js";
import type { SandboxContext } from "./sandbox.js";
import type { SkillEntry } from "./skills.js";
describe("buildEmbeddedSandboxInfo", () => {
it("returns undefined when sandbox is missing", () => {
@@ -124,35 +122,6 @@ 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", () => {
const makeAssistantFirst = () =>
[

View File

@@ -89,9 +89,8 @@ import { resolveSandboxContext } from "./sandbox.js";
import {
applySkillEnvOverrides,
applySkillEnvOverridesFromSnapshot,
buildWorkspaceSkillsPrompt,
loadWorkspaceSkillEntries,
type SkillEntry,
resolveSkillsPromptForRun,
type SkillSnapshot,
} from "./skills.js";
import { buildAgentSystemPrompt } from "./system-prompt.js";
@@ -622,24 +621,6 @@ export function createSystemPromptOverride(
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
// 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
@@ -866,9 +847,9 @@ export async function compactEmbeddedPiSession(params: {
skills: skillEntries ?? [],
config: params.config,
});
const skillsPrompt = resolveSkillsPrompt({
const skillsPrompt = resolveSkillsPromptForRun({
skillsSnapshot: params.skillsSnapshot,
skillEntries: shouldLoadSkillEntries ? skillEntries : undefined,
entries: shouldLoadSkillEntries ? skillEntries : undefined,
config: params.config,
workspaceDir: effectiveWorkspace,
});
@@ -1196,9 +1177,9 @@ export async function runEmbeddedPiAgent(params: {
skills: skillEntries ?? [],
config: params.config,
});
const skillsPrompt = resolveSkillsPrompt({
const skillsPrompt = resolveSkillsPromptForRun({
skillsSnapshot: params.skillsSnapshot,
skillEntries: shouldLoadSkillEntries ? skillEntries : undefined,
entries: shouldLoadSkillEntries ? skillEntries : undefined,
config: params.config,
workspaceDir: effectiveWorkspace,
});

View File

@@ -10,6 +10,8 @@ import {
buildWorkspaceSkillSnapshot,
buildWorkspaceSkillsPrompt,
loadWorkspaceSkillEntries,
resolveSkillsPromptForRun,
type SkillEntry,
syncSkillsToWorkspace,
} from "./skills.js";
import { buildWorkspaceSkillStatus } from "./skills-status.js";
@@ -404,6 +406,35 @@ describe("buildWorkspaceSkillsPrompt", () => {
});
});
describe("resolveSkillsPromptForRun", () => {
it("prefers snapshot prompt when available", () => {
const prompt = resolveSkillsPromptForRun({
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 = resolveSkillsPromptForRun({
entries: [entry],
workspaceDir: "/tmp/clawd",
});
expect(prompt).toContain("<available_skills>");
expect(prompt).toContain("/app/skills/demo-skill/SKILL.md");
});
});
describe("loadWorkspaceSkillEntries", () => {
it("handles an empty managed skills dir without throwing", async () => {
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-"));

View File

@@ -610,6 +610,24 @@ export function buildWorkspaceSkillsPrompt(
return formatSkillsForPrompt(eligible.map((entry) => entry.skill));
}
export function resolveSkillsPromptForRun(params: {
skillsSnapshot?: SkillSnapshot;
entries?: SkillEntry[];
config?: ClawdbotConfig;
workspaceDir: string;
}): string {
const snapshotPrompt = params.skillsSnapshot?.prompt?.trim();
if (snapshotPrompt) return snapshotPrompt;
if (params.entries && params.entries.length > 0) {
const prompt = buildWorkspaceSkillsPrompt(params.workspaceDir, {
entries: params.entries,
config: params.config,
});
return prompt.trim() ? prompt : "";
}
return "";
}
export function loadWorkspaceSkillEntries(
workspaceDir: string,
opts?: {

View File

@@ -83,9 +83,11 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).toContain("update.run");
});
it("includes skills guidance with workspace path", () => {
it("includes skills guidance when skills prompt is present", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd",
skillsPrompt:
"<available_skills>\n <skill>\n <name>demo</name>\n </skill>\n</available_skills>",
});
expect(prompt).toContain("## Skills");
@@ -105,6 +107,15 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).toContain("<name>demo</name>");
});
it("omits skills section when no skills prompt is provided", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd",
});
expect(prompt).not.toContain("## Skills");
expect(prompt).not.toContain("<available_skills>");
});
it("renders project context files when provided", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd",

View File

@@ -139,6 +139,14 @@ export function buildAgentSystemPrompt(params: {
runtimeProvider === "telegram" &&
runtimeCapabilitiesLower.has("inlinebuttons");
const skillsLines = skillsPrompt ? [skillsPrompt, ""] : [];
const skillsSection = skillsPrompt
? [
"## Skills",
"Skills provide task-specific instructions. Use `read` to load the SKILL.md at the location listed for that skill.",
...skillsLines,
"",
]
: [];
const lines = [
"You are a personal assistant running inside Clawdbot.",
@@ -166,10 +174,7 @@ export function buildAgentSystemPrompt(params: {
"TOOLS.md does not control tool availability; it is user guidance for how to use external tools.",
"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 provide task-specific instructions. Use `read` to load the SKILL.md at the location listed for that skill.",
...skillsLines,
"",
...skillsSection,
hasGateway ? "## Clawdbot Self-Update" : "",
hasGateway
? [