refactor: centralize skills prompt resolution
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 = () =>
|
||||
[
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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-"));
|
||||
|
||||
@@ -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?: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
? [
|
||||
|
||||
Reference in New Issue
Block a user