Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- Providers
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
Write a Skill Source
A skill source adapter loads skills from S3, a database, or a registry. withSkills then reads them through that adapter.
Write a SkillSource for your store. It is a small interface that returns bytes only, so it works anywhere, including the edge. Then prove it with the conformance suite.
Quick setup#
1. Write the source#
A source needs two methods:
- list returns the catalog: a name and a description for each skill.
- load returns the raw SKILL.md of one skill, frontmatter included. It throws when the name does not exist.
This source reads from any store that you wrap in a SkillStore:
// db-skills.ts
import type { SkillSource } from '@tanstack/ai-skills'
export interface SkillRow {
name: string
description: string
/** The raw SKILL.md, frontmatter included. */
body: string
}
export interface SkillStore {
listSkills: () => Promise<Array<SkillRow>>
getSkill: (name: string) => Promise<SkillRow | undefined>
}
export function dbSkills(store: SkillStore): SkillSource {
return {
async list() {
const rows = await store.listSkills()
return rows.map((row) => ({
name: row.name,
description: row.description,
}))
},
async load(name) {
const row = await store.getSkill(name)
if (!row) {
throw new Error(`no skill named "${name}"`)
}
return row.body
},
}
}2. Pass it to withSkills#
import { withSkills } from '@tanstack/ai-skills'
import { dbSkills } from './db-skills'
const skills = withSkills(dbSkills(yourStore))Add skills to the middleware array of chat(), as in Portable Agent Skills. The middleware removes the frontmatter from what load returns.
3. Run the conformance suite#
The package ships a Vitest suite at @tanstack/ai-skills/testing. Seed your source with the fixture that the suite expects:
- A skill alpha with a description.
- A skill beta with a description.
// db-skills.test.ts
import { runSkillSourceConformance } from '@tanstack/ai-skills/testing'
import { dbSkills } from './db-skills'
import type { SkillRow } from './db-skills'
function makeTestStore() {
const rows: Array<SkillRow> = [
{
name: 'alpha',
description: 'First test skill.',
body: '---\nname: alpha\ndescription: First test skill.\n---\n# Alpha',
},
{
name: 'beta',
description: 'Second test skill.',
body: '---\nname: beta\ndescription: Second test skill.\n---\n# Beta',
},
]
return {
listSkills: async () => rows,
getSkill: async (name: string) => rows.find((row) => row.name === name),
}
}
runSkillSourceConformance(() => dbSkills(makeTestStore()), 'db')Run your tests. When the suite passes, your source is safe to give to withSkills.
How it works#
withSkills calls list() once for each chat() call to build the catalog. When the model calls load_skill, the middleware calls load(name).
A source has no path field. It returns bytes, so a database or a registry works the same as a folder.
The suite tests these rules:
- A missing skill name throws. It does not return an empty string.
- Two list() calls at the same time return the same skills.
- revision(), if present, returns the same value for the same content.
- Resources load, and a path like ../../etc/passwd is rejected.
- Script bytes come back unchanged.
The suite skips the resource and script checks when your source does not have those methods.
Advanced#
Optional methods#
| Method | Returns | Use it for |
|---|---|---|
| revision() | Promise<string> | A stable value that changes only when the content changes, such as a bucket ETag or a content hash. |
| listResources(name) | Promise<Array<string>> | Paths under references/ and assets/, relative to the skill root. |
| readResource(name, path) | Promise<string | Uint8Array> | The content of one resource. read_skill_resource calls it. |
| listScripts(name) | Promise<Array<SkillScriptRef>> | The scripts of a skill, listed but not run. |
| readScript(name, path) | Promise<Uint8Array> | The bytes of one script. |
withSkills does not cache on revision(). Add revision() so that combinators, your own cache, or a catalog memo can use it as a key.
Add resources and a revision#
This example reads from an S3-style bucket. Call assertSafeResourcePath before you read a path, so a path outside the skill root throws:
import { assertSafeResourcePath } from '@tanstack/ai-skills'
import type { SkillSource } from '@tanstack/ai-skills'
export function s3Skills(bucket: S3Bucket): SkillSource {
return {
revision: () => bucket.getText('skills/index.etag'),
async list() {
const index = await bucket.getJSON('skills/index.json')
return index.map((s) => ({ name: s.name, description: s.description }))
},
async load(name) {
return bucket.getText(`skills/${name}/SKILL.md`)
},
async listResources(name) {
return bucket.listKeys(`skills/${name}/references/`)
},
async readResource(name, path) {
assertSafeResourcePath(path)
return bucket.getBytes(`skills/${name}/${path}`)
},
}
}Full fixture for resources and scripts#
If your source has resource methods, the suite also expects this on alpha:
- A resource references/note.md with the content hello.
If your source has script methods, the suite also expects this on alpha:
- A script scripts/run.py whose bytes decode to print(1), listed with executable: false.