feat: add skill filter + group system prompt plumbing

This commit is contained in:
Peter Steinberger
2026-01-07 11:22:55 +01:00
parent 9bf6684366
commit 61f720b945
6 changed files with 81 additions and 6 deletions

View File

@@ -165,6 +165,33 @@ describe("buildWorkspaceSkillsPrompt", () => {
}
});
it("applies skill filters, including empty lists", async () => {
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-"));
await writeSkill({
dir: path.join(workspaceDir, "skills", "alpha"),
name: "alpha",
description: "Alpha skill",
});
await writeSkill({
dir: path.join(workspaceDir, "skills", "beta"),
name: "beta",
description: "Beta skill",
});
const filteredPrompt = buildWorkspaceSkillsPrompt(workspaceDir, {
managedSkillsDir: path.join(workspaceDir, ".managed"),
skillFilter: ["alpha"],
});
expect(filteredPrompt).toContain("alpha");
expect(filteredPrompt).not.toContain("beta");
const emptyPrompt = buildWorkspaceSkillsPrompt(workspaceDir, {
managedSkillsDir: path.join(workspaceDir, ".managed"),
skillFilter: [],
});
expect(emptyPrompt).toBe("");
});
it("prefers workspace skills over managed skills", async () => {
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-"));
const managedDir = path.join(workspaceDir, ".managed");

View File

@@ -382,8 +382,27 @@ function shouldIncludeSkill(params: {
function filterSkillEntries(
entries: SkillEntry[],
config?: ClawdbotConfig,
skillFilter?: string[],
): SkillEntry[] {
return entries.filter((entry) => shouldIncludeSkill({ entry, config }));
let filtered = entries.filter((entry) =>
shouldIncludeSkill({ entry, config }),
);
// If skillFilter is provided, only include skills in the filter list.
if (skillFilter !== undefined) {
const normalized = skillFilter
.map((entry) => String(entry).trim())
.filter(Boolean);
const label = normalized.length > 0 ? normalized.join(", ") : "(none)";
console.log(`[skills] Applying skill filter: ${label}`);
filtered =
normalized.length > 0
? filtered.filter((entry) => normalized.includes(entry.skill.name))
: [];
console.log(
`[skills] After filter: ${filtered.map((entry) => entry.skill.name).join(", ")}`,
);
}
return filtered;
}
export function applySkillEnvOverrides(params: {
@@ -548,10 +567,16 @@ export function buildWorkspaceSkillSnapshot(
managedSkillsDir?: string;
bundledSkillsDir?: string;
entries?: SkillEntry[];
/** If provided, only include skills with these names */
skillFilter?: string[];
},
): SkillSnapshot {
const skillEntries = opts?.entries ?? loadSkillEntries(workspaceDir, opts);
const eligible = filterSkillEntries(skillEntries, opts?.config);
const eligible = filterSkillEntries(
skillEntries,
opts?.config,
opts?.skillFilter,
);
const resolvedSkills = eligible.map((entry) => entry.skill);
return {
prompt: formatSkillsForPrompt(resolvedSkills),
@@ -570,10 +595,16 @@ export function buildWorkspaceSkillsPrompt(
managedSkillsDir?: string;
bundledSkillsDir?: string;
entries?: SkillEntry[];
/** If provided, only include skills with these names */
skillFilter?: string[];
},
): string {
const skillEntries = opts?.entries ?? loadSkillEntries(workspaceDir, opts);
const eligible = filterSkillEntries(skillEntries, opts?.config);
const eligible = filterSkillEntries(
skillEntries,
opts?.config,
opts?.skillFilter,
);
return formatSkillsForPrompt(eligible.map((entry) => entry.skill));
}

View File

@@ -593,6 +593,10 @@ export async function getReplyFromConfig(
silentToken: SILENT_REPLY_TOKEN,
})
: "";
const groupSystemPrompt = sessionCtx.GroupSystemPrompt?.trim() ?? "";
const extraSystemPrompt = [groupIntro, groupSystemPrompt]
.filter(Boolean)
.join("\n\n");
const baseBody = sessionCtx.BodyStripped ?? sessionCtx.Body ?? "";
const rawBodyTrimmed = (ctx.Body ?? "").trim();
const baseBodyTrimmedRaw = baseBody.trim();
@@ -651,6 +655,7 @@ export async function getReplyFromConfig(
isFirstTurnInSession,
workspaceDir,
cfg,
skillFilter: opts?.skillFilter,
});
sessionEntry = skillResult.sessionEntry ?? sessionEntry;
systemSent = skillResult.systemSent;
@@ -759,7 +764,7 @@ export async function getReplyFromConfig(
blockReplyBreak: resolvedBlockStreamingBreak,
ownerNumbers:
command.ownerList.length > 0 ? command.ownerList : undefined,
extraSystemPrompt: groupIntro || undefined,
extraSystemPrompt: extraSystemPrompt || undefined,
...(provider === "ollama" ? { enforceFinalTag: true } : {}),
},
};

View File

@@ -49,6 +49,8 @@ export async function ensureSkillSnapshot(params: {
isFirstTurnInSession: boolean;
workspaceDir: string;
cfg: ClawdbotConfig;
/** If provided, only load skills with these names (for per-channel skill filtering) */
skillFilter?: string[];
}): Promise<{
sessionEntry?: SessionEntry;
skillsSnapshot?: SessionEntry["skillsSnapshot"];
@@ -63,6 +65,7 @@ export async function ensureSkillSnapshot(params: {
isFirstTurnInSession,
workspaceDir,
cfg,
skillFilter,
} = params;
let nextEntry = sessionEntry;
@@ -76,7 +79,10 @@ export async function ensureSkillSnapshot(params: {
};
const skillSnapshot =
isFirstTurnInSession || !current.skillsSnapshot
? buildWorkspaceSkillSnapshot(workspaceDir, { config: cfg })
? buildWorkspaceSkillSnapshot(workspaceDir, {
config: cfg,
skillFilter,
})
: current.skillsSnapshot;
nextEntry = {
...current,
@@ -96,7 +102,10 @@ export async function ensureSkillSnapshot(params: {
nextEntry?.skillsSnapshot ??
(isFirstTurnInSession
? undefined
: buildWorkspaceSkillSnapshot(workspaceDir, { config: cfg }));
: buildWorkspaceSkillSnapshot(workspaceDir, {
config: cfg,
skillFilter,
}));
if (
skillsSnapshot &&
sessionStore &&

View File

@@ -31,6 +31,7 @@ export type MsgContext = {
GroupRoom?: string;
GroupSpace?: string;
GroupMembers?: string;
GroupSystemPrompt?: string;
SenderName?: string;
SenderId?: string;
SenderUsername?: string;

View File

@@ -9,6 +9,8 @@ export type GetReplyOptions = {
onBlockReply?: (payload: ReplyPayload) => Promise<void> | void;
onToolResult?: (payload: ReplyPayload) => Promise<void> | void;
disableBlockStreaming?: boolean;
/** If provided, only load these skills for this session (empty = no skills). */
skillFilter?: string[];
};
export type ReplyPayload = {