Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/portable-agent-skills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@tanstack/ai-skills': minor
'@tanstack/ai': minor
'@tanstack/ai-anthropic': patch
'@tanstack/openai-base': patch
'@tanstack/ai-sandbox': 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`.
28 changes: 25 additions & 3 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
},
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -834,7 +856,7 @@
"updatedAt": "2026-08-21"
},
{
"label": "Sampling Ò†’ modelOptions",
"label": "Sampling \u00e2\u2020\u2019 modelOptions",
"to": "migration/sampling-options-to-model-options",
"addedAt": "2026-06-03"
}
Expand Down
9 changes: 8 additions & 1 deletion docs/getting-started/agent-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
## 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.
Expand Down Expand Up @@ -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
Expand Down
245 changes: 245 additions & 0 deletions docs/skills/agent-skills.md
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 }
})

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.
Loading
Loading