-
-
Notifications
You must be signed in to change notification settings - Fork 311
feat(ai-skills): add @tanstack/ai-skills — portable Agent Skills middleware #1236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f9525a2
c456b75
55b5322
57f268b
146b621
f11fd35
fa120d1
cbae084
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| --- | ||
| '@tanstack/ai-skills': minor | ||
| '@tanstack/ai': minor | ||
| '@tanstack/ai-anthropic': patch | ||
| '@tanstack/openai-base': patch | ||
| '@tanstack/ai-sandbox': patch | ||
| '@tanstack/ai-client': patch | ||
| '@tanstack/ai-event-client': patch | ||
| '@tanstack/ai-devtools-core': patch | ||
| --- | ||
|
|
||
| Add `@tanstack/ai-skills`: portable Agent Skills (`SKILL.md`) as a first-class `chat()` middleware. | ||
|
|
||
| `withSkills(sources, options?)` renders a skill catalog and a `load_skill` tool so any tool-calling model can load skills on demand, on any provider, with no server sandbox. Skills come from `inlineSkill`, `skillDirectory` (`/node`), or a build-time `staticSkills` bundle, and compose via `aggregate`/`dedupe`/`filter`/`cache`. `createResourceTool` exposes a skill's bundled files through `read_skill_resource`, and `runSkillSourceConformance` (`/testing`) validates custom `SkillSource` adapters. The catalog renders as `<available_skills>` XML for Anthropic models and markdown for others; portable and hosted (native) skills refuse to combine in one call. | ||
|
|
||
| Core `@tanstack/ai` now exports `SkillLimitError`. The native factories throw it (or add validation): `codeExecutionTool` (`@tanstack/ai-anthropic`) frames its 8-skill cap, and `shellTool` (`@tanstack/openai-base`) now validates `skill_id` format instead of nothing. `@tanstack/ai-sandbox` reuses the shared skill-directory walk from `@tanstack/ai-skills`. | ||
|
|
||
| `withSkills` sends a `skills:state` CUSTOM chunk so TanStack AI DevTools can show the catalog and which skills the model loaded. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -61,7 +61,7 @@ | |||||
| "label": "Agent Skills (TanStack Intent)", | ||||||
| "to": "getting-started/agent-skills", | ||||||
| "addedAt": "2026-04-17", | ||||||
| "updatedAt": "2026-07-26" | ||||||
| "updatedAt": "2026-08-23" | ||||||
| } | ||||||
| ] | ||||||
| }, | ||||||
|
|
@@ -94,7 +94,8 @@ | |||||
| { | ||||||
| "label": "Provider Skills", | ||||||
| "to": "tools/provider-skills", | ||||||
| "addedAt": "2026-06-04" | ||||||
| "addedAt": "2026-06-04", | ||||||
| "updatedAt": "2026-08-22" | ||||||
| }, | ||||||
| { | ||||||
| "label": "Tool Architecture", | ||||||
|
|
@@ -128,6 +129,27 @@ | |||||
| } | ||||||
| ] | ||||||
| }, | ||||||
| { | ||||||
| "label": "Skills", | ||||||
| "children": [ | ||||||
| { | ||||||
| "label": "Portable Agent Skills", | ||||||
| "to": "skills/agent-skills", | ||||||
| "addedAt": "2026-08-22", | ||||||
| "updatedAt": "2026-08-23" | ||||||
| }, | ||||||
| { | ||||||
| "label": "Skill Sources", | ||||||
| "to": "skills/skill-sources", | ||||||
| "addedAt": "2026-08-22" | ||||||
| }, | ||||||
| { | ||||||
| "label": "Write a Skill Source", | ||||||
| "to": "skills/writing-adapters", | ||||||
| "addedAt": "2026-08-22" | ||||||
| } | ||||||
| ] | ||||||
| }, | ||||||
| { | ||||||
| "label": "MCP", | ||||||
| "children": [ | ||||||
|
|
@@ -834,7 +856,7 @@ | |||||
| "updatedAt": "2026-08-21" | ||||||
| }, | ||||||
| { | ||||||
| "label": "Sampling → modelOptions", | ||||||
| "label": "Sampling \u00e2\u2020\u2019 modelOptions", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Store the actual right-arrow character.
Proposed fix- "label": "Sampling \u00e2\u2020\u2019 modelOptions",
+ "label": "Sampling \u2192 modelOptions",📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| "to": "migration/sampling-options-to-model-options", | ||||||
| "addedAt": "2026-06-03" | ||||||
| } | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,13 @@ keywords: | |
| - SKILL.md | ||
| - AGENTS.md | ||
| --- | ||
| > **Looking for runtime snippets inside Code Mode?** Those are a different feature — see [Code Mode with Snippets](../code-mode/code-mode-with-snippets). This page is about _agent-authoring_ skills: markdown files that teach your coding assistant how TanStack AI works. | ||
| > **Looking for runtime snippets inside Code Mode?** Those are a different feature, see [Code Mode with Snippets](../code-mode/code-mode-with-snippets). This page is about _agent-authoring_ skills: markdown files that teach your coding assistant how TanStack AI works. | ||
|
|
||
| > **Want your app's model to load `SKILL.md` skills at runtime?** That is a | ||
| > different feature with a confusingly similar name. See | ||
| > [Portable Agent Skills](../skills/agent-skills): a runtime catalog plus a | ||
| > `load_skill` tool, for the model inside your app. This page is only about | ||
| > teaching your _coding assistant_ how to use TanStack AI. | ||
|
Comment on lines
+17
to
+23
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Keep the callouts within one blockquote. Line 18 is a blank line inside the blockquote. This violates MD028 and can fail Markdown linting. Use a quoted blank line ( 🧰 Tools🪛 markdownlint-cli2 (0.23.2)[warning] 18-18: Blank line inside blockquote (MD028, no-blanks-blockquote) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| ## Step 1: Install TanStack AI | ||
|
|
||
| If you haven't already, install `@tanstack/ai` plus any adapter packages you need. See the [Quick Start](./quick-start) for a full walkthrough. | ||
|
|
@@ -48,6 +54,7 @@ TanStack AI publishes skills inside its packages so the guidance travels with `n | |
| | `@tanstack/ai-mcp` | `ai-mcp` | Connecting to MCP servers, running their tools inside `chat()`, resources, prompts, and the type-generating CLI | | ||
| | `@tanstack/ai-sandbox` | `ai-sandbox` | Running harness adapters inside isolated sandboxes with `defineSandbox` / `withSandbox` | | ||
| | `@tanstack/ai-code-mode` | `ai-code-mode` | Setting up Code Mode with a sandbox driver and registering server tools | | ||
| | `@tanstack/ai-skills` | `ai-skills` | Portable Agent Skills at runtime: the `withSkills` middleware, `load_skill`, the `SkillSource` interface, `inlineSkill` / `skillDirectory` / `staticSkills`, and adding your own tools for skills that carry code | | ||
|
|
||
| Skills route to each other: `ai-core` points at the companion packages' | ||
| skills, and `ai-persistence` is an entry point that routes to its own | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,245 @@ | ||
| --- | ||
| title: Portable Agent Skills | ||
| id: portable-agent-skills | ||
| order: 1 | ||
| description: "Give any tool-calling model a library of SKILL.md skills it can load on demand, on any provider, with the withSkills middleware from @tanstack/ai-skills." | ||
| keywords: | ||
| - tanstack ai | ||
| - agent skills | ||
| - SKILL.md | ||
| - portable skills | ||
| - withSkills | ||
| - load_skill | ||
| - skill catalog | ||
| --- | ||
|
|
||
| You have a set of `SKILL.md` files: reusable instructions that teach a model how | ||
| to do one thing well (build a slide deck, follow your brand voice, fill a PDF). | ||
| You want the model to reach for the right one on its own, on whatever provider | ||
| you happen to run, without pasting every skill into the system prompt. | ||
|
|
||
| `withSkills` from `@tanstack/ai-skills` does this. It renders a short catalog of | ||
| the skills you offer, and gives the model a `load_skill` tool. The model reads | ||
| the catalog, picks a skill, calls `load_skill`, and gets the full instructions | ||
| back, only when it needs them. This works with any tool-calling model. | ||
|
|
||
| > This is the **portable** path: it runs on the model you already use, no server | ||
| > sandbox required. For hosted skills that run in a provider's sandbox, see | ||
| > [Provider Skills](../tools/provider-skills). The two do not mix in one call, | ||
| > see [Portable vs hosted](../tools/provider-skills#portable-vs-hosted-skills). | ||
|
|
||
| ## Install | ||
|
|
||
| ```bash | ||
| npm install @tanstack/ai-skills | ||
| ``` | ||
|
|
||
| ## Add skills to a chat | ||
|
|
||
| Define a skill inline, then pass it to `withSkills` in the `middleware` array. | ||
| The middleware handles the catalog and the `load_skill` tool for you. | ||
|
|
||
| ```typescript | ||
| import { chat, toServerSentEventsResponse } from '@tanstack/ai' | ||
| import { anthropicText } from '@tanstack/ai-anthropic' | ||
| import { inlineSkill, withSkills } from '@tanstack/ai-skills' | ||
|
|
||
| const pptx = inlineSkill({ | ||
| name: 'pptx-builder', | ||
| description: 'Build and edit PowerPoint decks with python-pptx.', | ||
| instructions: ` | ||
| # Building a deck | ||
| Use python-pptx. Open or create the presentation, edit slides, then save. | ||
| Keep one idea per slide. | ||
| `, | ||
| }) | ||
|
|
||
| export async function POST(request: Request) { | ||
| const { messages } = await request.json() | ||
|
|
||
| const stream = chat({ | ||
| adapter: anthropicText('claude-sonnet-4-5'), | ||
| messages, | ||
| middleware: [withSkills(pptx)], | ||
| }) | ||
|
|
||
| return toServerSentEventsResponse(stream) | ||
| } | ||
| ``` | ||
|
|
||
| That is the whole setup. The model now sees `pptx-builder` in its catalog and | ||
| can call `load_skill` to pull in the instructions when a deck-building task comes | ||
| up. | ||
|
|
||
| ## What the model sees | ||
|
|
||
| `withSkills` adds two things to the request: | ||
|
|
||
| - A catalog in the system prompt, one line per skill (name plus description). | ||
| The `name` of `load_skill` is constrained to your skill names, so the model | ||
| cannot invent one. | ||
| - A `load_skill` tool. When the model calls it, the middleware returns the | ||
| skill body (frontmatter stripped) plus a list of any bundled resources. | ||
|
|
||
| Loading the same skill twice in one conversation returns a short "already | ||
| loaded" marker instead of repeating the body, so context stays lean. | ||
|
|
||
| ## Offer more than one skill | ||
|
|
||
| Pass an array. Skills are sorted by name and deduped for you. | ||
|
|
||
| ```typescript | ||
| import { chat, toServerSentEventsResponse } from '@tanstack/ai' | ||
| import { anthropicText } from '@tanstack/ai-anthropic' | ||
| import { inlineSkill, withSkills } from '@tanstack/ai-skills' | ||
|
|
||
| const pptx = inlineSkill({ | ||
| name: 'pptx-builder', | ||
| description: 'Build and edit PowerPoint decks with python-pptx.', | ||
| instructions: '# Building a deck\nUse python-pptx. Edit slides, then save.', | ||
| }) | ||
|
|
||
| const brand = inlineSkill({ | ||
| name: 'brand-voice', | ||
| description: 'Write in the company brand voice.', | ||
| instructions: '# Brand voice\nWarm, direct, no jargon.', | ||
| }) | ||
|
|
||
| export async function POST(request: Request) { | ||
| const { messages } = await request.json() | ||
|
|
||
| const stream = chat({ | ||
| adapter: anthropicText('claude-sonnet-4-5'), | ||
| messages, | ||
| middleware: [withSkills([pptx, brand])], | ||
| }) | ||
|
|
||
| return toServerSentEventsResponse(stream) | ||
| } | ||
| ``` | ||
|
|
||
| Inline skills are the quickest start, but you rarely keep skills in code. Read | ||
| them from a folder, a build-time bundle, or your own database. See | ||
| [Skill sources](./skill-sources). | ||
|
|
||
| ## Tune the catalog | ||
|
|
||
| `withSkills` takes options for the common cases: | ||
|
|
||
| ```ts ignore | ||
| withSkills(sources, { | ||
| // Cap the catalog so a big skill library doesn't tax every request. | ||
| // Default 4000 tokens; throws if exceeded unless you supply a reducer. | ||
| maxCatalogTokens: 4000, | ||
|
|
||
| // Require a human approval before load_skill runs. Default false. | ||
| requireApproval: true, | ||
| }) | ||
| ``` | ||
|
|
||
| The catalog is rendered per model family: Anthropic models get the | ||
| `<available_skills>` XML they are tuned for, everything else gets a plain | ||
| markdown list. You can override this with a `renderCatalog` function or an | ||
| `instructionTemplate` string that has a `{skills}` placeholder. | ||
|
|
||
| ## Read a skill's files | ||
|
|
||
| Some skills bundle reference files (a style guide, a schema, an example). To let | ||
| the model read them, add `createResourceTool` to your `tools`. `withSkills` | ||
| notices it and tells the model it can call `read_skill_resource`. | ||
|
|
||
| ```typescript | ||
| import { chat, toServerSentEventsResponse } from '@tanstack/ai' | ||
| import { anthropicText } from '@tanstack/ai-anthropic' | ||
| import { createResourceTool, inlineSkill, withSkills } from '@tanstack/ai-skills' | ||
|
|
||
| const pdf = inlineSkill({ | ||
| name: 'pdf-filler', | ||
| description: 'Fill a PDF form from a data object.', | ||
| instructions: '# Fill a PDF\nSee references/fields.md for the field map.', | ||
| resources: { 'references/fields.md': 'name -> field_1\nemail -> field_2' }, | ||
| }) | ||
|
|
||
| export async function POST(request: Request) { | ||
| const { messages } = await request.json() | ||
|
|
||
| const stream = chat({ | ||
| adapter: anthropicText('claude-sonnet-4-5'), | ||
| messages, | ||
| tools: [createResourceTool(pdf)], | ||
| middleware: [withSkills(pdf)], | ||
| }) | ||
|
|
||
| return toServerSentEventsResponse(stream) | ||
| } | ||
| ``` | ||
|
|
||
| Without the resource tool, resources are still listed in the `load_skill` | ||
| result, but the model is told they are not loadable in this setup. | ||
|
|
||
| ## Skills that come with code | ||
|
|
||
| Some skills ship scripts, or their instructions say "run `python3 extract.py`". | ||
| `withSkills` lists those scripts in the `load_skill` result but does not run | ||
| them. Running code is your app's job, and you wire it up by passing your own | ||
| tool. | ||
|
|
||
| `withSkills` composes with whatever tools you give `chat()`. So add an execution | ||
| tool, and write the skill so it tells the model to call that tool. The skill | ||
| supplies the "how" (the command); your tool supplies the ability to run it. | ||
|
|
||
| ```ts ignore | ||
| import { chat, toServerSentEventsResponse, toolDefinition } from '@tanstack/ai' | ||
| import { anthropicText } from '@tanstack/ai-anthropic' | ||
| import { inlineSkill, withSkills } from '@tanstack/ai-skills' | ||
| import { z } from 'zod' | ||
|
|
||
| // Your own execution tool. Run the command wherever you want: a provider | ||
| // sandbox, a local isolate, a serverless worker. Guard it in production. | ||
| const executeShell = toolDefinition({ | ||
| name: 'execute_shell', | ||
| description: 'Run a shell command and return its stdout.', | ||
| inputSchema: z.object({ command: z.string() }), | ||
| outputSchema: z.object({ stdout: z.string() }), | ||
| }).server(async ({ command }) => { | ||
| const { stdout } = await runInYourSandbox(command) | ||
| return { stdout } | ||
|
Comment on lines
+197
to
+206
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: printf '%s\n' '--- applicable repository guidance ---'
find /tmp/coderabbit-repo-knowledge/tanstack-ai-745fe4e5 -type f -name '*.md' -maxdepth 3 -print
printf '%s\n' '--- docs skill ---'
if [ -f .claude/skills/docs/SKILL.md ]; then
cat .claude/skills/docs/SKILL.md
else
echo 'docs skill not present'
fi
printf '%s\n' '--- target excerpt ---'
cat -n docs/skills/agent-skills.md | sed -n '160,225p'
printf '%s\n' '--- referenced sandbox symbol ---'
rg -n -C 3 'runInYourSandbox|execute_shell|toolDefinition' docs/skills/agent-skills.mdRepository: TanStack/ai Length of output: 28569 🏁 Script executed: printf '%s\n' '--- documentation conventions ---'
cat /tmp/coderabbit-repo-knowledge/tanstack-ai-745fe4e5/conventions/docs.md
printf '%s\n' '--- sandbox-related repository learning ---'
cat /tmp/coderabbit-repo-knowledge/tanstack-ai-745fe4e5/learnings/docs-sandbox.md
printf '%s\n' '--- complete execution-tool section ---'
cat -n docs/skills/agent-skills.md | sed -n '180,245p'Repository: TanStack/ai Length of output: 6549 Injection (CWE-78): Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') Reachability: External · Exploitability: Moderate Replace the unrestricted shell tool with bounded, authorized operations. The 🤖 Prompt for AI Agents |
||
| }) | ||
|
|
||
| const extractPdf = inlineSkill({ | ||
| name: 'pdf-extract', | ||
| description: 'Extract text from a PDF with a small Python script.', | ||
| instructions: ` | ||
| # Extract PDF text | ||
| Run this with the execute_shell tool, then return the text it prints: | ||
| python3 -c "import sys, pypdf; ..." | ||
| `, | ||
| }) | ||
|
|
||
| export async function POST(request: Request) { | ||
| const { messages } = await request.json() | ||
|
|
||
| const stream = chat({ | ||
| adapter: anthropicText('claude-sonnet-4-5'), | ||
| messages, | ||
| tools: [executeShell], | ||
| middleware: [withSkills(extractPdf)], | ||
| }) | ||
|
|
||
| return toServerSentEventsResponse(stream) | ||
| } | ||
| ``` | ||
|
|
||
| Swap `execute_shell` for any tool: a container runner, a Code Mode sandbox, or | ||
| a remote worker. The skill never changes, only the tool behind it. For hosted | ||
| skills that run in a provider's own sandbox instead, see | ||
| [Provider Skills](../tools/provider-skills). | ||
|
|
||
| ## Where to go next | ||
|
|
||
| - [Skill sources](./skill-sources) — load skills from a folder, a build-time | ||
| bundle, or your own store, and combine several sources. | ||
| - [Write a skill source](./writing-adapters) — back skills with S3, a database, | ||
| or a registry, and prove it with the conformance suite. | ||
| - [Provider Skills](../tools/provider-skills) — hosted skills that run in a | ||
| provider sandbox, and when to use them instead. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use the current date for documentation metadata.
The changed entries use
2026-08-22and2026-08-23, but the current date is2026-08-27. Set the affectedaddedAtandupdatedAtvalues to2026-08-27.As per coding guidelines, new pages must use today’s
addedAt, and edited pages must use today’supdatedAt.Also applies to: 97-98, 138-149
🤖 Prompt for AI Agents
Source: Coding guidelines