From 6f67bb77b8e9c140e7a3eab3acd6ff01d30348da Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sun, 15 Feb 2026 13:26:06 +0800 Subject: [PATCH] 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 --- packages/core/src/agent/skills/eligibility.ts | 12 ++ packages/core/src/agent/skills/index.ts | 133 +++++++++++++----- .../src/agent/system-prompt/sections.test.ts | 7 + .../core/src/agent/system-prompt/sections.ts | 3 +- 4 files changed, 121 insertions(+), 34 deletions(-) diff --git a/packages/core/src/agent/skills/eligibility.ts b/packages/core/src/agent/skills/eligibility.ts index a89b44a9..8f5cb971 100644 --- a/packages/core/src/agent/skills/eligibility.ts +++ b/packages/core/src/agent/skills/eligibility.ts @@ -470,6 +470,7 @@ function generateEnvHint(envVars: string[], skill: Skill): string { */ function getApiKeyHint(envVar: string): string | null { const keyHints: Record = { + // 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; diff --git a/packages/core/src/agent/skills/index.ts b/packages/core/src/agent/skills/index.ts index 72c58962..e7343eb2 100644 --- a/packages/core/src/agent/skills/index.ts +++ b/packages/core/src/agent/skills/index.ts @@ -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(); 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"); + } } diff --git a/packages/core/src/agent/system-prompt/sections.test.ts b/packages/core/src/agent/system-prompt/sections.test.ts index df446356..5ac3bfe0 100644 --- a/packages/core/src/agent/system-prompt/sections.test.ts +++ b/packages/core/src/agent/system-prompt/sections.test.ts @@ -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([]); }); diff --git a/packages/core/src/agent/system-prompt/sections.ts b/packages/core/src/agent/system-prompt/sections.ts index 60caee95..05e53a89 100644 --- a/packages/core/src/agent/system-prompt/sections.ts +++ b/packages/core/src/agent/system-prompt/sections.ts @@ -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, "",