feat(skills): expose ineligible skills in system prompt for auto-discovery
Add buildIneligibleSkillsSummary() to SkillManager that surfaces skills with actionable issues (missing env vars, binaries) in the agent's system prompt. Expand getApiKeyHint() with common service API providers. Update buildSkillsSection() to guide the agent to suggest activating inactive skills when they match user intent. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0678431a7d
commit
6f67bb77b8
4 changed files with 121 additions and 34 deletions
|
|
@ -470,6 +470,7 @@ function generateEnvHint(envVars: string[], skill: Skill): string {
|
|||
*/
|
||||
function getApiKeyHint(envVar: string): string | null {
|
||||
const keyHints: Record<string, string> = {
|
||||
// LLM providers
|
||||
OPENAI_API_KEY: "Get from: platform.openai.com/api-keys",
|
||||
ANTHROPIC_API_KEY: "Get from: console.anthropic.com",
|
||||
GOOGLE_API_KEY: "Get from: console.cloud.google.com",
|
||||
|
|
@ -478,6 +479,17 @@ function getApiKeyHint(envVar: string): string | null {
|
|||
GROQ_API_KEY: "Get from: console.groq.com",
|
||||
MISTRAL_API_KEY: "Get from: console.mistral.ai",
|
||||
TOGETHER_API_KEY: "Get from: api.together.xyz",
|
||||
// Common service APIs
|
||||
GMAIL_API_KEY: "Get from: console.cloud.google.com (enable Gmail API)",
|
||||
GITHUB_TOKEN: "Get from: github.com/settings/tokens",
|
||||
SLACK_BOT_TOKEN: "Get from: api.slack.com/apps",
|
||||
NOTION_API_KEY: "Get from: notion.so/my-integrations",
|
||||
LINEAR_API_KEY: "Get from: linear.app/settings/api",
|
||||
JIRA_API_TOKEN: "Get from: id.atlassian.com/manage-profile/security/api-tokens",
|
||||
SENDGRID_API_KEY: "Get from: app.sendgrid.com/settings/api_keys",
|
||||
STRIPE_API_KEY: "Get from: dashboard.stripe.com/apikeys",
|
||||
TWILIO_API_KEY: "Get from: console.twilio.com",
|
||||
FINANCIAL_DATASETS_API_KEY: "Get from: financialdatasets.ai",
|
||||
};
|
||||
|
||||
return keyHints[envVar] ?? null;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import { loadAllSkills, getProfileSkillsDir, initializeManagedSkills, getManaged
|
|||
import {
|
||||
filterEligibleSkills,
|
||||
checkEligibility,
|
||||
checkEligibilityDetailed,
|
||||
type EligibilityContext,
|
||||
type DiagnosticItem,
|
||||
} from "./eligibility.js";
|
||||
import {
|
||||
startSkillsWatcher,
|
||||
|
|
@ -316,27 +318,31 @@ export class SkillManager {
|
|||
buildSkillsPrompt(): string {
|
||||
this.ensureLoaded();
|
||||
|
||||
if (this.eligibleSkills!.size === 0) {
|
||||
return "";
|
||||
const parts: string[] = [];
|
||||
|
||||
if (this.eligibleSkills!.size > 0) {
|
||||
parts.push("# Available Skills\n");
|
||||
parts.push("You have access to the following skills:\n");
|
||||
|
||||
for (const [id, skill] of this.eligibleSkills!) {
|
||||
const emoji = skill.frontmatter.metadata?.emoji ?? "🔧";
|
||||
const name = skill.frontmatter.name;
|
||||
const desc = skill.frontmatter.description ?? "No description provided";
|
||||
|
||||
parts.push(`## ${emoji} ${name} (${id})`);
|
||||
parts.push(`${desc}\n`);
|
||||
|
||||
// Include full instructions
|
||||
if (skill.instructions) {
|
||||
parts.push(skill.instructions);
|
||||
parts.push("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
parts.push("# Available Skills\n");
|
||||
parts.push("You have access to the following skills:\n");
|
||||
|
||||
for (const [id, skill] of this.eligibleSkills!) {
|
||||
const emoji = skill.frontmatter.metadata?.emoji ?? "🔧";
|
||||
const name = skill.frontmatter.name;
|
||||
const desc = skill.frontmatter.description ?? "No description provided";
|
||||
|
||||
parts.push(`## ${emoji} ${name} (${id})`);
|
||||
parts.push(`${desc}\n`);
|
||||
|
||||
// Include full instructions
|
||||
if (skill.instructions) {
|
||||
parts.push(skill.instructions);
|
||||
parts.push("");
|
||||
}
|
||||
const ineligibleSummary = this.buildIneligibleSkillsSummary();
|
||||
if (ineligibleSummary) {
|
||||
parts.push(ineligibleSummary);
|
||||
}
|
||||
|
||||
return parts.join("\n");
|
||||
|
|
@ -491,6 +497,8 @@ export class SkillManager {
|
|||
buildModelSkillsPrompt(): string {
|
||||
this.ensureLoaded();
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
const modelSkills = new Map<string, Skill>();
|
||||
for (const [id, skill] of this.eligibleSkills!) {
|
||||
if (isModelInvocable(skill)) {
|
||||
|
|
@ -498,24 +506,27 @@ export class SkillManager {
|
|||
}
|
||||
}
|
||||
|
||||
if (modelSkills.size === 0) {
|
||||
return "";
|
||||
if (modelSkills.size > 0) {
|
||||
parts.push("# Available Skills\n");
|
||||
parts.push("You have access to the following skills. When you need to use a skill, the full instructions will be provided.\n");
|
||||
|
||||
for (const [id, skill] of modelSkills) {
|
||||
const emoji = skill.frontmatter.metadata?.emoji ?? "🔧";
|
||||
const name = skill.frontmatter.name;
|
||||
const desc = skill.frontmatter.description ?? "No description provided";
|
||||
|
||||
// Progressive loading: only output metadata, not full instructions
|
||||
parts.push(`- ${emoji} **${name}** (\`${id}\`): ${desc}`);
|
||||
}
|
||||
|
||||
parts.push("");
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
parts.push("# Available Skills\n");
|
||||
parts.push("You have access to the following skills. When you need to use a skill, the full instructions will be provided.\n");
|
||||
|
||||
for (const [id, skill] of modelSkills) {
|
||||
const emoji = skill.frontmatter.metadata?.emoji ?? "🔧";
|
||||
const name = skill.frontmatter.name;
|
||||
const desc = skill.frontmatter.description ?? "No description provided";
|
||||
|
||||
// Progressive loading: only output metadata, not full instructions
|
||||
parts.push(`- ${emoji} **${name}** (\`${id}\`): ${desc}`);
|
||||
const ineligibleSummary = this.buildIneligibleSkillsSummary();
|
||||
if (ineligibleSummary) {
|
||||
parts.push(ineligibleSummary);
|
||||
}
|
||||
|
||||
parts.push("");
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
|
|
@ -547,4 +558,60 @@ export class SkillManager {
|
|||
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a compact summary of ineligible skills for system prompt awareness.
|
||||
*
|
||||
* Only includes skills with actionable issues (missing env vars, binaries, config).
|
||||
* Excludes platform-incompatible or disabled skills since the agent cannot fix those.
|
||||
*/
|
||||
buildIneligibleSkillsSummary(): string {
|
||||
this.ensureLoaded();
|
||||
|
||||
const actionable: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
diagnostics: DiagnosticItem[];
|
||||
}> = [];
|
||||
|
||||
for (const [id, skill] of this.skills!) {
|
||||
if (this.eligibleSkills!.has(id)) continue;
|
||||
|
||||
const result = checkEligibilityDetailed(skill, this.getEligibilityContext());
|
||||
if (!result.diagnostics) continue;
|
||||
|
||||
const actionableDiagnostics = result.diagnostics.filter(
|
||||
(d) => d.type === "env" || d.type === "binary" || d.type === "any_binary" || d.type === "config",
|
||||
);
|
||||
|
||||
if (actionableDiagnostics.length === 0) continue;
|
||||
|
||||
actionable.push({
|
||||
id,
|
||||
name: skill.frontmatter.name,
|
||||
description: skill.frontmatter.description ?? "",
|
||||
diagnostics: actionableDiagnostics,
|
||||
});
|
||||
}
|
||||
|
||||
if (actionable.length === 0) return "";
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push("# Installed But Inactive Skills\n");
|
||||
lines.push("These skills are installed but not yet ready. You can help the user activate them.\n");
|
||||
|
||||
for (const s of actionable) {
|
||||
lines.push(`- **${s.name}** (\`${s.id}\`): ${s.description}`);
|
||||
for (const d of s.diagnostics) {
|
||||
lines.push(` - ${d.message}`);
|
||||
if (d.hint) {
|
||||
lines.push(` - Fix: ${d.hint}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,6 +208,13 @@ describe("buildSkillsSection", () => {
|
|||
expect(text).toContain("## commit");
|
||||
});
|
||||
|
||||
it("includes inactive skill guidance", () => {
|
||||
const result = buildSkillsSection("## commit\nDo commits.", "full");
|
||||
const text = result.join("\n");
|
||||
expect(text).toContain("inactive skill");
|
||||
expect(text).toContain("suggest activating it");
|
||||
});
|
||||
|
||||
it("returns empty in minimal mode", () => {
|
||||
expect(buildSkillsSection("skills", "minimal")).toEqual([]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -371,7 +371,8 @@ export function buildSkillsSection(
|
|||
"Before replying: scan the available skills below.",
|
||||
"- If exactly one skill clearly applies: follow its instructions.",
|
||||
"- If multiple could apply: choose the most specific one.",
|
||||
"- If none clearly apply: skip skill invocation.",
|
||||
"- If none clearly apply but an **inactive skill** matches the user's intent: suggest activating it.",
|
||||
"- If no skill matches at all: skip skill invocation.",
|
||||
"",
|
||||
trimmed,
|
||||
"",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue