fix: inject skills prompt list
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 = () =>
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user