diff --git a/docs/concepts/system-prompt.md b/docs/concepts/system-prompt.md index 484b08a17..165dd070a 100644 --- a/docs/concepts/system-prompt.md +++ b/docs/concepts/system-prompt.md @@ -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. ``` diff --git a/src/agents/pi-embedded-runner.test.ts b/src/agents/pi-embedded-runner.test.ts index 70085ce0b..fb102092e 100644 --- a/src/agents/pi-embedded-runner.test.ts +++ b/src/agents/pi-embedded-runner.test.ts @@ -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(""); - expect(prompt).toContain("/app/skills/demo-skill/SKILL.md"); - }); -}); - describe("applyGoogleTurnOrderingFix", () => { const makeAssistantFirst = () => [ diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index 6f494956b..ce710d1f1 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -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, }); diff --git a/src/agents/skills.test.ts b/src/agents/skills.test.ts index b1ec8b60c..c14adb14f 100644 --- a/src/agents/skills.test.ts +++ b/src/agents/skills.test.ts @@ -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(""); + 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-")); diff --git a/src/agents/skills.ts b/src/agents/skills.ts index 405fd6beb..32fd071f7 100644 --- a/src/agents/skills.ts +++ b/src/agents/skills.ts @@ -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?: { diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index 37acde766..09b7c95cf 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -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: + "\n \n demo\n \n", }); expect(prompt).toContain("## Skills"); @@ -105,6 +107,15 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("demo"); }); + 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(""); + }); + it("renders project context files when provided", () => { const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/clawd", diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 9ff3a069e..4bac02e40 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -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 ? [