| 1 | /** |
| 2 | * An agent's skills in its instructions (docs.g1t.sh/guides/agent-skills/). |
| 3 | * Pure, so it is tested on its own. |
| 4 | * |
| 5 | * Each foundational skill that is on (@g1t/contracts skills.ts) puts its |
| 6 | * playbook in the system prompt, followed by what it can't do here: the |
| 7 | * abilities whose tools this turn doesn't offer (code tools in a |
| 8 | * conversation whose people can't all read code, say), and the abilities |
| 9 | * that are coming. A skill never adds a tool: the tools offered are the |
| 10 | * tool box's, decided before this runs. |
| 11 | */ |
| 12 | import { type AgentSkill, skillsOn } from "../../../packages/contracts/src/skills.ts"; |
| 13 | |
| 14 | /** "a, b and c". */ |
| 15 | function list(items: string[]): string { |
| 16 | if (items.length <= 1) return items.join(""); |
| 17 | return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`; |
| 18 | } |
| 19 | |
| 20 | /** What one skill says: its playbook, then what isn't available here and what is coming. */ |
| 21 | export function skillBlock(skill: AgentSkill, offered: ReadonlySet<string>): string { |
| 22 | const missingHere = skill.abilities.filter((a) => a.status === "ready" && a.tools.length > 0 && !a.tools.some((tool) => offered.has(tool))); |
| 23 | const coming = skill.abilities.filter((a) => a.status === "coming"); |
| 24 | const lines = [`### ${skill.name}`, "", skill.instructions]; |
| 25 | if (missingHere.length) { |
| 26 | lines.push(`- Not available in this conversation (its tools aren't offered here): ${list(missingHere.map((a) => a.label.toLowerCase()))}. If asked, say you can't do that here.`); |
| 27 | } |
| 28 | if (coming.length) { |
| 29 | lines.push(`- Not yet in g1t: ${list(coming.map((a) => a.label.toLowerCase()))}. If asked, say plainly it isn't available yet and offer what you can do instead; never pretend to have done it.`); |
| 30 | } |
| 31 | return lines.join("\n"); |
| 32 | } |
| 33 | |
| 34 | /** |
| 35 | * The "Your skills" section: every skill that is on, for the tools this |
| 36 | * turn offers. Null when every skill is off. |
| 37 | */ |
| 38 | export function skillsSection(off: readonly string[] | null | undefined, offered: Iterable<string>): string | null { |
| 39 | const on = skillsOn(off); |
| 40 | if (!on.length) return null; |
| 41 | const tools = new Set(offered); |
| 42 | return [ |
| 43 | "## Your skills", |
| 44 | "", |
| 45 | "Playbooks for the work people ask of you, from g1t. They use only the tools you have; they never add one. When a request matches a skill, follow its playbook and deliver the thing itself.", |
| 46 | "", |
| 47 | on.map((skill) => skillBlock(skill, tools)).join("\n\n"), |
| 48 | ].join("\n"); |
| 49 | } |