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:
|
The prompt is intentionally compact and uses fixed sections:
|
||||||
|
|
||||||
- **Tooling**: current tool list + short descriptions.
|
- **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`.
|
- **Clawdbot Self-Update**: how to run `config.apply` and `update.run`.
|
||||||
- **Workspace**: working directory (`agents.defaults.workspace`).
|
- **Workspace**: working directory (`agents.defaults.workspace`).
|
||||||
- **Workspace Files (injected)**: indicates bootstrap files are included below.
|
- **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**
|
When eligible skills exist, Clawdbot injects a compact **available skills list**
|
||||||
(`formatSkillsForPrompt`) that includes the **file path** for each skill. The
|
(`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
|
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>
|
<available_skills>
|
||||||
|
|||||||
@@ -6,11 +6,9 @@ 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", () => {
|
||||||
@@ -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", () => {
|
describe("applyGoogleTurnOrderingFix", () => {
|
||||||
const makeAssistantFirst = () =>
|
const makeAssistantFirst = () =>
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -89,9 +89,8 @@ import { resolveSandboxContext } from "./sandbox.js";
|
|||||||
import {
|
import {
|
||||||
applySkillEnvOverrides,
|
applySkillEnvOverrides,
|
||||||
applySkillEnvOverridesFromSnapshot,
|
applySkillEnvOverridesFromSnapshot,
|
||||||
buildWorkspaceSkillsPrompt,
|
|
||||||
loadWorkspaceSkillEntries,
|
loadWorkspaceSkillEntries,
|
||||||
type SkillEntry,
|
resolveSkillsPromptForRun,
|
||||||
type SkillSnapshot,
|
type SkillSnapshot,
|
||||||
} from "./skills.js";
|
} from "./skills.js";
|
||||||
import { buildAgentSystemPrompt } from "./system-prompt.js";
|
import { buildAgentSystemPrompt } from "./system-prompt.js";
|
||||||
@@ -622,24 +621,6 @@ 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
|
||||||
@@ -866,9 +847,9 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
skills: skillEntries ?? [],
|
skills: skillEntries ?? [],
|
||||||
config: params.config,
|
config: params.config,
|
||||||
});
|
});
|
||||||
const skillsPrompt = resolveSkillsPrompt({
|
const skillsPrompt = resolveSkillsPromptForRun({
|
||||||
skillsSnapshot: params.skillsSnapshot,
|
skillsSnapshot: params.skillsSnapshot,
|
||||||
skillEntries: shouldLoadSkillEntries ? skillEntries : undefined,
|
entries: shouldLoadSkillEntries ? skillEntries : undefined,
|
||||||
config: params.config,
|
config: params.config,
|
||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
});
|
});
|
||||||
@@ -1196,9 +1177,9 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
skills: skillEntries ?? [],
|
skills: skillEntries ?? [],
|
||||||
config: params.config,
|
config: params.config,
|
||||||
});
|
});
|
||||||
const skillsPrompt = resolveSkillsPrompt({
|
const skillsPrompt = resolveSkillsPromptForRun({
|
||||||
skillsSnapshot: params.skillsSnapshot,
|
skillsSnapshot: params.skillsSnapshot,
|
||||||
skillEntries: shouldLoadSkillEntries ? skillEntries : undefined,
|
entries: shouldLoadSkillEntries ? skillEntries : undefined,
|
||||||
config: params.config,
|
config: params.config,
|
||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
buildWorkspaceSkillSnapshot,
|
buildWorkspaceSkillSnapshot,
|
||||||
buildWorkspaceSkillsPrompt,
|
buildWorkspaceSkillsPrompt,
|
||||||
loadWorkspaceSkillEntries,
|
loadWorkspaceSkillEntries,
|
||||||
|
resolveSkillsPromptForRun,
|
||||||
|
type SkillEntry,
|
||||||
syncSkillsToWorkspace,
|
syncSkillsToWorkspace,
|
||||||
} from "./skills.js";
|
} from "./skills.js";
|
||||||
import { buildWorkspaceSkillStatus } from "./skills-status.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", () => {
|
describe("loadWorkspaceSkillEntries", () => {
|
||||||
it("handles an empty managed skills dir without throwing", async () => {
|
it("handles an empty managed skills dir without throwing", async () => {
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-"));
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-"));
|
||||||
|
|||||||
@@ -610,6 +610,24 @@ export function buildWorkspaceSkillsPrompt(
|
|||||||
return formatSkillsForPrompt(eligible.map((entry) => entry.skill));
|
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(
|
export function loadWorkspaceSkillEntries(
|
||||||
workspaceDir: string,
|
workspaceDir: string,
|
||||||
opts?: {
|
opts?: {
|
||||||
|
|||||||
@@ -83,9 +83,11 @@ describe("buildAgentSystemPrompt", () => {
|
|||||||
expect(prompt).toContain("update.run");
|
expect(prompt).toContain("update.run");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes skills guidance with workspace path", () => {
|
it("includes skills guidance when skills prompt is present", () => {
|
||||||
const prompt = buildAgentSystemPrompt({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
|
skillsPrompt:
|
||||||
|
"<available_skills>\n <skill>\n <name>demo</name>\n </skill>\n</available_skills>",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(prompt).toContain("## Skills");
|
expect(prompt).toContain("## Skills");
|
||||||
@@ -105,6 +107,15 @@ describe("buildAgentSystemPrompt", () => {
|
|||||||
expect(prompt).toContain("<name>demo</name>");
|
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", () => {
|
it("renders project context files when provided", () => {
|
||||||
const prompt = buildAgentSystemPrompt({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
|
|||||||
@@ -139,6 +139,14 @@ export function buildAgentSystemPrompt(params: {
|
|||||||
runtimeProvider === "telegram" &&
|
runtimeProvider === "telegram" &&
|
||||||
runtimeCapabilitiesLower.has("inlinebuttons");
|
runtimeCapabilitiesLower.has("inlinebuttons");
|
||||||
const skillsLines = skillsPrompt ? [skillsPrompt, ""] : [];
|
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 = [
|
const lines = [
|
||||||
"You are a personal assistant running inside Clawdbot.",
|
"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.",
|
"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.",
|
"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",
|
...skillsSection,
|
||||||
"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
|
||||||
? [
|
? [
|
||||||
|
|||||||
Reference in New Issue
Block a user