feat: add skill filter + group system prompt plumbing
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -31,6 +31,7 @@ export type MsgContext = {
|
||||
GroupRoom?: string;
|
||||
GroupSpace?: string;
|
||||
GroupMembers?: string;
|
||||
GroupSystemPrompt?: string;
|
||||
SenderName?: string;
|
||||
SenderId?: string;
|
||||
SenderUsername?: string;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user