@tanstack/ai-skills
Version:
Portable Agent Skills (SKILL.md) as a first-class chat() middleware for TanStack AI.
67 lines (62 loc) • 1.99 kB
text/typescript
/**
* `inlineSkill` — an edge-safe {@link SkillSource} for a single skill defined
* next to app code, in a DB row, or per-session/per-tenant. Resource values may
* be thunks, evaluated at read time.
*/
import { stableHash } from '../util'
import { validateSkill } from '../validate'
import type { SkillMetadata, SkillSource } from '../types'
export interface InlineSkillConfig {
name: string
description: string
instructions: string
resources?: Record<string, string | (() => string | Promise<string>)>
compatibility?: string
}
export function inlineSkill(config: InlineSkillConfig): SkillSource {
const metadata: SkillMetadata = {
name: config.name,
description: config.description,
...(config.compatibility && { compatibility: config.compatibility }),
}
const lint = validateSkill(metadata)
if (!lint.ok) {
throw new Error(
`inlineSkill "${config.name}": ${lint.issues.map((i) => i.message).join('; ')}`,
)
}
const resourcePaths = Object.keys(config.resources ?? {})
const revision = stableHash(
JSON.stringify({
metadata,
instructions: config.instructions,
resources: resourcePaths,
}),
)
const assertName = (name: string) => {
if (name !== config.name) {
throw new Error(`inlineSkill has no skill named "${name}"`)
}
}
return {
revision: () => Promise.resolve(revision),
list: () => Promise.resolve([metadata]),
// Async so an unknown-name throw surfaces as a rejected promise.
load: async (name) => {
assertName(name)
return config.instructions
},
listResources: async (name) => {
assertName(name)
return resourcePaths
},
readResource: async (name, path) => {
assertName(name)
const value = config.resources?.[path]
if (value === undefined) {
throw new Error(`skill "${name}" has no resource "${path}"`)
}
return typeof value === 'function' ? await value() : value
},
}
}