Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/harness-skills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-skills': minor
---

`@tanstack/ai-skills/harness`: the new `skills({ dirs })` plugin gives the harness model the skills of a list of folders, and makes each skill a `/<name>` command that starts a turn with that skill. A skill with the name of another command is `/skill:<name>`, and `/skills` lists them. The plugin watches the folders, so a skill that you add or remove while the session runs changes the commands at once.
5 changes: 5 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,11 @@
"label": "Run agents from a harness",
"to": "harness/subagents",
"addedAt": "2026-10-13"
},
{
"label": "Add skills",
"to": "harness/skills",
"addedAt": "2026-10-13"
}
]
},
Expand Down
2 changes: 2 additions & 0 deletions docs/harness/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ export const greetings = definePlugin({
})
```

The [skills plugin](./skills) works this way: one command for each skill in a folder.

## Change agents while the session runs

Agents can come from data that changes too. `ctx.agents.set` and `ctx.agents.delete` work like `ctx.commands`. Here `/helper` adds an agent that the session can run at once:
Expand Down
103 changes: 103 additions & 0 deletions docs/harness/skills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
title: Add skills to a harness
id: harness-skills
order: 10
description: "Give the harness model the skills in a list of folders, and make each skill a slash command. A skill that you add while the session runs works at once."
keywords:
- tanstack ai
- harness
- skills
- SKILL.md
- slash commands
---

Your users keep skills as `SKILL.md` folders, in the repo and in their home folder, and they want to call them from your agent like any other command. The skills plugin reads those folders, gives the model the list of skills, and makes each skill a `/<name>` command. A skill that a user adds while the session runs works at once.

The plugin reads the folders with `node:fs`, so it runs on Node.js. It does not work on Cloudflare Workers.

## 1. Add the plugin

<!-- ::start:tabs variant="package-manager" mode="install" -->

react: @tanstack/ai-skills
vue: @tanstack/ai-skills
solid: @tanstack/ai-skills
svelte: @tanstack/ai-skills
preact: @tanstack/ai-skills
angular: @tanstack/ai-skills
octane: @tanstack/ai-skills
vanilla: @tanstack/ai-skills

<!-- ::end:tabs -->

```ts group=harness-skills
import { homedir } from 'node:os'
import { join } from 'node:path'
import { defineHarness } from '@tanstack/ai-harness'
import { openaiText } from '@tanstack/ai-openai'
import { skills } from '@tanstack/ai-skills/harness'

export const assistant = defineHarness({
name: 'acme/assistant',
adapter: openaiText('gpt-6.1-sol'),
plugins: () => [
skills({
dirs: ['./.agents/skills', join(homedir(), '.acme', 'skills')],
}),
],
})
```

- `dirs` lists the skill folders. Each one holds skill folders with a `SKILL.md`.
- When two folders have a skill with the same name, the first folder wins. Put the repo folder first, so a project can replace a user's skill.
- A folder that does not exist yet is fine. Its skills show once the user makes it.

## 2. Call a skill

`/release-notes write them for v2` starts a turn with "Use the release-notes skill: write them for v2". The model loads the skill with `load_skill`, then does the task. `/release-notes` alone asks for the skill with no task.

The model also sees the list of skills, so it can load one on its own when a task matches.

## Names that are taken

A skill with the name of another command gets a prefix: a `model` skill is `/skill:model`, and `/model` stays the model picker. Pass `reserved` with the names that your UI handles itself:

```ts group=harness-skills
export const cliSkills = skills({
dirs: ['./.agents/skills'],
reserved: ['help', 'exit'],
})
```

`/skills` lists every skill, its folder, and its command.

## Skills that change while the session runs

The plugin watches the folders, and it lists them again at the start of each turn:

- A new `SKILL.md` adds its command.
- A removed skill takes its command away.
- An edited description updates the command.
- A broken `SKILL.md` is left out until it is fixed.

Each change sends a `harness.commands.changed` event, so a UI can show the new command list at once.

## Many skills

The model's list has one line for each skill: the name and the first sentence of the description. When the list is longer than `maxCatalogTokens` (default 4000), the skills of the earlier folders stay, and the rest leave the model's list. A skill that the user calls with its command always stays in it.

The other options of `withSkills` pass through, for example `maxCatalogTokens` and `requireApproval`:

```ts group=harness-skills
export const manySkills = skills({
dirs: ['./.agents/skills'],
maxCatalogTokens: 8000,
})
```

## What you have now

- The skills of your folders, in the model's list and as `/<name>` commands.
- New and changed skills that work in a running session, with no restart.

Next: [Agent Skills](../skills/agent-skills) has the `SKILL.md` format, and [Write a harness plugin](./plugins#change-commands-while-the-session-runs) shows the `ctx.commands` API that the plugin uses.
9 changes: 9 additions & 0 deletions packages/ai-skills/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"types": "./dist/esm/index.d.ts",
"import": "./dist/esm/index.js"
},
"./harness": {
"types": "./dist/esm/harness.d.ts",
"import": "./dist/esm/harness.js"
},
"./node": {
"types": "./dist/esm/node/index.d.ts",
"import": "./dist/esm/node/index.js"
Expand Down Expand Up @@ -75,15 +79,20 @@
},
"peerDependencies": {
"@tanstack/ai": "workspace:^",
"@tanstack/ai-harness": "workspace:^",
"vitest": "^4.1.10",
"zod": "^3.0.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"@tanstack/ai-harness": {
"optional": true
},
"vitest": {
"optional": true
}
},
"devDependencies": {
"@tanstack/ai-harness": "workspace:*",
"@vitest/coverage-v8": "4.1.10",
"vitest": "^4.1.10",
"zod": "^4.2.0"
Expand Down
Loading
Loading