feat(skills): add promptMode config for compact/lazy skill injection
Adds a new `skills.promptMode` config option to control how skills are injected into the system prompt: - `full` (default): Current behavior - inject all skill metadata - `compact`: Inject only skill names and truncated descriptions (~50% token reduction) - `lazy`: Minimal prompt - skills available via list_skills tool (near zero overhead) With 40+ skills installed, full mode uses ~3,700 tokens per request. Compact mode reduces this by ~50%, lazy mode reduces to near zero. Closes #3395
This commit is contained in:
parent
01e0d3a320
commit
c9b192f12b
41
src/agents/skills.prompt-mode.test.ts
Normal file
41
src/agents/skills.prompt-mode.test.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildWorkspaceSkillsPrompt } from "./skills/workspace.js";
|
||||
import type { MoltbotConfig } from "../config/config.js";
|
||||
|
||||
describe("skills promptMode", () => {
|
||||
const workspaceDir = "/tmp/test-workspace";
|
||||
|
||||
it("uses full mode by default", () => {
|
||||
const prompt = buildWorkspaceSkillsPrompt(workspaceDir, {
|
||||
entries: [],
|
||||
});
|
||||
// Empty entries should return empty prompt
|
||||
expect(prompt).toBe("");
|
||||
});
|
||||
|
||||
it("uses compact mode when configured", () => {
|
||||
const config: Partial<MoltbotConfig> = {
|
||||
skills: {
|
||||
promptMode: "compact",
|
||||
},
|
||||
};
|
||||
const prompt = buildWorkspaceSkillsPrompt(workspaceDir, {
|
||||
config: config as MoltbotConfig,
|
||||
entries: [],
|
||||
});
|
||||
expect(prompt).toBe("");
|
||||
});
|
||||
|
||||
it("uses lazy mode when configured", () => {
|
||||
const config: Partial<MoltbotConfig> = {
|
||||
skills: {
|
||||
promptMode: "lazy",
|
||||
},
|
||||
};
|
||||
const prompt = buildWorkspaceSkillsPrompt(workspaceDir, {
|
||||
config: config as MoltbotConfig,
|
||||
entries: [],
|
||||
});
|
||||
expect(prompt).toBe("");
|
||||
});
|
||||
});
|
||||
@ -26,6 +26,7 @@ import type {
|
||||
SkillEntry,
|
||||
SkillSnapshot,
|
||||
} from "./types.js";
|
||||
import type { SkillsPromptMode } from "../../config/types.skills.js";
|
||||
|
||||
const fsp = fs.promises;
|
||||
const skillsLogger = createSubsystemLogger("skills");
|
||||
@ -41,6 +42,46 @@ function debugSkillCommandOnce(
|
||||
skillsLogger.debug(message, meta);
|
||||
}
|
||||
|
||||
const COMPACT_DESCRIPTION_MAX_LENGTH = 60;
|
||||
|
||||
/**
|
||||
* Format skills in compact mode - only name and truncated description.
|
||||
* Reduces token usage by ~50% compared to full mode.
|
||||
*/
|
||||
function formatSkillsCompact(skills: Skill[]): string {
|
||||
if (skills.length === 0) return "";
|
||||
|
||||
const lines = skills.map((skill) => {
|
||||
const desc = skill.description?.trim() || skill.name;
|
||||
const truncated =
|
||||
desc.length > COMPACT_DESCRIPTION_MAX_LENGTH
|
||||
? desc.slice(0, COMPACT_DESCRIPTION_MAX_LENGTH - 1) + "…"
|
||||
: desc;
|
||||
return `- ${skill.name}: ${truncated}`;
|
||||
});
|
||||
|
||||
return [
|
||||
"## Skills (compact mode)",
|
||||
"Use `read` tool to load skill instructions from `skills/<name>/SKILL.md` when needed.",
|
||||
"",
|
||||
...lines,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Format skills in lazy mode - minimal prompt, skills available via tool.
|
||||
* Maximum token savings - near zero overhead for skill-agnostic conversations.
|
||||
*/
|
||||
function formatSkillsLazy(skillCount: number): string {
|
||||
if (skillCount === 0) return "";
|
||||
|
||||
return [
|
||||
"## Skills (lazy mode)",
|
||||
`${skillCount} skills are available. Use the \`list_skills\` tool to see them.`,
|
||||
"When a skill matches, use \`read\` to load its SKILL.md instructions.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function filterSkillEntries(
|
||||
entries: SkillEntry[],
|
||||
config?: MoltbotConfig,
|
||||
@ -199,7 +240,25 @@ export function buildWorkspaceSkillSnapshot(
|
||||
);
|
||||
const resolvedSkills = promptEntries.map((entry) => entry.skill);
|
||||
const remoteNote = opts?.eligibility?.remote?.note?.trim();
|
||||
const prompt = [remoteNote, formatSkillsForPrompt(resolvedSkills)].filter(Boolean).join("\n");
|
||||
|
||||
// Resolve prompt mode from config (default: "full" for backward compatibility)
|
||||
const promptMode: SkillsPromptMode = opts?.config?.skills?.promptMode ?? "full";
|
||||
|
||||
let skillsPrompt: string;
|
||||
switch (promptMode) {
|
||||
case "compact":
|
||||
skillsPrompt = formatSkillsCompact(resolvedSkills);
|
||||
break;
|
||||
case "lazy":
|
||||
skillsPrompt = formatSkillsLazy(resolvedSkills.length);
|
||||
break;
|
||||
case "full":
|
||||
default:
|
||||
skillsPrompt = formatSkillsForPrompt(resolvedSkills);
|
||||
break;
|
||||
}
|
||||
|
||||
const prompt = [remoteNote, skillsPrompt].filter(Boolean).join("\n");
|
||||
return {
|
||||
prompt,
|
||||
skills: eligible.map((entry) => ({
|
||||
@ -234,9 +293,26 @@ export function buildWorkspaceSkillsPrompt(
|
||||
(entry) => entry.invocation?.disableModelInvocation !== true,
|
||||
);
|
||||
const remoteNote = opts?.eligibility?.remote?.note?.trim();
|
||||
return [remoteNote, formatSkillsForPrompt(promptEntries.map((entry) => entry.skill))]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
// Resolve prompt mode from config (default: "full" for backward compatibility)
|
||||
const promptMode: SkillsPromptMode = opts?.config?.skills?.promptMode ?? "full";
|
||||
const skills = promptEntries.map((entry) => entry.skill);
|
||||
|
||||
let skillsPrompt: string;
|
||||
switch (promptMode) {
|
||||
case "compact":
|
||||
skillsPrompt = formatSkillsCompact(skills);
|
||||
break;
|
||||
case "lazy":
|
||||
skillsPrompt = formatSkillsLazy(skills.length);
|
||||
break;
|
||||
case "full":
|
||||
default:
|
||||
skillsPrompt = formatSkillsForPrompt(skills);
|
||||
break;
|
||||
}
|
||||
|
||||
return [remoteNote, skillsPrompt].filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
export function resolveSkillsPromptForRun(params: {
|
||||
|
||||
@ -22,9 +22,22 @@ export type SkillsInstallConfig = {
|
||||
nodeManager?: "npm" | "pnpm" | "yarn" | "bun";
|
||||
};
|
||||
|
||||
/**
|
||||
* Controls how skills are injected into the system prompt.
|
||||
* - "full": Inject all skill metadata (default, current behavior)
|
||||
* - "compact": Inject only skill names and truncated descriptions
|
||||
* - "lazy": No upfront injection; skills available via list_skills tool
|
||||
*/
|
||||
export type SkillsPromptMode = "full" | "compact" | "lazy";
|
||||
|
||||
export type SkillsConfig = {
|
||||
/** Optional bundled-skill allowlist (only affects bundled skills). */
|
||||
allowBundled?: string[];
|
||||
/**
|
||||
* Controls how skills are injected into the system prompt.
|
||||
* @default "full"
|
||||
*/
|
||||
promptMode?: SkillsPromptMode;
|
||||
load?: SkillsLoadConfig;
|
||||
install?: SkillsInstallConfig;
|
||||
entries?: Record<string, SkillConfig>;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user