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, "",