diff --git a/.changeset/rename-code-mode-skills-to-snippets.md b/.changeset/rename-code-mode-skills-to-snippets.md new file mode 100644 index 0000000000..efdc64baa7 --- /dev/null +++ b/.changeset/rename-code-mode-skills-to-snippets.md @@ -0,0 +1,28 @@ +--- +'@tanstack/ai-code-mode-snippets': minor +'@tanstack/ai-code-mode': minor +'@tanstack/ai': minor +--- + +Rename Code Mode "skills" to "snippets" to disambiguate them from agent skills (the `SKILL.md` packaging system). + +**Breaking — package rename.** `@tanstack/ai-code-mode-skills` is now published as **`@tanstack/ai-code-mode-snippets`**. Update your dependency and imports. The `/storage` subpath is unchanged. + +**Breaking — API rename.** Every `Skill`/`skill` identifier in the package public API becomes `Snippet`/`snippet`, for example: + +- `codeModeWithSkills()` → `codeModeWithSnippets()` +- `skillsToTools()` / `skillToTool()` → `snippetsToTools()` / `snippetToTool()` +- `skillsToBindings()` / `skillsToSimpleBindings()` → `snippetsToBindings()` / `snippetsToSimpleBindings()` +- `selectRelevantSkills()` → `selectRelevantSnippets()` +- `createSkillManagementTools()` → `createSnippetManagementTools()` +- `createSkillsSystemPrompt()` → `createSnippetsSystemPrompt()` +- `generateSkillTypes()` → `generateSnippetTypes()` +- `createFileSkillStorage()` / `createMemorySkillStorage()` → `createFileSnippetStorage()` / `createMemorySnippetStorage()` +- Types: `Skill`, `SkillStorage`, `SkillIndexEntry`, `SkillStats`, `SkillBinding`, `SkillsConfig`, `CodeModeWithSkillsOptions`/`Result` → the `Snippet…` equivalents +- Options: `skills` → `snippets`, `skillsAsTools` → `snippetsAsTools`, `maxSkillsInContext` → `maxSnippetsInContext` +- Runtime tools: `search_skills` / `get_skill` / `register_skill` → `search_snippets` / `get_snippet` / `register_snippet` +- Sandbox bindings are now exposed with the `snippet_` prefix (was `skill_`) + +**Breaking — sandbox hook (`@tanstack/ai-code-mode`).** The `createCodeModeTool` config option `getSkillBindings` is renamed to **`getSnippetBindings`** (same signature — an optional `() => Promise>` returning dynamic bindings merged at execution time). + +**Breaking — wire contract (`@tanstack/ai`).** The Code Mode custom events are renamed: `code_mode:skill_call` / `_result` / `_error` → `code_mode:snippet_*` (payload field `skill` → `snippet`), and `skill:registered` → `snippet:registered`. The exported event types `CodeModeSkillCallEvent` / `CodeModeSkillResultEvent` / `CodeModeSkillErrorEvent` / `SkillRegisteredEvent` are renamed to their `Snippet` equivalents. diff --git a/README.md b/README.md index 9252787d8c..82723d1ab0 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ Learn more in the build low-latency realtime voice experiences. - [Code Mode](https://tanstack.com/ai/latest/docs/code-mode/code-mode) - let models write and execute TypeScript inside a secure isolate. -- [Code Mode with Skills](https://tanstack.com/ai/latest/docs/code-mode/code-mode-with-skills) - +- [Code Mode with Snippets](https://tanstack.com/ai/latest/docs/code-mode/code-mode-with-snippets) - give Code Mode reusable runtime capabilities. ## Providers diff --git a/docs/code-mode/client-integration.md b/docs/code-mode/client-integration.md index 6e6928c451..b30976f283 100644 --- a/docs/code-mode/client-integration.md +++ b/docs/code-mode/client-integration.md @@ -283,4 +283,4 @@ The `onCustomEvent` callback is available through `ChatClient` from `@tanstack/a (eventType: string, data: unknown, context: { toolCallId?: string }) => void ``` -See [Code Mode](./code-mode) for setting up the server side, and [Code Mode with Skills](./code-mode-with-skills) for adding persistent skill libraries. +See [Code Mode](./code-mode) for setting up the server side, and [Code Mode with Snippets](./code-mode-with-snippets) for adding persistent snippet libraries. diff --git a/docs/code-mode/code-mode-isolates.md b/docs/code-mode/code-mode-isolates.md index 2426838801..91081b0c72 100644 --- a/docs/code-mode/code-mode-isolates.md +++ b/docs/code-mode/code-mode-isolates.md @@ -348,5 +348,5 @@ You can implement this interface to build a custom driver — for example, a Doc - [Code Mode](./code-mode) — Core setup, API reference, and getting started guide - [Showing Code Mode in the UI](./client-integration) — Display execution progress in your React app -- [Code Mode with Skills](./code-mode-with-skills) — Add persistent, reusable skill libraries +- [Code Mode with Snippets](./code-mode-with-snippets) — Add persistent, reusable snippet libraries diff --git a/docs/code-mode/code-mode-with-skills.md b/docs/code-mode/code-mode-with-skills.md deleted file mode 100644 index f5dfea65a8..0000000000 --- a/docs/code-mode/code-mode-with-skills.md +++ /dev/null @@ -1,368 +0,0 @@ ---- -title: Code Mode with Skills -id: code-mode-with-skills -order: 3 -description: "Teach Code Mode to save and reuse working code as named skills backed by persistent storage — faster follow-up requests and composable agent memory." -keywords: - - tanstack ai - - code mode - - skills - - skill library - - register_skill - - reusable snippets - - agent memory - - skill storage ---- - -Skills extend [Code Mode](./code-mode.md) with a persistent library of reusable TypeScript snippets. When the LLM writes a useful piece of code — say, a function that fetches and ranks NPM packages — it can save that code as a _skill_. On future requests, relevant skills are loaded from storage and made available as first-class tools the LLM can call without re-writing the logic. - -> **Different from agent-authoring skills.** The skills on this page are _runtime_ snippets the chat LLM saves and reuses. If you're looking to teach your coding assistant (Claude Code, Cursor, etc.) how TanStack AI itself works, see [Agent Skills (TanStack Intent)](../getting-started/agent-skills). - -## Overview - -The skills system has two integration paths: - -| Approach | Entry point | Skill selection | Best for | -|----------|-------------|----------------|----------| -| **High-level** | `codeModeWithSkills()` | Automatic (LLM-based) | New projects, turnkey setup | -| **Manual** | Individual functions (`skillsToTools`, `createSkillManagementTools`, etc.) | You decide which skills to load | Full control, existing setups | - -Both paths share the same storage, trust, and execution primitives — they differ only in how skills are selected and assembled. - -## How It Works - -A request with skills enabled goes through these stages: - -``` -┌─────────────────────────────────────────────────────┐ -│ 1. Load skill index (metadata only, no code) │ -├─────────────────────────────────────────────────────┤ -│ 2. Select relevant skills (LLM call — fast model) │ -├─────────────────────────────────────────────────────┤ -│ 3. Build tool registry │ -│ ├── execute_typescript (Code Mode sandbox) │ -│ ├── search_skills / get_skill / register_skill │ -│ └── skill tools (one per selected skill) │ -├─────────────────────────────────────────────────────┤ -│ 4. Generate system prompt │ -│ ├── Code Mode type stubs │ -│ └── Skill library documentation │ -├─────────────────────────────────────────────────────┤ -│ 5. Main chat() call (strong model) │ -│ ├── Can call skill tools directly │ -│ ├── Can write code via execute_typescript │ -│ └── Can register new skills for future use │ -└─────────────────────────────────────────────────────┘ -``` - -### LLM calls - -There are **two** LLM interactions per request when using the high-level API: - -1. **Skill selection** (`selectRelevantSkills`) — A single chat call using the adapter you provide. It sends the last 5 conversation messages plus a catalog of skill names/descriptions, and asks the model to return a JSON array of relevant skill names. This should be a cheap/fast model (e.g., `gpt-4o-mini`, `claude-haiku-4-5`). - -2. **Main chat** — The primary `chat()` call with your full model. This is where the LLM reasons, calls tools, writes code, and registers skills. - -The selection call is lightweight — it only sees skill metadata (names, descriptions, usage hints), not full code. If there are no skills in storage or no messages, it short-circuits and skips the LLM call entirely. - -## High-Level API: `codeModeWithSkills()` - -### Installation - -```bash -pnpm add @tanstack/ai-code-mode-skills -``` - -### Usage - -```typescript -import { chat, maxIterations, toServerSentEventsStream } from '@tanstack/ai' -import { createNodeIsolateDriver } from '@tanstack/ai-isolate-node' -import { codeModeWithSkills } from '@tanstack/ai-code-mode-skills' -import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' -import { openaiText } from '@tanstack/ai-openai' -import { myTool1, myTool2 } from './tools' - -const messages = [{ role: 'user' as const, content: 'Hello' }] -const storage = createFileSkillStorage({ directory: './.skills' }) -const driver = createNodeIsolateDriver() - -const { toolsRegistry, systemPrompt, selectedSkills } = await codeModeWithSkills({ - config: { - driver, - tools: [myTool1, myTool2], - timeout: 60_000, - memoryLimit: 128, - }, - adapter: openaiText('gpt-5-mini'), // cheap model for skill selection - skills: { - storage, - maxSkillsInContext: 5, - }, - messages, // current conversation -}) - -const stream = chat({ - adapter: openaiText('gpt-5.5'), // strong model for reasoning - tools: toolsRegistry.getTools(), - messages, - systemPrompts: ['You are a helpful assistant.', systemPrompt], - agentLoopStrategy: maxIterations(15), -}) -``` - -`codeModeWithSkills` returns: - -| Property | Type | Description | -|----------|------|-------------| -| `toolsRegistry` | `ToolRegistry` | Mutable registry containing all tools. Pass to `chat()` via `tools: toolsRegistry.getTools()`. | -| `systemPrompt` | `string` | Combined Code Mode + skill library documentation. | -| `selectedSkills` | `Array` | Skills the selection model chose for this conversation. | - -### What goes into the registry - -The registry is populated with: - -- **`execute_typescript`** — The Code Mode sandbox tool. Inside the sandbox, skills are also available as `skill_*` functions (loaded dynamically at execution time). -- **`search_skills`** — Search the skill library by query. Returns matching skill metadata. -- **`get_skill`** — Retrieve full details (including code) for a specific skill. -- **`register_skill`** — Save working code as a new skill. Newly registered skills are immediately added to the registry as callable tools. -- **One tool per selected skill** — Each selected skill becomes a direct tool (prefixed with `[SKILL]` in its description) that the LLM can call without going through `execute_typescript`. - -## Manual API - -If you want full control — for example, loading all skills instead of using LLM-based selection — use the lower-level functions directly. This is the approach used in the `ts-code-mode-web` example. - -```typescript -import { chat, maxIterations } from '@tanstack/ai' -import { createCodeMode } from '@tanstack/ai-code-mode' -import { createNodeIsolateDriver } from '@tanstack/ai-isolate-node' -import { - createAlwaysTrustedStrategy, - createSkillManagementTools, - createSkillsSystemPrompt, - skillsToTools, -} from '@tanstack/ai-code-mode-skills' -import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' -import { openaiText } from '@tanstack/ai-openai' -import { myTool1, myTool2, BASE_PROMPT } from './tools' - -const messages = [{ role: 'user' as const, content: 'Hello' }] -const trustStrategy = createAlwaysTrustedStrategy() -const storage = createFileSkillStorage({ - directory: './.skills', - trustStrategy, -}) -const driver = createNodeIsolateDriver() - -// 1. Create Code Mode tool + prompt -const { tool: codeModeTool, systemPrompt: codeModePrompt } = - createCodeMode({ - driver, - tools: [myTool1, myTool2], - timeout: 60_000, - memoryLimit: 128, - }) - -// 2. Load all skills and convert to tools -const allSkills = await storage.loadAll() -const skillIndex = await storage.loadIndex() - -const skillTools = allSkills.length > 0 - ? skillsToTools({ - skills: allSkills, - driver, - tools: [myTool1, myTool2], - storage, - timeout: 60_000, - memoryLimit: 128, - }) - : [] - -// 3. Create management tools -const managementTools = createSkillManagementTools({ - storage, - trustStrategy, -}) - -// 4. Generate skill library prompt -const skillsPrompt = createSkillsSystemPrompt({ - selectedSkills: allSkills, - totalSkillCount: skillIndex.length, - skillsAsTools: true, -}) - -// 5. Assemble and call chat() -const stream = chat({ - adapter: openaiText('gpt-5.5'), - tools: [codeModeTool, ...managementTools, ...skillTools], - messages, - systemPrompts: [BASE_PROMPT, codeModePrompt, skillsPrompt], - agentLoopStrategy: maxIterations(15), -}) -``` - -This approach skips the selection LLM call entirely — you load whichever skills you want and pass them in directly. - -## Skill Storage - -Skills are persisted through the `SkillStorage` interface. Two implementations are provided: - -### File storage (production) - -`createFileSkillStorage` is Node-only — it imports `node:fs` / `node:path` — so -it lives behind the `/storage` subpath rather than the package root. This keeps -the root export safe to bundle for Cloudflare Workers and browser builds; only -reach for the subpath in a Node runtime. - -```typescript -import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' -import { createDefaultTrustStrategy } from '@tanstack/ai-code-mode-skills' - -const trustStrategy = createDefaultTrustStrategy() -const storage = createFileSkillStorage({ - directory: './.skills', - trustStrategy, // optional, defaults to createDefaultTrustStrategy() -}) -``` - -Creates a directory structure: - -``` -.skills/ - _index.json # Lightweight catalog for fast loading - fetch_github_stats/ - meta.json # Description, schemas, hints, stats - code.ts # TypeScript source - compare_npm_packages/ - meta.json - code.ts -``` - -### Memory storage (testing & edge runtimes) - -```typescript -import { createMemorySkillStorage } from '@tanstack/ai-code-mode-skills' - -const storage = createMemorySkillStorage() -``` - -Keeps everything in memory — no `node:fs` dependency, so it is re-exported from -the package root and is safe to use in Workers and browsers. Useful for tests, -demos, and edge deployments. (It is also available from the `/storage` subpath.) - -### Storage interface - -Both implementations satisfy this interface: - -| Method | Description | -|--------|-------------| -| `loadIndex()` | Load lightweight metadata for all skills (no code) | -| `loadAll()` | Load all skills with full details including code | -| `get(name)` | Get a single skill by name | -| `save(skill)` | Create or update a skill | -| `delete(name)` | Remove a skill | -| `search(query, options?)` | Search skills by text query | -| `updateStats(name, success)` | Record an execution result for trust tracking | - -## Trust Strategies - -Skills start untrusted and earn trust through successful executions. The trust level is metadata only — it does not currently gate execution. Four built-in strategies are available: - -```typescript group=code-mode-with-skills -import { - createDefaultTrustStrategy, - createAlwaysTrustedStrategy, - createRelaxedTrustStrategy, - createCustomTrustStrategy, -} from '@tanstack/ai-code-mode-skills' -``` - -| Strategy | Initial level | Provisional | Trusted | -|----------|--------------|-------------|---------| -| **Default** | `untrusted` | 10+ runs, ≥90% success | 100+ runs, ≥95% success | -| **Relaxed** | `untrusted` | 3+ runs, ≥80% success | 10+ runs, ≥90% success | -| **Always trusted** | `trusted` | — | — | -| **Custom** | Configurable | Configurable | Configurable | - -```typescript group=code-mode-with-skills -const strategy = createCustomTrustStrategy({ - initialLevel: 'untrusted', - provisionalThreshold: { executions: 5, successRate: 0.85 }, - trustedThreshold: { executions: 50, successRate: 0.95 }, -}) -``` - -## Skill Lifecycle - -### Registration - -When the LLM produces useful code via `execute_typescript`, the system prompt instructs it to call `register_skill` with: - -- `name` — snake_case identifier (becomes the tool name) -- `description` — what the skill does -- `code` — TypeScript source that receives an `input` variable -- `inputSchema` / `outputSchema` — JSON Schema strings -- `usageHints` — when to use this skill -- `dependsOn` — other skills this one calls - -The skill is saved to storage and (if a `ToolRegistry` was provided) immediately added as a callable tool in the current session. - -### Execution - -When a skill tool is called, the system: - -1. Wraps the skill code with `const input = ;` -2. Strips TypeScript syntax to plain JavaScript -3. Creates a fresh sandbox context with `external_*` bindings -4. Executes the code and returns the result -5. Updates execution stats (success/failure count) asynchronously - -### Selection (high-level API only) - -On each new request, `selectRelevantSkills`: - -1. Takes the last 5 conversation messages as context -2. Builds a catalog from the skill index (name + description + first usage hint) -3. Asks the adapter to return a JSON array of relevant skill names (max `maxSkillsInContext`) -4. Loads full skill data for the selected names - -If parsing fails or the model returns invalid JSON, it falls back to an empty selection — the request proceeds without pre-loaded skills, but the LLM can still search and use skills via the management tools. - -## Skills as Tools vs. Sandbox Bindings - -The `skillsAsTools` option (default: `true`) controls how skills are exposed: - -| Mode | How the LLM calls a skill | Pros | Cons | -|------|--------------------------|------|------| -| **As tools** (`true`) | Direct tool call: `skill_name({ ... })` | Simpler for the LLM, shows in tool-call UI, proper input validation | One tool per skill in the tool list | -| **As bindings** (`false`) | Inside `execute_typescript`: `await skill_fetch_data({ ... })` | Skills composable in code, fewer top-level tools | LLM must write code to use them | - -When `skillsAsTools` is enabled, the system prompt documents each skill with its schema, usage hints, and example calls. When disabled, skills appear as typed `skill_*` functions in the sandbox type stubs. - -## Custom Events - -Skill execution emits events through the TanStack AI event system: - -| Event | When | Payload | -|-------|------|---------| -| `code_mode:skill_call` | Skill tool invoked | `{ skill, input, timestamp }` | -| `code_mode:skill_result` | Skill completed successfully | `{ skill, result, duration, timestamp }` | -| `code_mode:skill_error` | Skill execution failed | `{ skill, error, duration, timestamp }` | -| `skill:registered` | New skill saved via `register_skill` | `{ id, name, description, timestamp }` | - -To render these events in your React app alongside Code Mode execution events, see [Showing Code Mode in the UI](./client-integration). - -## Tips - -- **Use a cheap model for selection.** The selection call only needs to match skill names to conversation context — `gpt-4o-mini` or `claude-haiku-4-5` work well. -- **Start without skills.** Get Code Mode working first, then add `@tanstack/ai-code-mode-skills` once you have tools that produce reusable patterns. -- **Monitor the skill count.** As the library grows, consider increasing `maxSkillsInContext` or switching to the manual API where you control which skills load. -- **Newly registered skills are available on the next message,** not in the current turn's tool list (unless using `ToolRegistry` with the high-level API, which adds them immediately). -- **Skills can call other skills.** Inside the sandbox, both `external_*` and `skill_*` functions are available. Set `dependsOn` when registering to document these relationships. - -## Next Steps - -- [Code Mode](./code-mode) — Core Code Mode setup and API reference -- [Showing Code Mode in the UI](./client-integration) — Display execution progress in your React app -- [Isolate Drivers](./code-mode-isolates) — Compare sandbox runtimes diff --git a/docs/code-mode/code-mode-with-snippets.md b/docs/code-mode/code-mode-with-snippets.md new file mode 100644 index 0000000000..99aa7e7052 --- /dev/null +++ b/docs/code-mode/code-mode-with-snippets.md @@ -0,0 +1,368 @@ +--- +title: Code Mode with Snippets +id: code-mode-with-snippets +order: 3 +description: "Teach Code Mode to save and reuse working code as named snippets backed by persistent storage — faster follow-up requests and composable agent memory." +keywords: + - tanstack ai + - code mode + - snippets + - snippet library + - register_snippet + - reusable snippets + - agent memory + - snippet storage +--- + +Snippets extend [Code Mode](./code-mode.md) with a persistent library of reusable TypeScript snippets. When the LLM writes a useful piece of code — say, a function that fetches and ranks NPM packages — it can save that code as a _snippet_. On future requests, relevant snippets are loaded from storage and made available as first-class tools the LLM can call without re-writing the logic. + +> **Different from agent-authoring skills.** The snippets on this page are _runtime_ snippets the chat LLM saves and reuses. If you're looking to teach your coding assistant (Claude Code, Cursor, etc.) how TanStack AI itself works, see [Agent Skills (TanStack Intent)](../getting-started/agent-skills). + +## Overview + +The snippets system has two integration paths: + +| Approach | Entry point | Snippet selection | Best for | +|----------|-------------|----------------|----------| +| **High-level** | `codeModeWithSnippets()` | Automatic (LLM-based) | New projects, turnkey setup | +| **Manual** | Individual functions (`snippetsToTools`, `createSnippetManagementTools`, etc.) | You decide which snippets to load | Full control, existing setups | + +Both paths share the same storage, trust, and execution primitives — they differ only in how snippets are selected and assembled. + +## How It Works + +A request with snippets enabled goes through these stages: + +```text +┌─────────────────────────────────────────────────────┐ +│ 1. Load snippet index (metadata only, no code) │ +├─────────────────────────────────────────────────────┤ +│ 2. Select relevant snippets (LLM call — fast model) │ +├─────────────────────────────────────────────────────┤ +│ 3. Build tool registry │ +│ ├── execute_typescript (Code Mode sandbox) │ +│ ├── search_snippets / get_snippet / register_snippet │ +│ └── snippet tools (one per selected snippet) │ +├─────────────────────────────────────────────────────┤ +│ 4. Generate system prompt │ +│ ├── Code Mode type stubs │ +│ └── Snippet library documentation │ +├─────────────────────────────────────────────────────┤ +│ 5. Main chat() call (strong model) │ +│ ├── Can call snippet tools directly │ +│ ├── Can write code via execute_typescript │ +│ └── Can register new snippets for future use │ +└─────────────────────────────────────────────────────┘ +``` + +### LLM calls + +There are **two** LLM interactions per request when using the high-level API: + +1. **Snippet selection** (`selectRelevantSnippets`) — A single chat call using the adapter you provide. It sends the last 5 conversation messages plus a catalog of snippet names/descriptions, and asks the model to return a JSON array of relevant snippet names. This should be a cheap/fast model (e.g., `gpt-4o-mini`, `claude-haiku-4-5`). + +2. **Main chat** — The primary `chat()` call with your full model. This is where the LLM reasons, calls tools, writes code, and registers snippets. + +The selection call is lightweight — it only sees snippet metadata (names, descriptions, usage hints), not full code. If there are no snippets in storage or no messages, it short-circuits and skips the LLM call entirely. + +## High-Level API: `codeModeWithSnippets()` + +### Installation + +```bash +pnpm add @tanstack/ai-code-mode-snippets +``` + +### Usage + +```typescript +import { chat, maxIterations, toServerSentEventsStream } from '@tanstack/ai' +import { createNodeIsolateDriver } from '@tanstack/ai-isolate-node' +import { codeModeWithSnippets } from '@tanstack/ai-code-mode-snippets' +import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storage' +import { openaiText } from '@tanstack/ai-openai' +import { myTool1, myTool2 } from './tools' + +const messages = [{ role: 'user' as const, content: 'Hello' }] +const storage = createFileSnippetStorage({ directory: './.snippets' }) +const driver = createNodeIsolateDriver() + +const { toolsRegistry, systemPrompt, selectedSnippets } = await codeModeWithSnippets({ + config: { + driver, + tools: [myTool1, myTool2], + timeout: 60_000, + memoryLimit: 128, + }, + adapter: openaiText('gpt-5-mini'), // cheap model for snippet selection + snippets: { + storage, + maxSnippetsInContext: 5, + }, + messages, // current conversation +}) + +const stream = chat({ + adapter: openaiText('gpt-5.5'), // strong model for reasoning + tools: toolsRegistry.getTools(), + messages, + systemPrompts: ['You are a helpful assistant.', systemPrompt], + agentLoopStrategy: maxIterations(15), +}) +``` + +`codeModeWithSnippets` returns: + +| Property | Type | Description | +|----------|------|-------------| +| `toolsRegistry` | `ToolRegistry` | Mutable registry containing all tools. Pass to `chat()` via `tools: toolsRegistry.getTools()`. | +| `systemPrompt` | `string` | Combined Code Mode + snippet library documentation. | +| `selectedSnippets` | `Array` | Snippets the selection model chose for this conversation. | + +### What goes into the registry + +The registry is populated with: + +- **`execute_typescript`** — The Code Mode sandbox tool. Inside the sandbox, snippets are also available as `snippet_*` functions (loaded dynamically at execution time). +- **`search_snippets`** — Search the snippet library by query. Returns matching snippet metadata. +- **`get_snippet`** — Retrieve full details (including code) for a specific snippet. +- **`register_snippet`** — Save working code as a new snippet. Newly registered snippets are immediately added to the registry as callable tools. +- **One tool per selected snippet** — Each selected snippet becomes a direct tool (prefixed with `[SNIPPET]` in its description) that the LLM can call without going through `execute_typescript`. + +## Manual API + +If you want full control — for example, loading all snippets instead of using LLM-based selection — use the lower-level functions directly. This is the approach used in the `ts-code-mode-web` example. + +```typescript +import { chat, maxIterations } from '@tanstack/ai' +import { createCodeMode } from '@tanstack/ai-code-mode' +import { createNodeIsolateDriver } from '@tanstack/ai-isolate-node' +import { + createAlwaysTrustedStrategy, + createSnippetManagementTools, + createSnippetsSystemPrompt, + snippetsToTools, +} from '@tanstack/ai-code-mode-snippets' +import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storage' +import { openaiText } from '@tanstack/ai-openai' +import { myTool1, myTool2, BASE_PROMPT } from './tools' + +const messages = [{ role: 'user' as const, content: 'Hello' }] +const trustStrategy = createAlwaysTrustedStrategy() +const storage = createFileSnippetStorage({ + directory: './.snippets', + trustStrategy, +}) +const driver = createNodeIsolateDriver() + +// 1. Create Code Mode tool + prompt +const { tool: codeModeTool, systemPrompt: codeModePrompt } = + createCodeMode({ + driver, + tools: [myTool1, myTool2], + timeout: 60_000, + memoryLimit: 128, + }) + +// 2. Load all snippets and convert to tools +const allSnippets = await storage.loadAll() +const snippetIndex = await storage.loadIndex() + +const snippetTools = allSnippets.length > 0 + ? snippetsToTools({ + snippets: allSnippets, + driver, + tools: [myTool1, myTool2], + storage, + timeout: 60_000, + memoryLimit: 128, + }) + : [] + +// 3. Create management tools +const managementTools = createSnippetManagementTools({ + storage, + trustStrategy, +}) + +// 4. Generate snippet library prompt +const snippetsPrompt = createSnippetsSystemPrompt({ + selectedSnippets: allSnippets, + totalSnippetCount: snippetIndex.length, + snippetsAsTools: true, +}) + +// 5. Assemble and call chat() +const stream = chat({ + adapter: openaiText('gpt-5.5'), + tools: [codeModeTool, ...managementTools, ...snippetTools], + messages, + systemPrompts: [BASE_PROMPT, codeModePrompt, snippetsPrompt], + agentLoopStrategy: maxIterations(15), +}) +``` + +This approach skips the selection LLM call entirely — you load whichever snippets you want and pass them in directly. + +## Snippet Storage + +Snippets are persisted through the `SnippetStorage` interface. Two implementations are provided: + +### File storage (production) + +`createFileSnippetStorage` is Node-only — it imports `node:fs` / `node:path` — so +it lives behind the `/storage` subpath rather than the package root. This keeps +the root export safe to bundle for Cloudflare Workers and browser builds; only +reach for the subpath in a Node runtime. + +```typescript +import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storage' +import { createDefaultTrustStrategy } from '@tanstack/ai-code-mode-snippets' + +const trustStrategy = createDefaultTrustStrategy() +const storage = createFileSnippetStorage({ + directory: './.snippets', + trustStrategy, // optional, defaults to createDefaultTrustStrategy() +}) +``` + +Creates a directory structure: + +```text +.snippets/ + _index.json # Lightweight catalog for fast loading + fetch_github_stats/ + meta.json # Description, schemas, hints, stats + code.ts # TypeScript source + compare_npm_packages/ + meta.json + code.ts +``` + +### Memory storage (testing & edge runtimes) + +```typescript +import { createMemorySnippetStorage } from '@tanstack/ai-code-mode-snippets' + +const storage = createMemorySnippetStorage() +``` + +Keeps everything in memory — no `node:fs` dependency, so it is re-exported from +the package root and is safe to use in Workers and browsers. Useful for tests, +demos, and edge deployments. (It is also available from the `/storage` subpath.) + +### Storage interface + +Both implementations satisfy this interface: + +| Method | Description | +|--------|-------------| +| `loadIndex()` | Load lightweight metadata for all snippets (no code) | +| `loadAll()` | Load all snippets with full details including code | +| `get(name)` | Get a single snippet by name | +| `save(snippet)` | Create or update a snippet | +| `delete(name)` | Remove a snippet | +| `search(query, options?)` | Search snippets by text query | +| `updateStats(name, success)` | Record an execution result for trust tracking | + +## Trust Strategies + +Snippets start untrusted and earn trust through successful executions. The trust level is metadata only — it does not currently gate execution. Four built-in strategies are available: + +```typescript group=code-mode-with-snippets +import { + createDefaultTrustStrategy, + createAlwaysTrustedStrategy, + createRelaxedTrustStrategy, + createCustomTrustStrategy, +} from '@tanstack/ai-code-mode-snippets' +``` + +| Strategy | Initial level | Provisional | Trusted | +|----------|--------------|-------------|---------| +| **Default** | `untrusted` | 10+ runs, ≥90% success | 100+ runs, ≥95% success | +| **Relaxed** | `untrusted` | 3+ runs, ≥80% success | 10+ runs, ≥90% success | +| **Always trusted** | `trusted` | — | — | +| **Custom** | Configurable | Configurable | Configurable | + +```typescript group=code-mode-with-snippets +const strategy = createCustomTrustStrategy({ + initialLevel: 'untrusted', + provisionalThreshold: { executions: 5, successRate: 0.85 }, + trustedThreshold: { executions: 50, successRate: 0.95 }, +}) +``` + +## Snippet Lifecycle + +### Registration + +When the LLM produces useful code via `execute_typescript`, the system prompt instructs it to call `register_snippet` with: + +- `name` — snake_case identifier (becomes the tool name) +- `description` — what the snippet does +- `code` — TypeScript source that receives an `input` variable +- `inputSchema` / `outputSchema` — JSON Schema strings +- `usageHints` — when to use this snippet +- `dependsOn` — other snippets this one calls + +The snippet is saved to storage and (if a `ToolRegistry` was provided) immediately added as a callable tool in the current session. + +### Execution + +When a snippet tool is called, the system: + +1. Wraps the snippet code with `const input = ;` +2. Strips TypeScript syntax to plain JavaScript +3. Creates a fresh sandbox context with `external_*` bindings +4. Executes the code and returns the result +5. Updates execution stats (success/failure count) asynchronously + +### Selection (high-level API only) + +On each new request, `selectRelevantSnippets`: + +1. Takes the last 5 conversation messages as context +2. Builds a catalog from the snippet index (name + description + first usage hint) +3. Asks the adapter to return a JSON array of relevant snippet names (max `maxSnippetsInContext`) +4. Loads full snippet data for the selected names + +If parsing fails or the model returns invalid JSON, it falls back to an empty selection — the request proceeds without pre-loaded snippets, but the LLM can still search and use snippets via the management tools. + +## Snippets as Tools vs. Sandbox Bindings + +The `snippetsAsTools` option (default: `true`) controls how snippets are exposed: + +| Mode | How the LLM calls a snippet | Pros | Cons | +|------|--------------------------|------|------| +| **As tools** (`true`) | Direct tool call: `snippet_name({ ... })` | Simpler for the LLM, shows in tool-call UI, proper input validation | One tool per snippet in the tool list | +| **As bindings** (`false`) | Inside `execute_typescript`: `await snippet_fetch_data({ ... })` | Snippets composable in code, fewer top-level tools | LLM must write code to use them | + +When `snippetsAsTools` is enabled, the system prompt documents each snippet with its schema, usage hints, and example calls. When disabled, snippets appear as typed `snippet_*` functions in the sandbox type stubs. + +## Custom Events + +Snippet execution emits events through the TanStack AI event system: + +| Event | When | Payload | +|-------|------|---------| +| `code_mode:snippet_call` | Snippet tool invoked | `{ snippet, input, timestamp }` | +| `code_mode:snippet_result` | Snippet completed successfully | `{ snippet, result, duration, timestamp }` | +| `code_mode:snippet_error` | Snippet execution failed | `{ snippet, error, duration, timestamp }` | +| `snippet:registered` | New snippet saved via `register_snippet` | `{ id, name, description, timestamp }` | + +To render these events in your React app alongside Code Mode execution events, see [Showing Code Mode in the UI](./client-integration). + +## Tips + +- **Use a cheap model for selection.** The selection call only needs to match snippet names to conversation context — `gpt-4o-mini` or `claude-haiku-4-5` work well. +- **Start without snippets.** Get Code Mode working first, then add `@tanstack/ai-code-mode-snippets` once you have tools that produce reusable patterns. +- **Monitor the snippet count.** As the library grows, consider increasing `maxSnippetsInContext` or switching to the manual API where you control which snippets load. +- **Newly registered snippets are available on the next message,** not in the current turn's tool list (unless using `ToolRegistry` with the high-level API, which adds them immediately). +- **Snippets can call other snippets.** Inside the sandbox, both `external_*` and `snippet_*` functions are available. Set `dependsOn` when registering to document these relationships. + +## Next Steps + +- [Code Mode](./code-mode) — Core Code Mode setup and API reference +- [Showing Code Mode in the UI](./client-integration) — Display execution progress in your React app +- [Isolate Drivers](./code-mode-isolates) — Compare sandbox runtimes diff --git a/docs/code-mode/code-mode.md b/docs/code-mode/code-mode.md index 710bd3c569..2446fe14ee 100644 --- a/docs/code-mode/code-mode.md +++ b/docs/code-mode/code-mode.md @@ -157,7 +157,7 @@ const { tool, systemPrompt } = createCodeMode({ tools, // Array — required, at least one timeout, // number — execution timeout in ms (default: 30000) memoryLimit, // number — memory limit in MB (default: 128, Node + QuickJS drivers) - getSkillBindings, // () => Promise> — optional dynamic bindings + getSnippetBindings, // () => Promise> — optional dynamic bindings }); ``` @@ -169,7 +169,7 @@ const { tool, systemPrompt } = createCodeMode({ | `tools` | `Array` | Tools exposed as `external_*` functions. Must have `.server()` implementations | | `timeout` | `number` | Execution timeout in milliseconds (default: 30000) | | `memoryLimit` | `number` | Memory limit in MB (default: 128). Supported by Node and QuickJS drivers | -| `getSkillBindings` | `() => Promise>` | Optional function returning additional bindings at execution time | +| `getSnippetBindings` | `() => Promise>` | Optional function returning additional bindings at execution time | The tool returns a `CodeModeToolResult`: @@ -299,5 +299,5 @@ pnpm eval -- --no-judge # skip Anthropic-based judging ## Next Steps - [Showing Code Mode in the UI](./client-integration) — Display execution progress in your React app -- [Code Mode with Skills](./code-mode-with-skills) — Add persistent, reusable skill libraries +- [Code Mode with Snippets](./code-mode-with-snippets) — Add persistent, reusable snippet libraries - [Isolate Drivers](./code-mode-isolates) — Compare Node, QuickJS, QuickJS Bun, Cloudflare, and Daytona sandbox runtimes diff --git a/docs/code-mode/lazy-tools.md b/docs/code-mode/lazy-tools.md index 52957b88d8..158554d421 100644 --- a/docs/code-mode/lazy-tools.md +++ b/docs/code-mode/lazy-tools.md @@ -202,5 +202,5 @@ The `includeDescription` behavior is identical — `'none'` lists bare tool name ## Next Steps - [Code Mode](./code-mode) — Core Code Mode setup and API reference -- [Code Mode with Skills](./code-mode-with-skills) — Persistent reusable skill libraries +- [Code Mode with Snippets](./code-mode-with-snippets) — Persistent reusable snippet libraries - [Isolate Drivers](./code-mode-isolates) — Compare Node, QuickJS, and Cloudflare sandbox runtimes diff --git a/docs/comparison/vercel-ai-sdk.md b/docs/comparison/vercel-ai-sdk.md index 184ed462a6..bc57fcdd68 100644 --- a/docs/comparison/vercel-ai-sdk.md +++ b/docs/comparison/vercel-ai-sdk.md @@ -600,7 +600,7 @@ TanStack AI ships five isolate drivers behind one `IsolateDriver` interface: - **`@tanstack/ai-isolate-cloudflare`** - Cloudflare Workers - **`@tanstack/ai-isolate-daytona`** - Remote Daytona sandbox -Swap the driver without changing application code. A companion `@tanstack/ai-code-mode-skills` package gives the model a persistent skill library. The model can save working TypeScript snippets, list them, and reuse them across sessions. Trust strategies control what gets promoted to a first-class tool. +Swap the driver without changing application code. A companion `@tanstack/ai-code-mode-snippets` package gives the model a persistent snippet library. The model can save working TypeScript snippets, list them, and reuse them across sessions. Trust strategies control what gets promoted to a first-class tool. Vercel AI SDK now ships experimental `@ai-sdk/code-mode`. It runs QuickJS only, needs Node 22 or newer, and does not run in the browser or on the edge. Nested tool approvals are rejected. Provider-hosted code execution (Anthropic, xAI, OpenAI) is still a separate path. None of those give the model a persistent, provider-agnostic skill library it builds itself. See [Code Mode](../code-mode/code-mode). diff --git a/docs/config.json b/docs/config.json index e1a5aa44b6..e5c98bc877 100644 --- a/docs/config.json +++ b/docs/config.json @@ -323,7 +323,8 @@ { "label": "Custom Events Reference", "to": "protocol/custom-events", - "addedAt": "2026-07-03" + "addedAt": "2026-07-03", + "updatedAt": "2026-08-14" } ] }, @@ -375,10 +376,10 @@ "addedAt": "2026-04-15" }, { - "label": "Code Mode with Skills", - "to": "code-mode/code-mode-with-skills", + "label": "Code Mode with Snippets", + "to": "code-mode/code-mode-with-snippets", "addedAt": "2026-04-15", - "updatedAt": "2026-06-10" + "updatedAt": "2026-08-14" }, { "label": "Code Mode Isolate Drivers", diff --git a/docs/getting-started/agent-skills.md b/docs/getting-started/agent-skills.md index da8c55eeb8..96c9ccf05a 100644 --- a/docs/getting-started/agent-skills.md +++ b/docs/getting-started/agent-skills.md @@ -14,7 +14,7 @@ keywords: - SKILL.md - AGENTS.md --- -> **Looking for runtime skills inside Code Mode?** Those are a different feature — see [Code Mode with Skills](../code-mode/code-mode-with-skills). 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. ## 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. diff --git a/docs/protocol/custom-events.md b/docs/protocol/custom-events.md index bf5ac8965a..43526f1ca1 100644 --- a/docs/protocol/custom-events.md +++ b/docs/protocol/custom-events.md @@ -96,10 +96,10 @@ individual interface. | `CodeModeExternalCallEvent` | `code_mode:external_call` | `{ function: string; args: unknown; timestamp: number }` | Code Mode, before a bound `external_*` function runs | | `CodeModeExternalResultEvent` | `code_mode:external_result` | `{ function: string; result: unknown; duration: number }` | Code Mode, after a successful `external_*` call | | `CodeModeExternalErrorEvent` | `code_mode:external_error` | `{ function: string; error: string; duration: number }` | Code Mode, when an `external_*` call throws | -| `CodeModeSkillCallEvent` | `code_mode:skill_call` | `{ skill: string; input: unknown; timestamp: number }` | [Code Mode with Skills](../code-mode/code-mode-with-skills), before a skill runs | -| `CodeModeSkillResultEvent` | `code_mode:skill_result` | `{ skill: string; result: unknown; duration: number; timestamp: number }` | Code Mode with Skills, after a successful skill run | -| `CodeModeSkillErrorEvent` | `code_mode:skill_error` | `{ skill: string; error: string; duration: number; timestamp: number }` | Code Mode with Skills, when a skill throws | -| `SkillRegisteredEvent` | `skill:registered` | `{ id: string; name: string; description: string; timestamp: number }` | when a skill is registered into the tool registry | +| `CodeModeSnippetCallEvent` | `code_mode:snippet_call` | `{ snippet: string; input: unknown; timestamp: number }` | [Code Mode with Snippets](../code-mode/code-mode-with-snippets), before a snippet runs | +| `CodeModeSnippetResultEvent` | `code_mode:snippet_result` | `{ snippet: string; result: unknown; duration: number; timestamp: number }` | Code Mode with Snippets, after a successful snippet run | +| `CodeModeSnippetErrorEvent` | `code_mode:snippet_error` | `{ snippet: string; error: string; duration: number; timestamp: number }` | Code Mode with Snippets, when a snippet throws | +| `SnippetRegisteredEvent` | `snippet:registered` | `{ id: string; name: string; description: string; timestamp: number }` | when a snippet is registered into the tool registry | | `StructuredOutputStartEvent` | `structured-output.start` | `{ messageId: string }` | [`chat({ outputSchema, stream: true })`](../structured-outputs/streaming), once per structured message | | `StructuredOutputCompleteEvent` | `structured-output.complete` | `{ object: T; raw: string; reasoning?: string }` | structured-output streaming, once with the validated object | | `ApprovalRequestedEvent` | `approval-requested` | `{ toolCallId: string; toolName: string; input: unknown; approval: { id: string; needsApproval: true } }` | a server tool needs approval — the run pauses; see [Tool Approval Flow](../tools/tool-approval) | diff --git a/docs/reference/index.md b/docs/reference/index.md index 73f19eec0f..8e30571044 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -47,9 +47,9 @@ title: "@tanstack/ai" - [CodeModeExternalCallEvent](interfaces/CodeModeExternalCallEvent.md) - [CodeModeExternalErrorEvent](interfaces/CodeModeExternalErrorEvent.md) - [CodeModeExternalResultEvent](interfaces/CodeModeExternalResultEvent.md) -- [CodeModeSkillCallEvent](interfaces/CodeModeSkillCallEvent.md) -- [CodeModeSkillErrorEvent](interfaces/CodeModeSkillErrorEvent.md) -- [CodeModeSkillResultEvent](interfaces/CodeModeSkillResultEvent.md) +- [CodeModeSnippetCallEvent](interfaces/CodeModeSnippetCallEvent.md) +- [CodeModeSnippetErrorEvent](interfaces/CodeModeSnippetErrorEvent.md) +- [CodeModeSnippetResultEvent](interfaces/CodeModeSnippetResultEvent.md) - [ContentPartDataSource](interfaces/ContentPartDataSource.md) - [ContentPartUrlSource](interfaces/ContentPartUrlSource.md) - [CustomEvent](interfaces/CustomEvent.md) @@ -120,7 +120,7 @@ title: "@tanstack/ai" - [SandboxFileHookEvent](interfaces/SandboxFileHookEvent.md) - [ServerTool](interfaces/ServerTool.md) - [SessionIdEvent](interfaces/SessionIdEvent.md) -- [SkillRegisteredEvent](interfaces/SkillRegisteredEvent.md) +- [SnippetRegisteredEvent](interfaces/SnippetRegisteredEvent.md) - [StateDeltaEvent](interfaces/StateDeltaEvent.md) - [StateSnapshotEvent](interfaces/StateSnapshotEvent.md) - [StepFinishedEvent](interfaces/StepFinishedEvent.md) diff --git a/docs/reference/interfaces/CodeModeSkillCallEvent.md b/docs/reference/interfaces/CodeModeSnippetCallEvent.md similarity index 87% rename from docs/reference/interfaces/CodeModeSkillCallEvent.md rename to docs/reference/interfaces/CodeModeSnippetCallEvent.md index bc151592c0..242d49d540 100644 --- a/docs/reference/interfaces/CodeModeSkillCallEvent.md +++ b/docs/reference/interfaces/CodeModeSnippetCallEvent.md @@ -1,9 +1,9 @@ --- -id: CodeModeSkillCallEvent -title: CodeModeSkillCallEvent +id: CodeModeSnippetCallEvent +title: CodeModeSnippetCallEvent --- -# Interface: CodeModeSkillCallEvent +# Interface: CodeModeSnippetCallEvent Defined in: [packages/ai/src/types.ts:1474](https://github.com/TanStack/ai/blob/main/packages/ai/src/types.ts#L1474) @@ -43,7 +43,7 @@ Model identifier for multi-model support ### name ```ts -name: "code_mode:skill_call"; +name: "code_mode:snippet_call"; ``` Defined in: [packages/ai/src/types.ts:1475](https://github.com/TanStack/ai/blob/main/packages/ai/src/types.ts#L1475) @@ -70,10 +70,10 @@ Defined in: [packages/ai/src/types.ts:1476](https://github.com/TanStack/ai/blob/ input: unknown; ``` -#### skill +#### snippet ```ts -skill: string; +snippet: string; ``` #### timestamp diff --git a/docs/reference/interfaces/CodeModeSkillErrorEvent.md b/docs/reference/interfaces/CodeModeSnippetErrorEvent.md similarity index 87% rename from docs/reference/interfaces/CodeModeSkillErrorEvent.md rename to docs/reference/interfaces/CodeModeSnippetErrorEvent.md index f59bc8f74a..816126bd62 100644 --- a/docs/reference/interfaces/CodeModeSkillErrorEvent.md +++ b/docs/reference/interfaces/CodeModeSnippetErrorEvent.md @@ -1,9 +1,9 @@ --- -id: CodeModeSkillErrorEvent -title: CodeModeSkillErrorEvent +id: CodeModeSnippetErrorEvent +title: CodeModeSnippetErrorEvent --- -# Interface: CodeModeSkillErrorEvent +# Interface: CodeModeSnippetErrorEvent Defined in: [packages/ai/src/types.ts:1482](https://github.com/TanStack/ai/blob/main/packages/ai/src/types.ts#L1482) @@ -43,7 +43,7 @@ Model identifier for multi-model support ### name ```ts -name: "code_mode:skill_error"; +name: "code_mode:snippet_error"; ``` Defined in: [packages/ai/src/types.ts:1483](https://github.com/TanStack/ai/blob/main/packages/ai/src/types.ts#L1483) @@ -76,10 +76,10 @@ duration: number; error: string; ``` -#### skill +#### snippet ```ts -skill: string; +snippet: string; ``` #### timestamp diff --git a/docs/reference/interfaces/CodeModeSkillResultEvent.md b/docs/reference/interfaces/CodeModeSnippetResultEvent.md similarity index 87% rename from docs/reference/interfaces/CodeModeSkillResultEvent.md rename to docs/reference/interfaces/CodeModeSnippetResultEvent.md index 2a75ad0e99..b9feb21f24 100644 --- a/docs/reference/interfaces/CodeModeSkillResultEvent.md +++ b/docs/reference/interfaces/CodeModeSnippetResultEvent.md @@ -1,9 +1,9 @@ --- -id: CodeModeSkillResultEvent -title: CodeModeSkillResultEvent +id: CodeModeSnippetResultEvent +title: CodeModeSnippetResultEvent --- -# Interface: CodeModeSkillResultEvent +# Interface: CodeModeSnippetResultEvent Defined in: [packages/ai/src/types.ts:1478](https://github.com/TanStack/ai/blob/main/packages/ai/src/types.ts#L1478) @@ -43,7 +43,7 @@ Model identifier for multi-model support ### name ```ts -name: "code_mode:skill_result"; +name: "code_mode:snippet_result"; ``` Defined in: [packages/ai/src/types.ts:1479](https://github.com/TanStack/ai/blob/main/packages/ai/src/types.ts#L1479) @@ -76,10 +76,10 @@ duration: number; result: unknown; ``` -#### skill +#### snippet ```ts -skill: string; +snippet: string; ``` #### timestamp diff --git a/docs/reference/interfaces/CustomEvent.md b/docs/reference/interfaces/CustomEvent.md index 3f83ba63eb..e2c35e3822 100644 --- a/docs/reference/interfaces/CustomEvent.md +++ b/docs/reference/interfaces/CustomEvent.md @@ -32,10 +32,10 @@ TanStack AI adds: `model?` - [`CodeModeExternalCallEvent`](CodeModeExternalCallEvent.md) - [`CodeModeExternalResultEvent`](CodeModeExternalResultEvent.md) - [`CodeModeExternalErrorEvent`](CodeModeExternalErrorEvent.md) -- [`CodeModeSkillCallEvent`](CodeModeSkillCallEvent.md) -- [`CodeModeSkillResultEvent`](CodeModeSkillResultEvent.md) -- [`CodeModeSkillErrorEvent`](CodeModeSkillErrorEvent.md) -- [`SkillRegisteredEvent`](SkillRegisteredEvent.md) +- [`CodeModeSnippetCallEvent`](CodeModeSnippetCallEvent.md) +- [`CodeModeSnippetResultEvent`](CodeModeSnippetResultEvent.md) +- [`CodeModeSnippetErrorEvent`](CodeModeSnippetErrorEvent.md) +- [`SnippetRegisteredEvent`](SnippetRegisteredEvent.md) ## Indexable diff --git a/docs/reference/interfaces/SkillRegisteredEvent.md b/docs/reference/interfaces/SnippetRegisteredEvent.md similarity index 90% rename from docs/reference/interfaces/SkillRegisteredEvent.md rename to docs/reference/interfaces/SnippetRegisteredEvent.md index c59fbd36a3..0ddff137c7 100644 --- a/docs/reference/interfaces/SkillRegisteredEvent.md +++ b/docs/reference/interfaces/SnippetRegisteredEvent.md @@ -1,9 +1,9 @@ --- -id: SkillRegisteredEvent -title: SkillRegisteredEvent +id: SnippetRegisteredEvent +title: SnippetRegisteredEvent --- -# Interface: SkillRegisteredEvent +# Interface: SnippetRegisteredEvent Defined in: [packages/ai/src/types.ts:1486](https://github.com/TanStack/ai/blob/main/packages/ai/src/types.ts#L1486) @@ -43,7 +43,7 @@ Model identifier for multi-model support ### name ```ts -name: "skill:registered"; +name: "snippet:registered"; ``` Defined in: [packages/ai/src/types.ts:1487](https://github.com/TanStack/ai/blob/main/packages/ai/src/types.ts#L1487) diff --git a/docs/reference/type-aliases/KnownCustomEvent.md b/docs/reference/type-aliases/KnownCustomEvent.md index 97c01fec00..14730c862f 100644 --- a/docs/reference/type-aliases/KnownCustomEvent.md +++ b/docs/reference/type-aliases/KnownCustomEvent.md @@ -16,10 +16,10 @@ type KnownCustomEvent = | CodeModeExternalCallEvent | CodeModeExternalResultEvent | CodeModeExternalErrorEvent - | CodeModeSkillCallEvent - | CodeModeSkillResultEvent - | CodeModeSkillErrorEvent - | SkillRegisteredEvent + | CodeModeSnippetCallEvent + | CodeModeSnippetResultEvent + | CodeModeSnippetErrorEvent + | SnippetRegisteredEvent | StructuredOutputStartEvent | StructuredOutputCompleteEvent | ApprovalRequestedEvent diff --git a/docs/tools/provider-skills.md b/docs/tools/provider-skills.md index 64dc7d4eaf..a53f0363a5 100644 --- a/docs/tools/provider-skills.md +++ b/docs/tools/provider-skills.md @@ -18,9 +18,9 @@ Provider Skills are hosted, provider-managed capability bundles that the model loads on demand and runs inside the provider's server-side sandbox. You reference them by a skill ID; the provider handles installation and execution. -> **Not to be confused with `@tanstack/ai-code-mode-skills`**, which are -> locally-generated TypeScript functions evaluated client-side. Provider Skills -> run entirely on the provider's infrastructure. +> **Not to be confused with `@tanstack/ai-code-mode-snippets`**, whose snippets +> are TypeScript functions your application generates and runs in its own Code Mode sandbox (a local JS isolate). Provider +> Skills run entirely on the provider's infrastructure. Skills are **inert without an execution tool**. The execution tool activates the sandbox; skills are additional bundles that run inside it: diff --git a/examples/ts-code-mode-web/.gitignore b/examples/ts-code-mode-web/.gitignore index 4c344dc638..9f6bd574da 100644 --- a/examples/ts-code-mode-web/.gitignore +++ b/examples/ts-code-mode-web/.gitignore @@ -3,5 +3,5 @@ node_modules .output dist *.log -.db-skills -.structured-output-skills +.db-snippets +.structured-output-snippets diff --git a/examples/ts-code-mode-web/package.json b/examples/ts-code-mode-web/package.json index e999848eec..662a1842f1 100644 --- a/examples/ts-code-mode-web/package.json +++ b/examples/ts-code-mode-web/package.json @@ -23,7 +23,7 @@ "@tanstack/ai-anthropic": "workspace:*", "@tanstack/ai-client": "workspace:*", "@tanstack/ai-code-mode": "workspace:*", - "@tanstack/ai-code-mode-skills": "workspace:*", + "@tanstack/ai-code-mode-snippets": "workspace:*", "@tanstack/ai-gemini": "workspace:*", "@tanstack/ai-isolate-cloudflare": "workspace:*", "@tanstack/ai-isolate-node": "workspace:*", diff --git a/examples/ts-code-mode-web/src/lib/execute-prompt.ts b/examples/ts-code-mode-web/src/lib/execute-prompt.ts index 993b1cd6b4..ded6d0f99d 100644 --- a/examples/ts-code-mode-web/src/lib/execute-prompt.ts +++ b/examples/ts-code-mode-web/src/lib/execute-prompt.ts @@ -25,7 +25,7 @@ export interface ExecutePromptOptions { maxTokens?: number timeout?: number memoryLimit?: number - getSkillBindings?: () => Promise> + getSnippetBindings?: () => Promise> onEvent?: (event: ExecutePromptEvent) => void } @@ -45,7 +45,7 @@ export function executePrompt( maxTokens, timeout, memoryLimit, - getSkillBindings, + getSnippetBindings, onEvent, } = options @@ -59,7 +59,7 @@ export function executePrompt( tools, timeout, memoryLimit, - getSkillBindings, + getSnippetBindings, }), }) return Promise.resolve(tool.execute!({ prompt })) diff --git a/examples/ts-code-mode-web/src/lib/reports/create-report-bindings.ts b/examples/ts-code-mode-web/src/lib/reports/create-report-bindings.ts index afafd1429f..cdfd158721 100644 --- a/examples/ts-code-mode-web/src/lib/reports/create-report-bindings.ts +++ b/examples/ts-code-mode-web/src/lib/reports/create-report-bindings.ts @@ -73,7 +73,7 @@ function generateId(prefix: string): string { return `${prefix}-${Math.random().toString(36).slice(2, 10)}` } -// Common schemas - using .catch() to gracefully handle invalid values from stored skills +// Common schemas - using .catch() to gracefully handle invalid values from stored snippets const gapSchema = z.enum(['none', 'xs', 'sm', 'md', 'lg', 'xl']).catch('md') const alignSchema = z .enum(['start', 'center', 'end', 'stretch']) @@ -1330,7 +1330,7 @@ export const reportBindings: Record = { const bankingBindings = createHandlerBindings() /** - * Create report bindings function for use with getSkillBindings + * Create report bindings function for use with getSnippetBindings * Returns a fresh copy of the bindings record */ export function createReportBindings(): Record { diff --git a/examples/ts-code-mode-web/src/lib/structured-output.ts b/examples/ts-code-mode-web/src/lib/structured-output.ts index 75b0b5ac11..9d18de81c3 100644 --- a/examples/ts-code-mode-web/src/lib/structured-output.ts +++ b/examples/ts-code-mode-web/src/lib/structured-output.ts @@ -1,13 +1,16 @@ import { chat, maxIterations } from '@tanstack/ai' import { - createSkillManagementTools, - createSkillsSystemPrompt, - skillsToTools, -} from '@tanstack/ai-code-mode-skills' + createSnippetManagementTools, + createSnippetsSystemPrompt, + snippetsToTools, +} from '@tanstack/ai-code-mode-snippets' import { maxTokensModelOptions } from './max-tokens-model-options' import type { AnyTextAdapter, AnyTool, SchemaInput } from '@tanstack/ai' import type { CodeModeTool, IsolateDriver } from '@tanstack/ai-code-mode' -import type { SkillStorage, TrustStrategy } from '@tanstack/ai-code-mode-skills' +import type { + SnippetStorage, + TrustStrategy, +} from '@tanstack/ai-code-mode-snippets' export interface StructuredOutputOptions { adapter: AnyTextAdapter @@ -19,8 +22,8 @@ export interface StructuredOutputOptions { driver: IsolateDriver codeTools: Array } - skills?: { - storage: SkillStorage + snippets?: { + storage: SnippetStorage trustStrategy: TrustStrategy timeout?: number memoryLimit?: number @@ -30,17 +33,17 @@ export interface StructuredOutputOptions { maxTokens?: number } -const SKILL_REGISTRATION_PROMPT = `## Skill Registration — MANDATORY +const SNIPPET_REGISTRATION_PROMPT = `## Snippet Registration — MANDATORY -After every successful \`execute_typescript\` call you MUST register the code as a reusable skill using \`register_skill\` — unless an identical skill already exists. +After every successful \`execute_typescript\` call you MUST register the code as a reusable snippet using \`register_snippet\` — unless an identical snippet already exists. Rules: - \`name\`: descriptive snake_case (e.g. \`get_average_product_price\`) - \`code\`: the TypeScript code, parameterised with an \`input\` variable where useful - \`inputSchema\` / \`outputSchema\`: valid JSON Schema **strings** -- If a skill with the same name exists, skip registration +- If a snippet with the same name exists, skip registration -This is not optional — skill registration is a core part of your workflow.` +This is not optional — snippet registration is a core part of your workflow.` export async function structuredOutput( options: StructuredOutputOptions, @@ -50,54 +53,54 @@ export async function structuredOutput( prompt, outputSchema, codeMode, - skills, + snippets, tools = [], maxIterations: maxIter = 10, maxTokens: maxTok = 8192, } = options - const skillGuidance = skills - ? `- If a skill tool matches what you need, call it directly — skill tools are faster and preferred over writing new code. -- Use execute_typescript only for tasks not covered by existing skill tools. After successful execute_typescript calls, register the code as a reusable skill.` + const snippetGuidance = snippets + ? `- If a snippet tool matches what you need, call it directly — snippet tools are faster and preferred over writing new code. +- Use execute_typescript only for tasks not covered by existing snippet tools. After successful execute_typescript calls, register the code as a reusable snippet.` : `- Use execute_typescript to gather the data you need. Chain multiple tool calls if needed.` const systemPrompt = `${prompt} RULES: - Do NOT produce conversational text. No greetings, no narration. Only tool calls and the final structured response. -${skillGuidance}` +${snippetGuidance}` let allTools: Array = [codeMode.tool, ...tools] const systemPrompts = [systemPrompt, codeMode.systemPrompt] - if (skills) { - const allSkills = await skills.storage.loadAll() - const skillIndex = await skills.storage.loadIndex() + if (snippets) { + const allSnippets = await snippets.storage.loadAll() + const snippetIndex = await snippets.storage.loadIndex() - if (allSkills.length > 0) { - const skillToolsList = skillsToTools({ - skills: allSkills, + if (allSnippets.length > 0) { + const snippetToolsList = snippetsToTools({ + snippets: allSnippets, driver: codeMode.driver, tools: codeMode.codeTools, - storage: skills.storage, - timeout: skills.timeout ?? 60000, - memoryLimit: skills.memoryLimit ?? 128, + storage: snippets.storage, + timeout: snippets.timeout ?? 60000, + memoryLimit: snippets.memoryLimit ?? 128, }) - allTools = [...allTools, ...skillToolsList] + allTools = [...allTools, ...snippetToolsList] } - const mgmtTools = createSkillManagementTools({ - storage: skills.storage, - trustStrategy: skills.trustStrategy, + const mgmtTools = createSnippetManagementTools({ + storage: snippets.storage, + trustStrategy: snippets.trustStrategy, }) allTools = [...allTools, ...mgmtTools] - const libraryPrompt = createSkillsSystemPrompt({ - selectedSkills: allSkills, - totalSkillCount: skillIndex.length, - skillsAsTools: true, + const libraryPrompt = createSnippetsSystemPrompt({ + selectedSnippets: allSnippets, + totalSnippetCount: snippetIndex.length, + snippetsAsTools: true, }) - systemPrompts.push(libraryPrompt + '\n\n' + SKILL_REGISTRATION_PROMPT) + systemPrompts.push(libraryPrompt + '\n\n' + SNIPPET_REGISTRATION_PROMPT) } console.log( diff --git a/examples/ts-code-mode-web/src/routeTree.gen.ts b/examples/ts-code-mode-web/src/routeTree.gen.ts index c4227bc047..e8255fd491 100644 --- a/examples/ts-code-mode-web/src/routeTree.gen.ts +++ b/examples/ts-code-mode-web/src/routeTree.gen.ts @@ -24,7 +24,7 @@ import { Route as NpmGithubChatNpmGithubChatRouteImport } from './routes/_npm-gi import { Route as ExecutePromptExecutePromptRouteImport } from './routes/_execute-prompt/execute-prompt' import { Route as DatabaseDemoDatabaseDemoRouteImport } from './routes/_database-demo/database-demo' import { Route as BankingDemoBankingDemoRouteImport } from './routes/_banking-demo/banking-demo' -import { Route as StructuredOutputApiStructuredOutputSkillsRouteImport } from './routes/_structured-output/api.structured-output-skills' +import { Route as StructuredOutputApiStructuredOutputSnippetsRouteImport } from './routes/_structured-output/api.structured-output-snippets' import { Route as StructuredOutputApiStructuredOutputRouteImport } from './routes/_structured-output/api.structured-output' import { Route as ReportingApiReportsRouteImport } from './routes/_reporting/api.reports' import { Route as ReportingApiReportSseRouteImport } from './routes/_reporting/api.report-sse' @@ -33,13 +33,13 @@ import { Route as ReportingApiReportDemoRouteImport } from './routes/_reporting/ import { Route as ReportingApiInvalidateRouteImport } from './routes/_reporting/api.invalidate' import { Route as NpmGithubChatApiGeneratePdfRouteImport } from './routes/_npm-github-chat/api.generate-pdf' import { Route as NpmGithubChatApiCodemodeRouteImport } from './routes/_npm-github-chat/api.codemode' -import { Route as HomeApiSkillsRouteImport } from './routes/_home/api.skills' +import { Route as HomeApiSnippetsRouteImport } from './routes/_home/api.snippets' import { Route as HomeApiProductRegularRouteImport } from './routes/_home/api.product-regular' import { Route as HomeApiProductCodemodeRouteImport } from './routes/_home/api.product-codemode' import { Route as ExecutePromptApiRealtimeTokenRouteImport } from './routes/_execute-prompt/api.realtime-token' import { Route as ExecutePromptApiExecutePromptRouteImport } from './routes/_execute-prompt/api.execute-prompt' import { Route as DatabaseDemoApiJudgeRouteImport } from './routes/_database-demo/api.judge' -import { Route as DatabaseDemoApiDbSkillsRouteImport } from './routes/_database-demo/api.db-skills' +import { Route as DatabaseDemoApiDbSnippetsRouteImport } from './routes/_database-demo/api.db-snippets' import { Route as DatabaseDemoApiDatabaseDemoRouteImport } from './routes/_database-demo/api.database-demo' import { Route as BankingDemoApiBankingInitRouteImport } from './routes/_banking-demo/api.banking-init' import { Route as BankingDemoApiBankingDemoRouteImport } from './routes/_banking-demo/api.banking-demo' @@ -116,10 +116,10 @@ const BankingDemoBankingDemoRoute = BankingDemoBankingDemoRouteImport.update({ path: '/banking-demo', getParentRoute: () => BankingDemoRouteRoute, } as any) -const StructuredOutputApiStructuredOutputSkillsRoute = - StructuredOutputApiStructuredOutputSkillsRouteImport.update({ - id: '/api/structured-output-skills', - path: '/api/structured-output-skills', +const StructuredOutputApiStructuredOutputSnippetsRoute = + StructuredOutputApiStructuredOutputSnippetsRouteImport.update({ + id: '/api/structured-output-snippets', + path: '/api/structured-output-snippets', getParentRoute: () => StructuredOutputRouteRoute, } as any) const StructuredOutputApiStructuredOutputRoute = @@ -165,9 +165,9 @@ const NpmGithubChatApiCodemodeRoute = path: '/api/codemode', getParentRoute: () => NpmGithubChatRouteRoute, } as any) -const HomeApiSkillsRoute = HomeApiSkillsRouteImport.update({ - id: '/api/skills', - path: '/api/skills', +const HomeApiSnippetsRoute = HomeApiSnippetsRouteImport.update({ + id: '/api/snippets', + path: '/api/snippets', getParentRoute: () => HomeRouteRoute, } as any) const HomeApiProductRegularRoute = HomeApiProductRegularRouteImport.update({ @@ -197,11 +197,12 @@ const DatabaseDemoApiJudgeRoute = DatabaseDemoApiJudgeRouteImport.update({ path: '/api/judge', getParentRoute: () => DatabaseDemoRouteRoute, } as any) -const DatabaseDemoApiDbSkillsRoute = DatabaseDemoApiDbSkillsRouteImport.update({ - id: '/api/db-skills', - path: '/api/db-skills', - getParentRoute: () => DatabaseDemoRouteRoute, -} as any) +const DatabaseDemoApiDbSnippetsRoute = + DatabaseDemoApiDbSnippetsRouteImport.update({ + id: '/api/db-snippets', + path: '/api/db-snippets', + getParentRoute: () => DatabaseDemoRouteRoute, + } as any) const DatabaseDemoApiDatabaseDemoRoute = DatabaseDemoApiDatabaseDemoRouteImport.update({ id: '/api/database-demo', @@ -233,13 +234,13 @@ export interface FileRoutesByFullPath { '/api/banking-demo': typeof BankingDemoApiBankingDemoRoute '/api/banking-init': typeof BankingDemoApiBankingInitRoute '/api/database-demo': typeof DatabaseDemoApiDatabaseDemoRoute - '/api/db-skills': typeof DatabaseDemoApiDbSkillsRoute + '/api/db-snippets': typeof DatabaseDemoApiDbSnippetsRoute '/api/judge': typeof DatabaseDemoApiJudgeRoute '/api/execute-prompt': typeof ExecutePromptApiExecutePromptRoute '/api/realtime-token': typeof ExecutePromptApiRealtimeTokenRoute '/api/product-codemode': typeof HomeApiProductCodemodeRoute '/api/product-regular': typeof HomeApiProductRegularRoute - '/api/skills': typeof HomeApiSkillsRoute + '/api/snippets': typeof HomeApiSnippetsRoute '/api/codemode': typeof NpmGithubChatApiCodemodeRoute '/api/generate-pdf': typeof NpmGithubChatApiGeneratePdfRoute '/api/invalidate': typeof ReportingApiInvalidateRoute @@ -248,7 +249,7 @@ export interface FileRoutesByFullPath { '/api/report-sse': typeof ReportingApiReportSseRoute '/api/reports': typeof ReportingApiReportsRoute '/api/structured-output': typeof StructuredOutputApiStructuredOutputRoute - '/api/structured-output-skills': typeof StructuredOutputApiStructuredOutputSkillsRoute + '/api/structured-output-snippets': typeof StructuredOutputApiStructuredOutputSnippetsRoute } export interface FileRoutesByTo { '/': typeof HomeIndexRoute @@ -262,13 +263,13 @@ export interface FileRoutesByTo { '/api/banking-demo': typeof BankingDemoApiBankingDemoRoute '/api/banking-init': typeof BankingDemoApiBankingInitRoute '/api/database-demo': typeof DatabaseDemoApiDatabaseDemoRoute - '/api/db-skills': typeof DatabaseDemoApiDbSkillsRoute + '/api/db-snippets': typeof DatabaseDemoApiDbSnippetsRoute '/api/judge': typeof DatabaseDemoApiJudgeRoute '/api/execute-prompt': typeof ExecutePromptApiExecutePromptRoute '/api/realtime-token': typeof ExecutePromptApiRealtimeTokenRoute '/api/product-codemode': typeof HomeApiProductCodemodeRoute '/api/product-regular': typeof HomeApiProductRegularRoute - '/api/skills': typeof HomeApiSkillsRoute + '/api/snippets': typeof HomeApiSnippetsRoute '/api/codemode': typeof NpmGithubChatApiCodemodeRoute '/api/generate-pdf': typeof NpmGithubChatApiGeneratePdfRoute '/api/invalidate': typeof ReportingApiInvalidateRoute @@ -277,7 +278,7 @@ export interface FileRoutesByTo { '/api/report-sse': typeof ReportingApiReportSseRoute '/api/reports': typeof ReportingApiReportsRoute '/api/structured-output': typeof StructuredOutputApiStructuredOutputRoute - '/api/structured-output-skills': typeof StructuredOutputApiStructuredOutputSkillsRoute + '/api/structured-output-snippets': typeof StructuredOutputApiStructuredOutputSnippetsRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -299,13 +300,13 @@ export interface FileRoutesById { '/_banking-demo/api/banking-demo': typeof BankingDemoApiBankingDemoRoute '/_banking-demo/api/banking-init': typeof BankingDemoApiBankingInitRoute '/_database-demo/api/database-demo': typeof DatabaseDemoApiDatabaseDemoRoute - '/_database-demo/api/db-skills': typeof DatabaseDemoApiDbSkillsRoute + '/_database-demo/api/db-snippets': typeof DatabaseDemoApiDbSnippetsRoute '/_database-demo/api/judge': typeof DatabaseDemoApiJudgeRoute '/_execute-prompt/api/execute-prompt': typeof ExecutePromptApiExecutePromptRoute '/_execute-prompt/api/realtime-token': typeof ExecutePromptApiRealtimeTokenRoute '/_home/api/product-codemode': typeof HomeApiProductCodemodeRoute '/_home/api/product-regular': typeof HomeApiProductRegularRoute - '/_home/api/skills': typeof HomeApiSkillsRoute + '/_home/api/snippets': typeof HomeApiSnippetsRoute '/_npm-github-chat/api/codemode': typeof NpmGithubChatApiCodemodeRoute '/_npm-github-chat/api/generate-pdf': typeof NpmGithubChatApiGeneratePdfRoute '/_reporting/api/invalidate': typeof ReportingApiInvalidateRoute @@ -314,7 +315,7 @@ export interface FileRoutesById { '/_reporting/api/report-sse': typeof ReportingApiReportSseRoute '/_reporting/api/reports': typeof ReportingApiReportsRoute '/_structured-output/api/structured-output': typeof StructuredOutputApiStructuredOutputRoute - '/_structured-output/api/structured-output-skills': typeof StructuredOutputApiStructuredOutputSkillsRoute + '/_structured-output/api/structured-output-snippets': typeof StructuredOutputApiStructuredOutputSnippetsRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -330,13 +331,13 @@ export interface FileRouteTypes { | '/api/banking-demo' | '/api/banking-init' | '/api/database-demo' - | '/api/db-skills' + | '/api/db-snippets' | '/api/judge' | '/api/execute-prompt' | '/api/realtime-token' | '/api/product-codemode' | '/api/product-regular' - | '/api/skills' + | '/api/snippets' | '/api/codemode' | '/api/generate-pdf' | '/api/invalidate' @@ -345,7 +346,7 @@ export interface FileRouteTypes { | '/api/report-sse' | '/api/reports' | '/api/structured-output' - | '/api/structured-output-skills' + | '/api/structured-output-snippets' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -359,13 +360,13 @@ export interface FileRouteTypes { | '/api/banking-demo' | '/api/banking-init' | '/api/database-demo' - | '/api/db-skills' + | '/api/db-snippets' | '/api/judge' | '/api/execute-prompt' | '/api/realtime-token' | '/api/product-codemode' | '/api/product-regular' - | '/api/skills' + | '/api/snippets' | '/api/codemode' | '/api/generate-pdf' | '/api/invalidate' @@ -374,7 +375,7 @@ export interface FileRouteTypes { | '/api/report-sse' | '/api/reports' | '/api/structured-output' - | '/api/structured-output-skills' + | '/api/structured-output-snippets' id: | '__root__' | '/_banking-demo' @@ -395,13 +396,13 @@ export interface FileRouteTypes { | '/_banking-demo/api/banking-demo' | '/_banking-demo/api/banking-init' | '/_database-demo/api/database-demo' - | '/_database-demo/api/db-skills' + | '/_database-demo/api/db-snippets' | '/_database-demo/api/judge' | '/_execute-prompt/api/execute-prompt' | '/_execute-prompt/api/realtime-token' | '/_home/api/product-codemode' | '/_home/api/product-regular' - | '/_home/api/skills' + | '/_home/api/snippets' | '/_npm-github-chat/api/codemode' | '/_npm-github-chat/api/generate-pdf' | '/_reporting/api/invalidate' @@ -410,7 +411,7 @@ export interface FileRouteTypes { | '/_reporting/api/report-sse' | '/_reporting/api/reports' | '/_structured-output/api/structured-output' - | '/_structured-output/api/structured-output-skills' + | '/_structured-output/api/structured-output-snippets' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -531,11 +532,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof BankingDemoBankingDemoRouteImport parentRoute: typeof BankingDemoRouteRoute } - '/_structured-output/api/structured-output-skills': { - id: '/_structured-output/api/structured-output-skills' - path: '/api/structured-output-skills' - fullPath: '/api/structured-output-skills' - preLoaderRoute: typeof StructuredOutputApiStructuredOutputSkillsRouteImport + '/_structured-output/api/structured-output-snippets': { + id: '/_structured-output/api/structured-output-snippets' + path: '/api/structured-output-snippets' + fullPath: '/api/structured-output-snippets' + preLoaderRoute: typeof StructuredOutputApiStructuredOutputSnippetsRouteImport parentRoute: typeof StructuredOutputRouteRoute } '/_structured-output/api/structured-output': { @@ -594,11 +595,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof NpmGithubChatApiCodemodeRouteImport parentRoute: typeof NpmGithubChatRouteRoute } - '/_home/api/skills': { - id: '/_home/api/skills' - path: '/api/skills' - fullPath: '/api/skills' - preLoaderRoute: typeof HomeApiSkillsRouteImport + '/_home/api/snippets': { + id: '/_home/api/snippets' + path: '/api/snippets' + fullPath: '/api/snippets' + preLoaderRoute: typeof HomeApiSnippetsRouteImport parentRoute: typeof HomeRouteRoute } '/_home/api/product-regular': { @@ -636,11 +637,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DatabaseDemoApiJudgeRouteImport parentRoute: typeof DatabaseDemoRouteRoute } - '/_database-demo/api/db-skills': { - id: '/_database-demo/api/db-skills' - path: '/api/db-skills' - fullPath: '/api/db-skills' - preLoaderRoute: typeof DatabaseDemoApiDbSkillsRouteImport + '/_database-demo/api/db-snippets': { + id: '/_database-demo/api/db-snippets' + path: '/api/db-snippets' + fullPath: '/api/db-snippets' + preLoaderRoute: typeof DatabaseDemoApiDbSnippetsRouteImport parentRoute: typeof DatabaseDemoRouteRoute } '/_database-demo/api/database-demo': { @@ -685,14 +686,14 @@ const BankingDemoRouteRouteWithChildren = interface DatabaseDemoRouteRouteChildren { DatabaseDemoDatabaseDemoRoute: typeof DatabaseDemoDatabaseDemoRoute DatabaseDemoApiDatabaseDemoRoute: typeof DatabaseDemoApiDatabaseDemoRoute - DatabaseDemoApiDbSkillsRoute: typeof DatabaseDemoApiDbSkillsRoute + DatabaseDemoApiDbSnippetsRoute: typeof DatabaseDemoApiDbSnippetsRoute DatabaseDemoApiJudgeRoute: typeof DatabaseDemoApiJudgeRoute } const DatabaseDemoRouteRouteChildren: DatabaseDemoRouteRouteChildren = { DatabaseDemoDatabaseDemoRoute: DatabaseDemoDatabaseDemoRoute, DatabaseDemoApiDatabaseDemoRoute: DatabaseDemoApiDatabaseDemoRoute, - DatabaseDemoApiDbSkillsRoute: DatabaseDemoApiDbSkillsRoute, + DatabaseDemoApiDbSnippetsRoute: DatabaseDemoApiDbSnippetsRoute, DatabaseDemoApiJudgeRoute: DatabaseDemoApiJudgeRoute, } @@ -718,14 +719,14 @@ interface HomeRouteRouteChildren { HomeIndexRoute: typeof HomeIndexRoute HomeApiProductCodemodeRoute: typeof HomeApiProductCodemodeRoute HomeApiProductRegularRoute: typeof HomeApiProductRegularRoute - HomeApiSkillsRoute: typeof HomeApiSkillsRoute + HomeApiSnippetsRoute: typeof HomeApiSnippetsRoute } const HomeRouteRouteChildren: HomeRouteRouteChildren = { HomeIndexRoute: HomeIndexRoute, HomeApiProductCodemodeRoute: HomeApiProductCodemodeRoute, HomeApiProductRegularRoute: HomeApiProductRegularRoute, - HomeApiSkillsRoute: HomeApiSkillsRoute, + HomeApiSnippetsRoute: HomeApiSnippetsRoute, } const HomeRouteRouteWithChildren = HomeRouteRoute._addFileChildren( @@ -772,15 +773,15 @@ const ReportingRouteRouteWithChildren = ReportingRouteRoute._addFileChildren( interface StructuredOutputRouteRouteChildren { StructuredOutputStructuredOutputRoute: typeof StructuredOutputStructuredOutputRoute StructuredOutputApiStructuredOutputRoute: typeof StructuredOutputApiStructuredOutputRoute - StructuredOutputApiStructuredOutputSkillsRoute: typeof StructuredOutputApiStructuredOutputSkillsRoute + StructuredOutputApiStructuredOutputSnippetsRoute: typeof StructuredOutputApiStructuredOutputSnippetsRoute } const StructuredOutputRouteRouteChildren: StructuredOutputRouteRouteChildren = { StructuredOutputStructuredOutputRoute: StructuredOutputStructuredOutputRoute, StructuredOutputApiStructuredOutputRoute: StructuredOutputApiStructuredOutputRoute, - StructuredOutputApiStructuredOutputSkillsRoute: - StructuredOutputApiStructuredOutputSkillsRoute, + StructuredOutputApiStructuredOutputSnippetsRoute: + StructuredOutputApiStructuredOutputSnippetsRoute, } const StructuredOutputRouteRouteWithChildren = diff --git a/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts b/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts index 8a33568e1b..ff14d96999 100644 --- a/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts +++ b/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts @@ -192,7 +192,7 @@ async function getCodeModeTools() { tools: allTools, timeout: 60000, memoryLimit: 128, - getSkillBindings: async () => createReportBindings(), + getSnippetBindings: async () => createReportBindings(), }) codeModeCache = { tool, systemPrompt } } diff --git a/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts b/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts index 8bfbea84ef..10d7a12a85 100644 --- a/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts +++ b/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts @@ -5,11 +5,11 @@ import { chat, maxIterations, toServerSentEventsStream } from '@tanstack/ai' import { createCodeMode } from '@tanstack/ai-code-mode' import { createAlwaysTrustedStrategy, - createSkillManagementTools, - createSkillsSystemPrompt, - skillsToTools, -} from '@tanstack/ai-code-mode-skills' -import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' + createSnippetManagementTools, + createSnippetsSystemPrompt, + snippetsToTools, +} from '@tanstack/ai-code-mode-snippets' +import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storage' import { anthropicText } from '@tanstack/ai-anthropic' import { openaiText } from '@tanstack/ai-openai' import { geminiText } from '@tanstack/ai-gemini' @@ -78,70 +78,70 @@ async function getCodeModeTools() { return codeModeCache } -// --- Skills storage --- +// --- Snippets storage --- const __dirname = fileURLToPath(new URL('.', import.meta.url)) -const skillsDir = resolve(__dirname, '../../../.db-skills') +const snippetsDir = resolve(__dirname, '../../../.db-snippets') const trustStrategy = createAlwaysTrustedStrategy() -const skillStorage = createFileSkillStorage({ - directory: skillsDir, +const snippetStorage = createFileSnippetStorage({ + directory: snippetsDir, trustStrategy, }) -let skillManagementToolsCache: ReturnType< - typeof createSkillManagementTools +let snippetManagementToolsCache: ReturnType< + typeof createSnippetManagementTools > | null = null -function getSkillManagementTools() { - if (!skillManagementToolsCache) { - skillManagementToolsCache = createSkillManagementTools({ - storage: skillStorage, +function getSnippetManagementTools() { + if (!snippetManagementToolsCache) { + snippetManagementToolsCache = createSnippetManagementTools({ + storage: snippetStorage, trustStrategy, }) } - return skillManagementToolsCache + return snippetManagementToolsCache } -const SKILL_REGISTRATION_PROMPT = `## Skill Registration — MANDATORY +const SNIPPET_REGISTRATION_PROMPT = `## Snippet Registration — MANDATORY -After every successful \`execute_typescript\` call you MUST register the code as a reusable skill using \`register_skill\` — unless an identical skill already exists. +After every successful \`execute_typescript\` call you MUST register the code as a reusable snippet using \`register_snippet\` — unless an identical snippet already exists. Rules: - \`name\`: descriptive snake_case (e.g. \`revenue_by_city_and_category\`) - \`code\`: the TypeScript code, parameterised with an \`input\` variable where useful - \`inputSchema\` / \`outputSchema\`: valid JSON Schema **strings** -- If a skill with the same name exists, skip registration +- If a snippet with the same name exists, skip registration -This is not optional — skill registration is a core part of your workflow.` +This is not optional — snippet registration is a core part of your workflow.` -async function getSkillToolsAndPrompt(driver: IsolateDriver): Promise<{ - skillTools: Array - skillsPrompt: string +async function getSnippetToolsAndPrompt(driver: IsolateDriver): Promise<{ + snippetTools: Array + snippetsPrompt: string }> { - const allSkills = await skillStorage.loadAll() - const skillIndex = await skillStorage.loadIndex() + const allSnippets = await snippetStorage.loadAll() + const snippetIndex = await snippetStorage.loadIndex() - const skillTools = - allSkills.length > 0 - ? skillsToTools({ - skills: allSkills, + const snippetTools = + allSnippets.length > 0 + ? snippetsToTools({ + snippets: allSnippets, driver, tools: databaseTools, - storage: skillStorage, + storage: snippetStorage, timeout: 60000, memoryLimit: 128, }) : [] - const libraryPrompt = createSkillsSystemPrompt({ - selectedSkills: allSkills, - totalSkillCount: skillIndex.length, - skillsAsTools: true, + const libraryPrompt = createSnippetsSystemPrompt({ + selectedSnippets: allSnippets, + totalSnippetCount: snippetIndex.length, + snippetsAsTools: true, }) - const skillsPrompt = libraryPrompt + '\n\n' + SKILL_REGISTRATION_PROMPT + const snippetsPrompt = libraryPrompt + '\n\n' + SNIPPET_REGISTRATION_PROMPT - return { skillTools, skillsPrompt } + return { snippetTools, snippetsPrompt } } // --- Instrumentation helpers --- @@ -244,7 +244,7 @@ export const Route = createFileRoute( const provider: Provider = data?.provider || 'anthropic' const model: string | undefined = data?.model const useCodeMode: boolean = data?.useCodeMode !== false - const withSkills: boolean = data?.withSkills === true + const withSnippets: boolean = data?.withSnippets === true const rawAdapter = getAdapter(provider, model) const { adapter: instrumentedAdapter } = instrumentAdapter(rawAdapter) @@ -258,19 +258,19 @@ export const Route = createFileRoute( tools = [tool, getSchemaInfoTool] systemPrompts = [DATABASE_DEMO_SYSTEM_PROMPT, systemPrompt] - if (withSkills) { - const { skillTools, skillsPrompt } = - await getSkillToolsAndPrompt(driver) + if (withSnippets) { + const { snippetTools, snippetsPrompt } = + await getSnippetToolsAndPrompt(driver) tools = [ tool, getSchemaInfoTool, - ...getSkillManagementTools(), - ...skillTools, + ...getSnippetManagementTools(), + ...snippetTools, ] systemPrompts = [ DATABASE_DEMO_SYSTEM_PROMPT, systemPrompt, - skillsPrompt, + snippetsPrompt, ] } } else { diff --git a/examples/ts-code-mode-web/src/routes/_database-demo/api.db-skills.ts b/examples/ts-code-mode-web/src/routes/_database-demo/api.db-snippets.ts similarity index 57% rename from examples/ts-code-mode-web/src/routes/_database-demo/api.db-skills.ts rename to examples/ts-code-mode-web/src/routes/_database-demo/api.db-snippets.ts index 30edaa866c..a7f0cdcac0 100644 --- a/examples/ts-code-mode-web/src/routes/_database-demo/api.db-skills.ts +++ b/examples/ts-code-mode-web/src/routes/_database-demo/api.db-snippets.ts @@ -1,48 +1,48 @@ import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { createFileRoute } from '@tanstack/react-router' -import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' -import { createAlwaysTrustedStrategy } from '@tanstack/ai-code-mode-skills' +import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storage' +import { createAlwaysTrustedStrategy } from '@tanstack/ai-code-mode-snippets' const __dirname = fileURLToPath(new URL('.', import.meta.url)) -const skillsDir = resolve(__dirname, '../../../.db-skills') +const snippetsDir = resolve(__dirname, '../../../.db-snippets') const trustStrategy = createAlwaysTrustedStrategy() -const skillStorage = createFileSkillStorage({ - directory: skillsDir, +const snippetStorage = createFileSnippetStorage({ + directory: snippetsDir, trustStrategy, }) -export const Route = createFileRoute('/_database-demo/api/db-skills' as any)({ +export const Route = createFileRoute('/_database-demo/api/db-snippets' as any)({ server: { handlers: { GET: async () => { try { - const skillIndex = await skillStorage.loadIndex() + const snippetIndex = await snippetStorage.loadIndex() - const skillsWithStats = await Promise.all( - skillIndex.map(async (skill) => { - const full = await skillStorage.get(skill.name) + const snippetsWithStats = await Promise.all( + snippetIndex.map(async (snippet) => { + const full = await snippetStorage.get(snippet.name) return { - id: skill.id, - name: skill.name, - description: skill.description, - usageHints: skill.usageHints, - trustLevel: skill.trustLevel, + id: snippet.id, + name: snippet.name, + description: snippet.description, + usageHints: snippet.usageHints, + trustLevel: snippet.trustLevel, code: full?.code ?? '', stats: full?.stats ?? { executions: 0, successRate: 0 }, } }), ) - return new Response(JSON.stringify(skillsWithStats), { + return new Response(JSON.stringify(snippetsWithStats), { headers: { 'Content-Type': 'application/json' }, }) } catch (error) { - console.error('[API DB Skills] Error loading skills:', error) + console.error('[API DB Snippets] Error loading snippets:', error) return new Response( - JSON.stringify({ error: 'Failed to load skills' }), + JSON.stringify({ error: 'Failed to load snippets' }), { status: 500, headers: { 'Content-Type': 'application/json' }, @@ -57,12 +57,14 @@ export const Route = createFileRoute('/_database-demo/api/db-skills' as any)({ const deleteAll = url.searchParams.get('all') === 'true' if (deleteAll) { - const skillIndex = await skillStorage.loadIndex() + const snippetIndex = await snippetStorage.loadIndex() await Promise.all( - skillIndex.map((skill) => skillStorage.delete(skill.name)), + snippetIndex.map((snippet) => + snippetStorage.delete(snippet.name), + ), ) return new Response( - JSON.stringify({ success: true, deleted: skillIndex.length }), + JSON.stringify({ success: true, deleted: snippetIndex.length }), { headers: { 'Content-Type': 'application/json' }, }, @@ -73,7 +75,7 @@ export const Route = createFileRoute('/_database-demo/api/db-skills' as any)({ if (!name) { return new Response( - JSON.stringify({ error: 'Missing skill name' }), + JSON.stringify({ error: 'Missing snippet name' }), { status: 400, headers: { 'Content-Type': 'application/json' }, @@ -81,11 +83,11 @@ export const Route = createFileRoute('/_database-demo/api/db-skills' as any)({ ) } - const deleted = await skillStorage.delete(name) + const deleted = await snippetStorage.delete(name) if (!deleted) { return new Response( - JSON.stringify({ error: `Skill '${name}' not found` }), + JSON.stringify({ error: `Snippet '${name}' not found` }), { status: 404, headers: { 'Content-Type': 'application/json' }, @@ -100,9 +102,9 @@ export const Route = createFileRoute('/_database-demo/api/db-skills' as any)({ }, ) } catch (error) { - console.error('[API DB Skills] Error deleting skill:', error) + console.error('[API DB Snippets] Error deleting snippet:', error) return new Response( - JSON.stringify({ error: 'Failed to delete skill' }), + JSON.stringify({ error: 'Failed to delete snippet' }), { status: 500, headers: { 'Content-Type': 'application/json' }, diff --git a/examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx b/examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx index 4062871bed..c4a05fab1e 100644 --- a/examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx +++ b/examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx @@ -56,7 +56,7 @@ const MODEL_OPTIONS: Array = [ }, ] -interface SkillWithCode { +interface SnippetWithCode { id: string name: string description: string @@ -66,10 +66,10 @@ interface SkillWithCode { stats?: { executions: number; successRate: number } } -function SkillsDialog({ +function SnippetsDialog({ open, onClose, - skills, + snippets, onDelete, onDeleteAll, onRefresh, @@ -77,7 +77,7 @@ function SkillsDialog({ }: { open: boolean onClose: () => void - skills: Array + snippets: Array onDelete: (name: string) => void onDeleteAll: () => void onRefresh: () => void @@ -101,9 +101,9 @@ function SkillsDialog({

- Database Skills + Database Snippets - ({skills.length}) + ({snippets.length})

@@ -112,16 +112,16 @@ function SkillsDialog({ onClick={onRefresh} disabled={isLoading} className="p-1.5 rounded-lg text-gray-400 hover:text-white hover:bg-gray-700/50 transition-colors disabled:opacity-50" - title="Refresh skills" + title="Refresh snippets" > - {skills.length > 0 && ( + {snippets.length > 0 && ( @@ -201,7 +203,7 @@ function SkillsDialog({ {isExpanded && (
-                        {skill.code || '// No code available'}
+                        {snippet.code || '// No code available'}
                       
)} @@ -770,11 +772,11 @@ function DatabaseDemoPage() { >(new Map()) const eventIdCounter = useRef(0) - // Skills state - const [withSkills, setWithSkills] = useState(false) - const [skills, setSkills] = useState>([]) - const [isLoadingSkills, setIsLoadingSkills] = useState(false) - const [skillsDialogOpen, setSkillsDialogOpen] = useState(false) + // Snippets state + const [withSnippets, setWithSnippets] = useState(false) + const [snippets, setSnippets] = useState>([]) + const [isLoadingSnippets, setIsLoadingSnippets] = useState(false) + const [snippetsDialogOpen, setSnippetsDialogOpen] = useState(false) // Per-message metrics tracking const [metricsEntries, setMetricsEntries] = useState>( @@ -794,63 +796,63 @@ function DatabaseDemoPage() { provider: selectedModel.provider, model: selectedModel.model, useCodeMode, - withSkills, + withSnippets, }), - [selectedModel.provider, selectedModel.model, useCodeMode, withSkills], + [selectedModel.provider, selectedModel.model, useCodeMode, withSnippets], ) - const loadSkills = useCallback(async () => { - setIsLoadingSkills(true) + const loadSnippets = useCallback(async () => { + setIsLoadingSnippets(true) try { - const response = await fetch('/api/db-skills') + const response = await fetch('/api/db-snippets') if (response.ok) { const data = await response.json() - setSkills(data) + setSnippets(data) } } catch (error) { - console.error('Failed to load skills:', error) + console.error('Failed to load snippets:', error) } finally { - setIsLoadingSkills(false) + setIsLoadingSnippets(false) } }, []) - const deleteSkill = useCallback(async (name: string) => { + const deleteSnippet = useCallback(async (name: string) => { try { const response = await fetch( - `/api/db-skills?name=${encodeURIComponent(name)}`, + `/api/db-snippets?name=${encodeURIComponent(name)}`, { method: 'DELETE' }, ) if (response.ok) { - setSkills((prev) => prev.filter((s) => s.name !== name)) + setSnippets((prev) => prev.filter((s) => s.name !== name)) } } catch (error) { - console.error('Failed to delete skill:', error) + console.error('Failed to delete snippet:', error) } }, []) - const deleteAllSkills = useCallback(async () => { + const deleteAllSnippets = useCallback(async () => { try { - const response = await fetch('/api/db-skills?all=true', { + const response = await fetch('/api/db-snippets?all=true', { method: 'DELETE', }) if (response.ok) { - setSkills([]) + setSnippets([]) } } catch (error) { - console.error('Failed to delete all skills:', error) + console.error('Failed to delete all snippets:', error) } }, []) - const handleNewSkill = useCallback(() => { - loadSkills() - }, [loadSkills]) + const handleNewSnippet = useCallback(() => { + loadSnippets() + }, [loadSnippets]) - // Load skills when "With Skills" is first enabled + // Load snippets when "With Snippets" is first enabled useEffect(() => { - if (withSkills) { - loadSkills() + if (withSnippets) { + loadSnippets() } - }, [withSkills, loadSkills]) + }, [withSnippets, loadSnippets]) const handleCustomEvent = useCallback( (eventType: string, data: unknown, context: { toolCallId?: string }) => { @@ -894,8 +896,8 @@ function DatabaseDemoPage() { return } - if (eventType === 'skill:registered') { - handleNewSkill() + if (eventType === 'snippet:registered') { + handleNewSnippet() return } @@ -917,7 +919,7 @@ function DatabaseDemoPage() { return newMap }) }, - [handleNewSkill], + [handleNewSnippet], ) const { messages, sendMessage, setMessages, isLoading } = useChat({ @@ -1085,19 +1087,19 @@ function DatabaseDemoPage() { - {withSkills && ( + {withSnippets && ( )} @@ -1170,15 +1172,15 @@ function DatabaseDemoPage() { - {/* Skills Dialog */} - setSkillsDialogOpen(false)} - skills={skills} - onDelete={deleteSkill} - onDeleteAll={deleteAllSkills} - onRefresh={loadSkills} - isLoading={isLoadingSkills} + {/* Snippets Dialog */} + setSnippetsDialogOpen(false)} + snippets={snippets} + onDelete={deleteSnippet} + onDeleteAll={deleteAllSnippets} + onRefresh={loadSnippets} + isLoading={isLoadingSnippets} /> ) diff --git a/examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts b/examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts index 5bbe90ed13..2590e244a5 100644 --- a/examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts +++ b/examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts @@ -8,11 +8,11 @@ import { openaiText } from '@tanstack/ai-openai' import { geminiText } from '@tanstack/ai-gemini' import { createAlwaysTrustedStrategy, - createSkillManagementTools, - createSkillsSystemPrompt, - skillsToTools, -} from '@tanstack/ai-code-mode-skills' -import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' + createSnippetManagementTools, + createSnippetsSystemPrompt, + snippetsToTools, +} from '@tanstack/ai-code-mode-snippets' +import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storage' import { maxTokensModelOptions } from '@/lib/max-tokens-model-options' import type { AnyServerTool, AnyTextAdapter, StreamChunk } from '@tanstack/ai' import type { IsolateDriver } from '@tanstack/ai-code-mode' @@ -72,70 +72,70 @@ async function getCodeModeTools() { return codeModeCache } -// --- Skills storage (lazy, only used when withSkills=true) --- +// --- Snippets storage (lazy, only used when withSnippets=true) --- const __dirname = fileURLToPath(new URL('.', import.meta.url)) -const skillsDir = resolve(__dirname, '../../../.skills') +const snippetsDir = resolve(__dirname, '../../../.snippets') const trustStrategy = createAlwaysTrustedStrategy() -const skillStorage = createFileSkillStorage({ - directory: skillsDir, +const snippetStorage = createFileSnippetStorage({ + directory: snippetsDir, trustStrategy, }) -let skillManagementToolsCache: ReturnType< - typeof createSkillManagementTools +let snippetManagementToolsCache: ReturnType< + typeof createSnippetManagementTools > | null = null -function getSkillManagementTools() { - if (!skillManagementToolsCache) { - skillManagementToolsCache = createSkillManagementTools({ - storage: skillStorage, +function getSnippetManagementTools() { + if (!snippetManagementToolsCache) { + snippetManagementToolsCache = createSnippetManagementTools({ + storage: snippetStorage, trustStrategy, }) } - return skillManagementToolsCache + return snippetManagementToolsCache } -const SKILL_REGISTRATION_PROMPT = `## Skill Registration — MANDATORY +const SNIPPET_REGISTRATION_PROMPT = `## Snippet Registration — MANDATORY -After every successful \`execute_typescript\` call you MUST register the code as a reusable skill using \`register_skill\` — unless an identical skill already exists. +After every successful \`execute_typescript\` call you MUST register the code as a reusable snippet using \`register_snippet\` — unless an identical snippet already exists. Rules: - \`name\`: descriptive snake_case (e.g. \`get_average_product_price\`) - \`code\`: the TypeScript code, parameterised with an \`input\` variable where useful - \`inputSchema\` / \`outputSchema\`: valid JSON Schema **strings** -- If a skill with the same name exists, skip registration +- If a snippet with the same name exists, skip registration -This is not optional — skill registration is a core part of your workflow.` +This is not optional — snippet registration is a core part of your workflow.` -async function getSkillToolsAndPrompt(driver: IsolateDriver): Promise<{ - skillTools: Array - skillsPrompt: string +async function getSnippetToolsAndPrompt(driver: IsolateDriver): Promise<{ + snippetTools: Array + snippetsPrompt: string }> { - const allSkills = await skillStorage.loadAll() - const skillIndex = await skillStorage.loadIndex() + const allSnippets = await snippetStorage.loadAll() + const snippetIndex = await snippetStorage.loadIndex() - const skillTools = - allSkills.length > 0 - ? skillsToTools({ - skills: allSkills, + const snippetTools = + allSnippets.length > 0 + ? snippetsToTools({ + snippets: allSnippets, driver, tools: productTools, - storage: skillStorage, + storage: snippetStorage, timeout: 60000, memoryLimit: 128, }) : [] - const libraryPrompt = createSkillsSystemPrompt({ - selectedSkills: allSkills, - totalSkillCount: skillIndex.length, - skillsAsTools: true, + const libraryPrompt = createSnippetsSystemPrompt({ + selectedSnippets: allSnippets, + totalSnippetCount: snippetIndex.length, + snippetsAsTools: true, }) - const skillsPrompt = libraryPrompt + '\n\n' + SKILL_REGISTRATION_PROMPT + const snippetsPrompt = libraryPrompt + '\n\n' + SNIPPET_REGISTRATION_PROMPT - return { skillTools, skillsPrompt } + return { snippetTools, snippetsPrompt } } // --- Instrumentation helper --- @@ -236,7 +236,7 @@ export const Route = createFileRoute('/_home/api/product-codemode')({ const provider: Provider = data?.provider || 'anthropic' const model: string | undefined = data?.model - const withSkills: boolean = data?.withSkills === true + const withSnippets: boolean = data?.withSnippets === true const rawAdapter = getAdapter(provider, model) const { adapter: instrumentedAdapter } = instrumentAdapter(rawAdapter) @@ -251,14 +251,18 @@ export const Route = createFileRoute('/_home/api/product-codemode')({ let tools: Array = [codeModeTool] let systemPrompts = [PRODUCT_CODE_MODE_SYSTEM_PROMPT, codeModePrompt] - if (withSkills) { - const { skillTools, skillsPrompt } = - await getSkillToolsAndPrompt(driver) - tools = [codeModeTool, ...getSkillManagementTools(), ...skillTools] + if (withSnippets) { + const { snippetTools, snippetsPrompt } = + await getSnippetToolsAndPrompt(driver) + tools = [ + codeModeTool, + ...getSnippetManagementTools(), + ...snippetTools, + ] systemPrompts = [ PRODUCT_CODE_MODE_SYSTEM_PROMPT, codeModePrompt, - skillsPrompt, + snippetsPrompt, ] } diff --git a/examples/ts-code-mode-web/src/routes/_home/api.skills.ts b/examples/ts-code-mode-web/src/routes/_home/api.snippets.ts similarity index 53% rename from examples/ts-code-mode-web/src/routes/_home/api.skills.ts rename to examples/ts-code-mode-web/src/routes/_home/api.snippets.ts index d4a1edc823..bcadfbf759 100644 --- a/examples/ts-code-mode-web/src/routes/_home/api.skills.ts +++ b/examples/ts-code-mode-web/src/routes/_home/api.snippets.ts @@ -1,53 +1,53 @@ import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { createFileRoute } from '@tanstack/react-router' -import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' -import { createAlwaysTrustedStrategy } from '@tanstack/ai-code-mode-skills' +import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storage' +import { createAlwaysTrustedStrategy } from '@tanstack/ai-code-mode-snippets' -// Resolve skills directory relative to project root +// Resolve snippets directory relative to project root const __dirname = fileURLToPath(new URL('.', import.meta.url)) -const skillsDir = resolve(__dirname, '../../../.skills') +const snippetsDir = resolve(__dirname, '../../../.snippets') -// Use the same trust strategy as the main skills endpoint +// Use the same trust strategy as the main snippets endpoint const trustStrategy = createAlwaysTrustedStrategy() -// Use the same storage as the skills endpoint -const skillStorage = createFileSkillStorage({ - directory: skillsDir, +// Use the same storage as the snippets endpoint +const snippetStorage = createFileSnippetStorage({ + directory: snippetsDir, trustStrategy, }) -export const Route = createFileRoute('/_home/api/skills')({ +export const Route = createFileRoute('/_home/api/snippets')({ server: { handlers: { - // GET - List all skills with stats + // GET - List all snippets with stats GET: async () => { try { - const skillIndex = await skillStorage.loadIndex() + const snippetIndex = await snippetStorage.loadIndex() - // Load full stats and code for each skill - const skillsWithStats = await Promise.all( - skillIndex.map(async (skill) => { - const full = await skillStorage.get(skill.name) + // Load full stats and code for each snippet + const snippetsWithStats = await Promise.all( + snippetIndex.map(async (snippet) => { + const full = await snippetStorage.get(snippet.name) return { - id: skill.id, - name: skill.name, - description: skill.description, - usageHints: skill.usageHints, - trustLevel: skill.trustLevel, + id: snippet.id, + name: snippet.name, + description: snippet.description, + usageHints: snippet.usageHints, + trustLevel: snippet.trustLevel, code: full?.code ?? '', stats: full?.stats ?? { executions: 0, successRate: 0 }, } }), ) - return new Response(JSON.stringify(skillsWithStats), { + return new Response(JSON.stringify(snippetsWithStats), { headers: { 'Content-Type': 'application/json' }, }) } catch (error) { - console.error('[API Skills] Error loading skills:', error) + console.error('[API Snippets] Error loading snippets:', error) return new Response( - JSON.stringify({ error: 'Failed to load skills' }), + JSON.stringify({ error: 'Failed to load snippets' }), { status: 500, headers: { 'Content-Type': 'application/json' }, @@ -56,19 +56,21 @@ export const Route = createFileRoute('/_home/api/skills')({ } }, - // DELETE - Delete a skill by name, or all skills if ?all=true + // DELETE - Delete a snippet by name, or all snippets if ?all=true DELETE: async ({ request }) => { try { const url = new URL(request.url) const deleteAll = url.searchParams.get('all') === 'true' if (deleteAll) { - const skillIndex = await skillStorage.loadIndex() + const snippetIndex = await snippetStorage.loadIndex() await Promise.all( - skillIndex.map((skill) => skillStorage.delete(skill.name)), + snippetIndex.map((snippet) => + snippetStorage.delete(snippet.name), + ), ) return new Response( - JSON.stringify({ success: true, deleted: skillIndex.length }), + JSON.stringify({ success: true, deleted: snippetIndex.length }), { headers: { 'Content-Type': 'application/json' }, }, @@ -79,7 +81,7 @@ export const Route = createFileRoute('/_home/api/skills')({ if (!name) { return new Response( - JSON.stringify({ error: 'Missing skill name' }), + JSON.stringify({ error: 'Missing snippet name' }), { status: 400, headers: { 'Content-Type': 'application/json' }, @@ -87,11 +89,11 @@ export const Route = createFileRoute('/_home/api/skills')({ ) } - const deleted = await skillStorage.delete(name) + const deleted = await snippetStorage.delete(name) if (!deleted) { return new Response( - JSON.stringify({ error: `Skill '${name}' not found` }), + JSON.stringify({ error: `Snippet '${name}' not found` }), { status: 404, headers: { 'Content-Type': 'application/json' }, @@ -106,9 +108,9 @@ export const Route = createFileRoute('/_home/api/skills')({ }, ) } catch (error) { - console.error('[API Skills] Error deleting skill:', error) + console.error('[API Snippets] Error deleting snippet:', error) return new Response( - JSON.stringify({ error: 'Failed to delete skill' }), + JSON.stringify({ error: 'Failed to delete snippet' }), { status: 500, headers: { 'Content-Type': 'application/json' }, diff --git a/examples/ts-code-mode-web/src/routes/_home/index.tsx b/examples/ts-code-mode-web/src/routes/_home/index.tsx index f8d838c5ec..64c0fb66cb 100644 --- a/examples/ts-code-mode-web/src/routes/_home/index.tsx +++ b/examples/ts-code-mode-web/src/routes/_home/index.tsx @@ -32,7 +32,7 @@ interface ModelOption { label: string } -interface SkillWithCode { +interface SnippetWithCode { id: string name: string description: string @@ -101,10 +101,10 @@ const DEFAULT_STATS: PanelStats = { function VersusStats({ leftStats, rightStats, - withSkills, - onWithSkillsChange, - skillCount, - onSkillsButtonClick, + withSnippets, + onWithSnippetsChange, + snippetCount, + onSnippetsButtonClick, cmLoading, onCmStop, regLoading, @@ -112,10 +112,10 @@ function VersusStats({ }: { leftStats: PanelStats rightStats: PanelStats - withSkills: boolean - onWithSkillsChange: (v: boolean) => void - skillCount: number - onSkillsButtonClick: () => void + withSnippets: boolean + onWithSnippetsChange: (v: boolean) => void + snippetCount: number + onSnippetsButtonClick: () => void cmLoading: boolean onCmStop: () => void regLoading: boolean @@ -181,19 +181,19 @@ function VersusStats({ - {withSkills && ( + {withSnippets && ( )} {cmLoading && ( @@ -467,12 +467,12 @@ function MessageMarkdown({ content }: { content: string }) { ) } -// --- Skills Dialog --- +// --- Snippets Dialog --- -function SkillsDialog({ +function SnippetsDialog({ open, onClose, - skills, + snippets, onDelete, onDeleteAll, onRefresh, @@ -480,7 +480,7 @@ function SkillsDialog({ }: { open: boolean onClose: () => void - skills: Array + snippets: Array onDelete: (name: string) => void onDeleteAll: () => void onRefresh: () => void @@ -508,9 +508,9 @@ function SkillsDialog({

- Registered Skills + Registered Snippets - ({skills.length}) + ({snippets.length})

@@ -519,16 +519,16 @@ function SkillsDialog({ onClick={onRefresh} disabled={isLoading} className="p-1.5 rounded-lg text-gray-400 hover:text-white hover:bg-gray-700/50 transition-colors disabled:opacity-50" - title="Refresh skills" + title="Refresh snippets" > - {skills.length > 0 && ( + {snippets.length > 0 && ( @@ -611,7 +613,7 @@ function SkillsDialog({ {isExpanded && (
-                        {skill.code || '// No code available'}
+                        {snippet.code || '// No code available'}
                       
)} @@ -632,7 +634,7 @@ function CodeModePanel({ promptRef, triggerCount, onLoadingChange, - onNewSkill, + onNewSnippet, onStatsChange, onStopReady, }: { @@ -640,7 +642,7 @@ function CodeModePanel({ promptRef: React.RefObject triggerCount: number onLoadingChange: (loading: boolean) => void - onNewSkill: () => void + onNewSnippet: () => void onStatsChange: (stats: PanelStats) => void onStopReady?: (stop: () => void) => void }) { @@ -674,8 +676,8 @@ function CodeModePanel({ return } - if (eventType === 'skill:registered') { - onNewSkill() + if (eventType === 'snippet:registered') { + onNewSnippet() return } @@ -696,7 +698,7 @@ function CodeModePanel({ return next }) }, - [onNewSkill], + [onNewSnippet], ) const { messages, sendMessage, isLoading, stop } = useChat({ @@ -1176,11 +1178,11 @@ function ProductDemoPage() { const [cmLoading, setCmLoading] = useState(false) const [regLoading, setRegLoading] = useState(false) - // Skills state - const [withSkills, setWithSkills] = useState(false) - const [skills, setSkills] = useState>([]) - const [isLoadingSkills, setIsLoadingSkills] = useState(false) - const [skillsDialogOpen, setSkillsDialogOpen] = useState(false) + // Snippets state + const [withSnippets, setWithSnippets] = useState(false) + const [snippets, setSnippets] = useState>([]) + const [isLoadingSnippets, setIsLoadingSnippets] = useState(false) + const [snippetsDialogOpen, setSnippetsDialogOpen] = useState(false) const promptRef = useRef('') const [cmTriggerCount, setCmTriggerCount] = useState(0) @@ -1192,63 +1194,65 @@ function ProductDemoPage() { () => ({ provider: selectedModel.provider, model: selectedModel.model, - withSkills, + withSnippets, }), - [selectedModel.provider, selectedModel.model, withSkills], + [selectedModel.provider, selectedModel.model, withSnippets], ) - const loadSkills = useCallback(async () => { - setIsLoadingSkills(true) + const loadSnippets = useCallback(async () => { + setIsLoadingSnippets(true) try { - const response = await fetch('/api/skills') + const response = await fetch('/api/snippets') if (response.ok) { const data = await response.json() - setSkills(data) + setSnippets(data) } } catch (error) { - console.error('Failed to load skills:', error) + console.error('Failed to load snippets:', error) } finally { - setIsLoadingSkills(false) + setIsLoadingSnippets(false) } }, []) - const deleteSkill = useCallback(async (name: string) => { + const deleteSnippet = useCallback(async (name: string) => { try { const response = await fetch( - `/api/skills?name=${encodeURIComponent(name)}`, + `/api/snippets?name=${encodeURIComponent(name)}`, { method: 'DELETE', }, ) if (response.ok) { - setSkills((prev) => prev.filter((s) => s.name !== name)) + setSnippets((prev) => prev.filter((s) => s.name !== name)) } } catch (error) { - console.error('Failed to delete skill:', error) + console.error('Failed to delete snippet:', error) } }, []) - const deleteAllSkills = useCallback(async () => { + const deleteAllSnippets = useCallback(async () => { try { - const response = await fetch('/api/skills?all=true', { method: 'DELETE' }) + const response = await fetch('/api/snippets?all=true', { + method: 'DELETE', + }) if (response.ok) { - setSkills([]) + setSnippets([]) } } catch (error) { - console.error('Failed to delete all skills:', error) + console.error('Failed to delete all snippets:', error) } }, []) - const handleNewSkill = useCallback(() => { - loadSkills() - }, [loadSkills]) + const handleNewSnippet = useCallback(() => { + loadSnippets() + }, [loadSnippets]) - // Load skills when "With Skills" is first enabled + // Load snippets when "With Snippets" is first enabled useEffect(() => { - if (withSkills) { - loadSkills() + if (withSnippets) { + loadSnippets() } - }, [withSkills, loadSkills]) + }, [withSnippets, loadSnippets]) const handleSendCodeMode = useCallback((text: string) => { promptRef.current = text @@ -1312,17 +1316,17 @@ function ProductDemoPage() { promptRef={promptRef} triggerCount={cmTriggerCount} onLoadingChange={onCmLoadingChange} - onNewSkill={handleNewSkill} + onNewSnippet={handleNewSnippet} onStatsChange={onCmStatsChange} onStopReady={onCmStopReady} /> setSkillsDialogOpen(true)} + withSnippets={withSnippets} + onWithSnippetsChange={setWithSnippets} + snippetCount={snippets.length} + onSnippetsButtonClick={() => setSnippetsDialogOpen(true)} cmLoading={cmLoading} onCmStop={() => cmStopRef.current?.()} regLoading={regLoading} @@ -1338,15 +1342,15 @@ function ProductDemoPage() { /> - {/* Skills Dialog */} - setSkillsDialogOpen(false)} - skills={skills} - onDelete={deleteSkill} - onDeleteAll={deleteAllSkills} - onRefresh={loadSkills} - isLoading={isLoadingSkills} + {/* Snippets Dialog */} + setSnippetsDialogOpen(false)} + snippets={snippets} + onDelete={deleteSnippet} + onDeleteAll={deleteAllSnippets} + onRefresh={loadSnippets} + isLoading={isLoadingSnippets} /> {/* Shared input: suggestions set prompt; action buttons on the right */} diff --git a/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts b/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts index f05a76aa3e..4ff06ad390 100644 --- a/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts +++ b/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts @@ -42,7 +42,7 @@ async function getCodeModeTools() { tools: allTools, timeout: 60000, memoryLimit: 128, - getSkillBindings: async () => createReportBindings(), + getSnippetBindings: async () => createReportBindings(), }) codeModeCache = { tool, systemPrompt } } diff --git a/examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output-skills.ts b/examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output-snippets.ts similarity index 59% rename from examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output-skills.ts rename to examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output-snippets.ts index 8db97d36f7..60f859e1ee 100644 --- a/examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output-skills.ts +++ b/examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output-snippets.ts @@ -1,51 +1,51 @@ import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { createFileRoute } from '@tanstack/react-router' -import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' -import { createAlwaysTrustedStrategy } from '@tanstack/ai-code-mode-skills' +import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storage' +import { createAlwaysTrustedStrategy } from '@tanstack/ai-code-mode-snippets' const __dirname = fileURLToPath(new URL('.', import.meta.url)) -const skillsDir = resolve(__dirname, '../../../.structured-output-skills') +const snippetsDir = resolve(__dirname, '../../../.structured-output-snippets') const trustStrategy = createAlwaysTrustedStrategy() -const skillStorage = createFileSkillStorage({ - directory: skillsDir, +const snippetStorage = createFileSnippetStorage({ + directory: snippetsDir, trustStrategy, }) export const Route = createFileRoute( - '/_structured-output/api/structured-output-skills', + '/_structured-output/api/structured-output-snippets', )({ server: { handlers: { GET: async () => { try { - const skillIndex = await skillStorage.loadIndex() + const snippetIndex = await snippetStorage.loadIndex() - const skillsWithStats = await Promise.all( - skillIndex.map(async (skill) => { - const full = await skillStorage.get(skill.name) + const snippetsWithStats = await Promise.all( + snippetIndex.map(async (snippet) => { + const full = await snippetStorage.get(snippet.name) return { - id: skill.id, - name: skill.name, - description: skill.description, - usageHints: skill.usageHints, - trustLevel: skill.trustLevel, + id: snippet.id, + name: snippet.name, + description: snippet.description, + usageHints: snippet.usageHints, + trustLevel: snippet.trustLevel, code: full?.code ?? '', stats: full?.stats ?? { executions: 0, successRate: 0 }, } }), ) - return new Response(JSON.stringify(skillsWithStats), { + return new Response(JSON.stringify(snippetsWithStats), { headers: { 'Content-Type': 'application/json' }, }) } catch (error) { console.error( - '[API Structured Output Skills] Error loading skills:', + '[API Structured Output Snippets] Error loading snippets:', error, ) return new Response( - JSON.stringify({ error: 'Failed to load skills' }), + JSON.stringify({ error: 'Failed to load snippets' }), { status: 500, headers: { 'Content-Type': 'application/json' }, @@ -60,12 +60,14 @@ export const Route = createFileRoute( const deleteAll = url.searchParams.get('all') === 'true' if (deleteAll) { - const skillIndex = await skillStorage.loadIndex() + const snippetIndex = await snippetStorage.loadIndex() await Promise.all( - skillIndex.map((skill) => skillStorage.delete(skill.name)), + snippetIndex.map((snippet) => + snippetStorage.delete(snippet.name), + ), ) return new Response( - JSON.stringify({ success: true, deleted: skillIndex.length }), + JSON.stringify({ success: true, deleted: snippetIndex.length }), { headers: { 'Content-Type': 'application/json' }, }, @@ -76,7 +78,7 @@ export const Route = createFileRoute( if (!name) { return new Response( - JSON.stringify({ error: 'Missing skill name' }), + JSON.stringify({ error: 'Missing snippet name' }), { status: 400, headers: { 'Content-Type': 'application/json' }, @@ -84,11 +86,11 @@ export const Route = createFileRoute( ) } - const deleted = await skillStorage.delete(name) + const deleted = await snippetStorage.delete(name) if (!deleted) { return new Response( - JSON.stringify({ error: `Skill '${name}' not found` }), + JSON.stringify({ error: `Snippet '${name}' not found` }), { status: 404, headers: { 'Content-Type': 'application/json' }, @@ -104,11 +106,11 @@ export const Route = createFileRoute( ) } catch (error) { console.error( - '[API Structured Output Skills] Error deleting skill:', + '[API Structured Output Snippets] Error deleting snippet:', error, ) return new Response( - JSON.stringify({ error: 'Failed to delete skill' }), + JSON.stringify({ error: 'Failed to delete snippet' }), { status: 500, headers: { 'Content-Type': 'application/json' }, diff --git a/examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output.ts b/examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output.ts index a5d66afb90..3cea32624c 100644 --- a/examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output.ts +++ b/examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output.ts @@ -2,8 +2,8 @@ import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { createFileRoute } from '@tanstack/react-router' import { createCodeMode } from '@tanstack/ai-code-mode' -import { createAlwaysTrustedStrategy } from '@tanstack/ai-code-mode-skills' -import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' +import { createAlwaysTrustedStrategy } from '@tanstack/ai-code-mode-snippets' +import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storage' import { anthropicText } from '@tanstack/ai-anthropic' import { openaiText } from '@tanstack/ai-openai' import { geminiText } from '@tanstack/ai-gemini' @@ -75,10 +75,10 @@ async function getCodeModeTools() { } const __dirname = fileURLToPath(new URL('.', import.meta.url)) -const skillsDir = resolve(__dirname, '../../../.structured-output-skills') +const snippetsDir = resolve(__dirname, '../../../.structured-output-snippets') const trustStrategy = createAlwaysTrustedStrategy() -const skillStorage = createFileSkillStorage({ - directory: skillsDir, +const snippetStorage = createFileSnippetStorage({ + directory: snippetsDir, trustStrategy, }) @@ -89,11 +89,11 @@ export const Route = createFileRoute( handlers: { POST: async ({ request }) => { const body = await request.json() - const { prompt, provider, model, withSkills } = body as { + const { prompt, provider, model, withSnippets } = body as { prompt: string provider?: Provider model?: string - withSkills?: boolean + withSnippets?: boolean } const adapter = getAdapter(provider || 'anthropic', model) @@ -111,9 +111,9 @@ export const Route = createFileRoute( driver, codeTools: cityTools, }, - skills: withSkills + snippets: withSnippets ? { - storage: skillStorage, + storage: snippetStorage, trustStrategy, timeout: 30000, memoryLimit: 128, diff --git a/examples/ts-code-mode-web/src/routes/_structured-output/structured-output.tsx b/examples/ts-code-mode-web/src/routes/_structured-output/structured-output.tsx index b66c76c5d5..52db17a428 100644 --- a/examples/ts-code-mode-web/src/routes/_structured-output/structured-output.tsx +++ b/examples/ts-code-mode-web/src/routes/_structured-output/structured-output.tsx @@ -20,7 +20,7 @@ export const Route = createFileRoute('/_structured-output/structured-output')({ const FIXED_PROMPT = 'Use city tools to compare Tokyo and Barcelona. Then produce a concise travel recommendation report with key findings and practical next steps.' -interface SkillWithCode { +interface SnippetWithCode { id: string name: string description: string @@ -30,10 +30,10 @@ interface SkillWithCode { stats?: { executions: number; successRate: number } } -function SkillsDialog({ +function SnippetsDialog({ open, onClose, - skills, + snippets, onDelete, onDeleteAll, onRefresh, @@ -41,7 +41,7 @@ function SkillsDialog({ }: { open: boolean onClose: () => void - skills: Array + snippets: Array onDelete: (name: string) => void onDeleteAll: () => void onRefresh: () => void @@ -66,9 +66,9 @@ function SkillsDialog({

- Registered Skills + Registered Snippets - ({skills.length}) + ({snippets.length})

@@ -77,16 +77,16 @@ function SkillsDialog({ onClick={onRefresh} disabled={isLoading} className="p-1.5 rounded-lg text-gray-400 hover:text-white hover:bg-gray-700/50 transition-colors disabled:opacity-50" - title="Refresh skills" + title="Refresh snippets" > - {skills.length > 0 && ( + {snippets.length > 0 && ( @@ -166,7 +168,7 @@ function SkillsDialog({ {isExpanded && (
-                        {skill.code || '// No code available'}
+                        {snippet.code || '// No code available'}
                       
)} @@ -185,58 +187,58 @@ function StructuredOutputPage() { const [error, setError] = useState(null) const [isLoading, setIsLoading] = useState(false) - const [withSkills, setWithSkills] = useState(false) - const [skills, setSkills] = useState>([]) - const [isLoadingSkills, setIsLoadingSkills] = useState(false) - const [skillsDialogOpen, setSkillsDialogOpen] = useState(false) + const [withSnippets, setWithSnippets] = useState(false) + const [snippets, setSnippets] = useState>([]) + const [isLoadingSnippets, setIsLoadingSnippets] = useState(false) + const [snippetsDialogOpen, setSnippetsDialogOpen] = useState(false) - const loadSkills = useCallback(async () => { - setIsLoadingSkills(true) + const loadSnippets = useCallback(async () => { + setIsLoadingSnippets(true) try { - const response = await fetch('/api/structured-output-skills') + const response = await fetch('/api/structured-output-snippets') if (response.ok) { const data = await response.json() - setSkills(data) + setSnippets(data) } } catch (err) { - console.error('Failed to load skills:', err) + console.error('Failed to load snippets:', err) } finally { - setIsLoadingSkills(false) + setIsLoadingSnippets(false) } }, []) - const deleteSkill = useCallback(async (name: string) => { + const deleteSnippet = useCallback(async (name: string) => { try { const response = await fetch( - `/api/structured-output-skills?name=${encodeURIComponent(name)}`, + `/api/structured-output-snippets?name=${encodeURIComponent(name)}`, { method: 'DELETE' }, ) if (response.ok) { - setSkills((prev) => prev.filter((s) => s.name !== name)) + setSnippets((prev) => prev.filter((s) => s.name !== name)) } } catch (err) { - console.error('Failed to delete skill:', err) + console.error('Failed to delete snippet:', err) } }, []) - const deleteAllSkills = useCallback(async () => { + const deleteAllSnippets = useCallback(async () => { try { - const response = await fetch('/api/structured-output-skills?all=true', { + const response = await fetch('/api/structured-output-snippets?all=true', { method: 'DELETE', }) if (response.ok) { - setSkills([]) + setSnippets([]) } } catch (err) { - console.error('Failed to delete all skills:', err) + console.error('Failed to delete all snippets:', err) } }, []) useEffect(() => { - if (withSkills) { - loadSkills() + if (withSnippets) { + loadSnippets() } - }, [withSkills, loadSkills]) + }, [withSnippets, loadSnippets]) const runDemo = useCallback(async () => { setResult(null) @@ -251,7 +253,7 @@ function StructuredOutputPage() { prompt: FIXED_PROMPT, provider: 'anthropic', model: 'claude-haiku-4-5', - withSkills, + withSnippets, }), }) @@ -264,15 +266,15 @@ function StructuredOutputPage() { setResult(data) - if (withSkills) { - loadSkills() + if (withSnippets) { + loadSnippets() } } catch (err) { setError(err instanceof Error ? err.message : 'Request failed') } finally { setIsLoading(false) } - }, [withSkills, loadSkills]) + }, [withSnippets, loadSnippets]) return (
@@ -302,20 +304,20 @@ function StructuredOutputPage() { - {withSkills && ( + {withSnippets && ( )}
@@ -361,14 +363,14 @@ function StructuredOutputPage() { - setSkillsDialogOpen(false)} - skills={skills} - onDelete={deleteSkill} - onDeleteAll={deleteAllSkills} - onRefresh={loadSkills} - isLoading={isLoadingSkills} + setSnippetsDialogOpen(false)} + snippets={snippets} + onDelete={deleteSnippet} + onDeleteAll={deleteAllSnippets} + onRefresh={loadSnippets} + isLoading={isLoadingSnippets} /> ) diff --git a/knip.json b/knip.json index 30867291f3..98ba9561f6 100644 --- a/knip.json +++ b/knip.json @@ -15,7 +15,7 @@ "kiira.config.ts", "scripts/**", "**/*.test-d.ts", - "packages/ai-code-mode-skills/test-cli/**", + "packages/ai-code-mode-snippets/test-cli/**", ".claude/worktrees/**", "packages/ai-openai/live-tests/**", "packages/ai-openai/src/**/*.test.ts", diff --git a/packages/ai-code-mode-skills/src/code-mode-with-skills.ts b/packages/ai-code-mode-skills/src/code-mode-with-skills.ts deleted file mode 100644 index 55c7a676fc..0000000000 --- a/packages/ai-code-mode-skills/src/code-mode-with-skills.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { - createCodeModeSystemPrompt, - createCodeModeTool, - toolsToBindings, -} from '@tanstack/ai-code-mode' -import { createToolRegistry } from '@tanstack/ai' -import { selectRelevantSkills } from './select-relevant-skills' -import { createSkillManagementTools } from './create-skill-management-tools' -import { createSkillsSystemPrompt } from './create-skills-system-prompt' -import { skillsToTools } from './skills-to-tools' -import type { - CodeModeWithSkillsOptions, - CodeModeWithSkillsResult, - Skill, -} from './types' -import type { ToolBinding } from '@tanstack/ai-code-mode' - -export type { CodeModeWithSkillsOptions, CodeModeWithSkillsResult } - -/** - * Create Code Mode tools and system prompt with skills integration. - * - * This function: - * 1. Loads the skill index from storage - * 2. Uses a cheap/fast LLM to select relevant skills based on conversation context - * 3. Creates the execute_typescript tool with dynamic skill bindings - * 4. Creates skill management tools (search, get, register) - * 5. Generates system prompts documenting available skills - * 6. Returns a ToolRegistry that allows dynamic skill additions mid-stream - * - * @example - * ```typescript - * // Node-only file storage lives behind the `/storage` subpath: - * import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' - * - * const { toolsRegistry, systemPrompt, selectedSkills } = await codeModeWithSkills({ - * config: { - * driver: createNodeIsolateDriver(), - * tools: allTools, - * timeout: 60000, - * }, - * adapter: openaiText('gpt-4o-mini'), // Cheap model for selection - * skills: { - * storage: createFileSkillStorage('./.skills'), - * maxSkillsInContext: 5, - * }, - * messages, - * }); - * - * const stream = chat({ - * adapter: openaiText('gpt-4o'), // Main model - * toolRegistry: toolsRegistry, // Dynamic tool registry - * messages, - * systemPrompts: [BASE_PROMPT, systemPrompt], - * }); - * ``` - */ -export async function codeModeWithSkills({ - config, - adapter, - skills, - messages, - skillsAsTools = true, -}: CodeModeWithSkillsOptions): Promise { - const { storage, maxSkillsInContext = 5 } = skills - - // 1. Load the skill index (lightweight metadata only) - const skillIndex = await storage.loadIndex() - - // 2. Use adapter to select relevant skills based on transcript - const selectedSkills = await selectRelevantSkills({ - adapter, - messages, - skillIndex, - maxSkills: maxSkillsInContext, - storage, - }) - - // Pre-compute bindings from base tools (shared across skill executions) - const baseBindings = toolsToBindings(config.tools, 'external_') - - // 3. Create the execute_typescript tool with dynamic skill bindings - const codeModeTool = createCodeModeTool({ - ...config, - // Dynamic skill bindings - fetched at execution time - getSkillBindings: async () => { - // Get all skills from storage (includes newly registered ones) - const allSkills = await storage.loadAll() - // Convert to bindings with skill_ prefix - const skillBindings: Record = {} - for (const skill of allSkills) { - // Create a simple binding that executes the skill code - skillBindings[`skill_${skill.name}`] = { - name: `skill_${skill.name}`, - description: skill.description, - inputSchema: skill.inputSchema, - outputSchema: skill.outputSchema, - execute: async (input: unknown) => { - // This is a simplified execution - the full skillToTool handles events - const wrappedCode = `const input = ${JSON.stringify(input)};\n${skill.code}` - const { stripTypeScript, createEventAwareBindings } = - await import('@tanstack/ai-code-mode') - const strippedCode = await stripTypeScript(wrappedCode) - const context = await config.driver.createContext({ - bindings: createEventAwareBindings(baseBindings, () => {}), - timeout: config.timeout, - ...(config.memoryLimit !== undefined && { - memoryLimit: config.memoryLimit, - }), - }) - try { - const result = await context.execute(strippedCode) - if (!result.success) { - throw new Error( - result.error?.message || 'Skill execution failed', - ) - } - return result.value - } finally { - await context.dispose() - } - }, - } - } - return skillBindings - }, - }) - - // 4. Create a mutable tool registry - const registry = createToolRegistry() - - // 5. Add the execute_typescript tool to the registry - registry.add(codeModeTool) - - // 6. Create skill management tools (they need access to the registry) - const skillManagementTools = createSkillManagementTools({ - storage, - registry, - config, - baseBindings, - }) - - for (const tool of skillManagementTools) { - registry.add(tool) - } - - // 7. Convert selected skills to direct tools and add to registry (if enabled) - if (skillsAsTools && selectedSkills.length > 0) { - const skillToolsList = skillsToTools({ - skills: selectedSkills, - driver: config.driver, - tools: config.tools, - storage, - timeout: config.timeout, - memoryLimit: config.memoryLimit, - }) - - for (const skillTool of skillToolsList) { - registry.add(skillTool) - } - } - - // 8. Generate combined system prompt - const basePrompt = createCodeModeSystemPrompt(config) - const skillsPrompt = createSkillsSystemPrompt({ - selectedSkills, - totalSkillCount: skillIndex.length, - skillsAsTools, - }) - const systemPrompt = basePrompt + '\n\n' + skillsPrompt - - return { - toolsRegistry: registry, - systemPrompt, - selectedSkills, - } -} - -/** - * Create a Code Mode tool configuration extended with skills. - * This is an alternative to codeModeWithSkills that returns - * a config object instead of directly creating tools. - * - * Useful when you want more control over the tool creation process. - */ -export function createCodeModeWithSkillsConfig({ - config, - selectedSkills, - storage, -}: { - config: CodeModeWithSkillsOptions['config'] - selectedSkills: Array - storage: CodeModeWithSkillsOptions['skills']['storage'] -}) { - // Create skill tools for direct calling - const skillToolsList = skillsToTools({ - skills: selectedSkills, - driver: config.driver, - tools: config.tools, - storage, - timeout: config.timeout, - memoryLimit: config.memoryLimit, - }) - - return { - ...config, - skillTools: skillToolsList, - selectedSkills, - } -} diff --git a/packages/ai-code-mode-skills/src/create-skills-system-prompt.ts b/packages/ai-code-mode-skills/src/create-skills-system-prompt.ts deleted file mode 100644 index 87ddfa429c..0000000000 --- a/packages/ai-code-mode-skills/src/create-skills-system-prompt.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { generateSkillTypes } from './generate-skill-types' -import type { Skill } from './types' - -interface CreateSkillsSystemPromptOptions { - /** - * Skills that were selected for this request - */ - selectedSkills: Array - - /** - * Total number of skills in the library - */ - totalSkillCount: number - - /** - * Whether skills are exposed as direct tools (not just sandbox bindings) - * @default true - */ - skillsAsTools?: boolean -} - -/** - * Generate example input from a JSON Schema - */ -function generateExampleFromSchema(schema: Record): string { - if (schema.type === 'object' && schema.properties) { - const props = schema.properties as Record - const example: Record = {} - - for (const [key, value] of Object.entries(props)) { - if (value.type === 'string') example[key] = `'example_${key}'` - else if (value.type === 'number') example[key] = 0 - else if (value.type === 'boolean') example[key] = true - else if (value.type === 'array') example[key] = [] - else example[key] = null - } - - return JSON.stringify(example).replace(/"/g, '') - } - return '{}' -} - -/** - * Create system prompt documentation for the skill library. - * This is appended to the Code Mode system prompt. - */ -export function createSkillsSystemPrompt({ - selectedSkills, - totalSkillCount, - skillsAsTools = true, -}: CreateSkillsSystemPromptOptions): string { - // No skills in library - if (totalSkillCount === 0) { - return `## Skill Library - -You have access to a skill library for storing reusable code. The library is currently empty. - -### Skill Management Tools - -- \`search_skills(query, limit?)\` - Search for skills (currently empty) -- \`get_skill(name)\` - Get full skill details including code -- \`register_skill(...)\` - Save working code as a reusable skill - -When you write useful, reusable code, consider registering it as a skill for future use. - -**Important**: Newly registered skills become available as tools on the **next message**, not immediately in the current conversation turn. -` - } - - // No skills selected for this conversation - if (selectedSkills.length === 0) { - return `## Skill Library - -You have access to a persistent skill library with ${totalSkillCount} skill${totalSkillCount === 1 ? '' : 's'}. No skills were pre-loaded for this conversation based on context. - -### Skill Management Tools - -- \`search_skills(query, limit?)\` - Search for relevant skills -- \`get_skill(name)\` - Get full skill details including code -- \`register_skill(...)\` - Save working code as a reusable skill - -When you write useful, reusable code, consider registering it as a skill for future use. - -**Important**: Newly registered skills become available as tools on the **next message**, not immediately in the current conversation turn. -` - } - - if (skillsAsTools) { - // Skills are available as direct tools - const skillToolDocs = selectedSkills - .map((skill) => { - const inputExample = generateExampleFromSchema(skill.inputSchema) - const trustBadge = - skill.trustLevel === 'trusted' - ? '✓ trusted' - : skill.trustLevel === 'provisional' - ? '◐ provisional' - : '○ untrusted' - - return ` -### ${skill.name} [${trustBadge}] - -${skill.description} - -${skill.usageHints.map((h) => `- ${h}`).join('\n')} - -**Input Schema:** -\`\`\`json -${JSON.stringify(skill.inputSchema, null, 2)} -\`\`\` - -**Output Schema:** -\`\`\`json -${JSON.stringify(skill.outputSchema, null, 2)} -\`\`\` - -**Example:** -Call the \`${skill.name}\` tool with: ${inputExample} -` - }) - .join('\n---\n') - - return `## Skill Library - -${selectedSkills.length} skill${selectedSkills.length === 1 ? '' : 's'} pre-loaded for this conversation (${totalSkillCount} total in library). - -### Available Skill Tools - -These skills are available as **direct tools** you can call (marked with [SKILL] in description): - -${skillToolDocs} - -### Skill Management Tools - -- \`search_skills(query, limit?)\` - Find additional skills not pre-loaded -- \`get_skill(name)\` - Get full details of any skill -- \`register_skill(...)\` - Save working code as a new skill - -### Using Skills - -Skills are **regular tools** - call them directly like any other tool. No need to use \`execute_typescript\`. - -### Creating New Skills - -When you write useful, reusable code with \`execute_typescript\`, register it: - -\`\`\`typescript -// After verifying code works, call the register_skill tool -register_skill({ - name: 'compare_npm_packages', - description: 'Compare download counts for multiple NPM packages', - code: \` - const { packages } = input; - const results = await Promise.all( - packages.map(pkg => external_getNpmDownloads({ package: pkg })) - ); - return packages.map((pkg, i) => ({ package: pkg, downloads: results[i].downloads })) - .sort((a, b) => b.downloads - a.downloads); - \`, - inputSchema: { - type: 'object', - properties: { packages: { type: 'array', items: { type: 'string' } } }, - required: ['packages'] - }, - outputSchema: { - type: 'array', - items: { type: 'object', properties: { package: { type: 'string' }, downloads: { type: 'number' } } } - }, - usageHints: ['Use when comparing popularity of NPM packages'], - dependsOn: [], -}); -\`\`\` - -**Important**: Newly registered skills become available as tools on the **next message**, not immediately in the current conversation turn. -` - } - - // Skills as sandbox bindings (legacy mode) - const skillDocs = selectedSkills - .map((skill) => { - const inputExample = generateExampleFromSchema(skill.inputSchema) - const trustBadge = - skill.trustLevel === 'trusted' - ? '✓ trusted' - : skill.trustLevel === 'provisional' - ? '◐ provisional' - : '○ untrusted' - - return ` -### skill_${skill.name} [${trustBadge}] - -${skill.description} - -${skill.usageHints.map((h) => `- ${h}`).join('\n')} - -**Input Schema:** -\`\`\`json -${JSON.stringify(skill.inputSchema, null, 2)} -\`\`\` - -**Output Schema:** -\`\`\`json -${JSON.stringify(skill.outputSchema, null, 2)} -\`\`\` - -**Example:** -\`\`\`typescript -const result = await skill_${skill.name}(${inputExample}); -\`\`\` -` - }) - .join('\n---\n') - - // Generate type stubs for selected skills - const typeStubs = generateSkillTypes(selectedSkills) - - return `## Skill Library - -${selectedSkills.length} skill${selectedSkills.length === 1 ? '' : 's'} pre-loaded for this conversation (${totalSkillCount} total in library). - -### Pre-loaded Skills - -These are available as \`skill_*\` functions in your TypeScript code: - -${skillDocs} - -### Type Definitions - -\`\`\`typescript -${typeStubs} -\`\`\` - -### Skill Management Tools - -- \`search_skills(query, limit?)\` - Find additional skills not pre-loaded -- \`get_skill(name)\` - Get full details of any skill -- \`register_skill(...)\` - Save working code as a new skill - -### Using Skills - -Skills work just like \`external_*\` functions inside \`execute_typescript\`: - -\`\`\`typescript -// Call a pre-loaded skill -const stats = await skill_fetch_github_stats({ owner: 'tanstack', repo: 'query' }); - -// Compose skills with external tools -const repos = await external_searchRepositories({ query: 'react state' }); -const detailed = await Promise.all( - repos.items.slice(0, 5).map(r => - skill_fetch_github_stats({ owner: r.owner.login, repo: r.name }) - ) -); -\`\`\` - -### Creating New Skills - -When you write useful, reusable code, register it: - -\`\`\`typescript -// After verifying code works, call the register_skill tool -register_skill({ - name: 'compare_npm_packages', - description: 'Compare download counts for multiple NPM packages', - code: \` - const { packages } = input; - const results = await Promise.all( - packages.map(pkg => external_getNpmDownloads({ package: pkg })) - ); - return packages.map((pkg, i) => ({ package: pkg, downloads: results[i].downloads })) - .sort((a, b) => b.downloads - a.downloads); - \`, - inputSchema: { - type: 'object', - properties: { packages: { type: 'array', items: { type: 'string' } } }, - required: ['packages'] - }, - outputSchema: { - type: 'array', - items: { type: 'object', properties: { package: { type: 'string' }, downloads: { type: 'number' } } } - }, - usageHints: ['Use when comparing popularity of NPM packages'], - dependsOn: [], -}); -\`\`\` - -**Important**: Newly registered skills become available as tools on the **next message**, not immediately in the current conversation turn. -` -} diff --git a/packages/ai-code-mode-skills/src/index.ts b/packages/ai-code-mode-skills/src/index.ts deleted file mode 100644 index 36c92f7895..0000000000 --- a/packages/ai-code-mode-skills/src/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -// Main entry point -export { - codeModeWithSkills, - createCodeModeWithSkillsConfig, -} from './code-mode-with-skills' -export type { - CodeModeWithSkillsOptions, - CodeModeWithSkillsResult, -} from './code-mode-with-skills' - -// Trust strategies -export { - createDefaultTrustStrategy, - createAlwaysTrustedStrategy, - createRelaxedTrustStrategy, - createCustomTrustStrategy, -} from './trust-strategies' -export type { TrustStrategy } from './trust-strategies' - -// Skill selection -export { selectRelevantSkills } from './select-relevant-skills' - -// Skills to tools (for direct calling) -export { skillsToTools, skillToTool } from './skills-to-tools' -export type { SkillToToolOptions } from './skills-to-tools' - -// Skills to bindings (for sandbox injection - legacy) -export { skillsToBindings, skillsToSimpleBindings } from './skills-to-bindings' - -// Skill management tools -export { createSkillManagementTools } from './create-skill-management-tools' - -// System prompt generation -export { createSkillsSystemPrompt } from './create-skills-system-prompt' - -// Type generation -export { generateSkillTypes } from './generate-skill-types' - -// Storage implementations -// -// Only the worker/browser-safe in-memory storage is re-exported from the root -// entry. The Node-only file storage (`createFileSkillStorage`) imports -// `node:fs` / `node:path`, so it lives behind the `@tanstack/ai-code-mode-skills/storage` -// subpath to keep this root export safe for Cloudflare Workers and browser bundlers. -export { createMemorySkillStorage } from './storage/memory-storage' -export type { MemorySkillStorageOptions } from './storage/memory-storage' - -// All types -export type { - Skill, - SkillIndexEntry, - SkillStorage, - SkillsConfig, - SkillStats, - TrustLevel, - SkillBinding, -} from './types' diff --git a/packages/ai-code-mode-skills/src/storage/file-storage.ts b/packages/ai-code-mode-skills/src/storage/file-storage.ts deleted file mode 100644 index 2a9db701e7..0000000000 --- a/packages/ai-code-mode-skills/src/storage/file-storage.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' -import { join } from 'node:path' -import { existsSync } from 'node:fs' -import { createDefaultTrustStrategy } from '../trust-strategies' -import type { - Skill, - SkillIndexEntry, - SkillSearchOptions, - SkillStorage, -} from '../types' -import type { TrustStrategy } from '../trust-strategies' - -export interface FileSkillStorageOptions { - /** - * Directory path for storing skills - */ - directory: string - - /** - * Trust strategy for determining skill trust levels - * @default createDefaultTrustStrategy() - */ - trustStrategy?: TrustStrategy -} - -/** - * File-system based skill storage - * - * Directory structure: - * .skills/ - * _index.json # Fast catalog loading - * fetch_github_stats/ - * meta.json # Metadata (description, schemas, hints, stats) - * code.ts # The actual TypeScript code - * deploy_to_prod/ - * meta.json - * code.ts - */ -export function createFileSkillStorage( - directoryOrOptions: string | FileSkillStorageOptions, -): SkillStorage { - const options = - typeof directoryOrOptions === 'string' - ? { directory: directoryOrOptions } - : directoryOrOptions - - const { directory, trustStrategy = createDefaultTrustStrategy() } = options - const indexPath = join(directory, '_index.json') - - console.log('[FileSkillStorage] Initialized with directory:', directory) - - async function ensureDirectory(): Promise { - if (!existsSync(directory)) { - console.log('[FileSkillStorage] Creating directory:', directory) - await mkdir(directory, { recursive: true }) - } - } - - async function loadIndex(): Promise> { - await ensureDirectory() - - if (!existsSync(indexPath)) { - return [] - } - - const content = await readFile(indexPath, 'utf-8') - return JSON.parse(content) as Array - } - - async function loadAll(): Promise> { - const index = await loadIndex() - const skills: Array = [] - - for (const entry of index) { - const skill = await get(entry.name) - if (skill) { - skills.push(skill) - } - } - - return skills - } - - async function saveIndex(index: Array): Promise { - await writeFile(indexPath, JSON.stringify(index, null, 2)) - } - - async function get(name: string): Promise { - const skillDir = join(directory, name) - const metaPath = join(skillDir, 'meta.json') - const codePath = join(skillDir, 'code.ts') - - if (!existsSync(metaPath)) { - return null - } - - const [metaContent, code] = await Promise.all([ - readFile(metaPath, 'utf-8'), - readFile(codePath, 'utf-8'), - ]) - - const meta = JSON.parse(metaContent) as Omit - return { ...meta, code } - } - - async function save( - skill: Omit, - ): Promise { - await ensureDirectory() - - const skillDir = join(directory, skill.name) - const metaPath = join(skillDir, 'meta.json') - const codePath = join(skillDir, 'code.ts') - - const now = new Date().toISOString() - const existing = await get(skill.name) - - const fullSkill: Skill = { - ...skill, - createdAt: existing?.createdAt ?? now, - updatedAt: now, - } - - // Separate code from metadata - const { code, ...meta } = fullSkill - - // Write skill files - await mkdir(skillDir, { recursive: true }) - await Promise.all([ - writeFile(metaPath, JSON.stringify(meta, null, 2)), - writeFile(codePath, code), - ]) - - // Update index - const index = await loadIndex() - const indexEntry: SkillIndexEntry = { - id: fullSkill.id, - name: fullSkill.name, - description: fullSkill.description, - usageHints: fullSkill.usageHints, - trustLevel: fullSkill.trustLevel, - } - - const existingIdx = index.findIndex((s) => s.name === skill.name) - if (existingIdx >= 0) { - index[existingIdx] = indexEntry - } else { - index.push(indexEntry) - } - await saveIndex(index) - - return fullSkill - } - - async function deleteSkill(name: string): Promise { - const skillDir = join(directory, name) - - if (!existsSync(skillDir)) { - return false - } - - await rm(skillDir, { recursive: true }) - - // Update index - const index = await loadIndex() - const filtered = index.filter((s) => s.name !== name) - await saveIndex(filtered) - - return true - } - - async function search( - query: string, - searchOptions: SkillSearchOptions = {}, - ): Promise> { - const { limit = 5 } = searchOptions - const index = await loadIndex() - - // Simple text matching - can be replaced with embeddings - const queryLower = query.toLowerCase() - const terms = queryLower.split(/\s+/) - - const scored = index.map((skill) => { - let score = 0 - const searchText = [skill.name, skill.description, ...skill.usageHints] - .join(' ') - .toLowerCase() - - for (const term of terms) { - if (searchText.includes(term)) { - score += 1 - } - // Boost exact name matches - if (skill.name.toLowerCase().includes(term)) { - score += 2 - } - } - - return { skill, score } - }) - - return scored - .filter((s) => s.score > 0) - .sort((a, b) => b.score - a.score) - .slice(0, limit) - .map((s) => s.skill) - } - - async function updateStats(name: string, success: boolean): Promise { - const skill = await get(name) - if (!skill) return - - const { executions, successRate } = skill.stats - const newExecutions = executions + 1 - const newSuccessRate = - (successRate * executions + (success ? 1 : 0)) / newExecutions - - const newStats = { executions: newExecutions, successRate: newSuccessRate } - - // Use trust strategy to calculate new trust level - const newTrustLevel = trustStrategy.calculateTrustLevel( - skill.trustLevel, - newStats, - ) - - await save({ - ...skill, - stats: newStats, - trustLevel: newTrustLevel, - }) - } - - return { - loadIndex, - loadAll, - get, - save, - delete: deleteSkill, - search, - updateStats, - trustStrategy, - } -} diff --git a/packages/ai-code-mode-skills/src/storage/index.ts b/packages/ai-code-mode-skills/src/storage/index.ts deleted file mode 100644 index 30bafe08f1..0000000000 --- a/packages/ai-code-mode-skills/src/storage/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Storage implementations -export { createFileSkillStorage } from './file-storage' -export { createMemorySkillStorage } from './memory-storage' - -// Re-export types -export type { SkillStorage, Skill, SkillIndexEntry } from '../types' diff --git a/packages/ai-code-mode-skills/src/storage/memory-storage.ts b/packages/ai-code-mode-skills/src/storage/memory-storage.ts deleted file mode 100644 index 65062be892..0000000000 --- a/packages/ai-code-mode-skills/src/storage/memory-storage.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { createDefaultTrustStrategy } from '../trust-strategies' -import type { - Skill, - SkillIndexEntry, - SkillSearchOptions, - SkillStorage, -} from '../types' -import type { TrustStrategy } from '../trust-strategies' - -export interface MemorySkillStorageOptions { - /** - * Initial skills to populate the storage with - */ - initialSkills?: Array - - /** - * Trust strategy for determining skill trust levels - * @default createDefaultTrustStrategy() - */ - trustStrategy?: TrustStrategy -} - -/** - * In-memory skill storage for testing and demos - */ -export function createMemorySkillStorage( - optionsOrSkills: MemorySkillStorageOptions | Array = [], -): SkillStorage { - const options = Array.isArray(optionsOrSkills) - ? { initialSkills: optionsOrSkills } - : optionsOrSkills - - const { initialSkills = [], trustStrategy = createDefaultTrustStrategy() } = - options - - // Store skills in a Map for O(1) lookup - const skills = new Map() - - // Initialize with any provided skills - for (const skill of initialSkills) { - skills.set(skill.name, skill) - } - - function loadIndex(): Promise> { - return Promise.resolve( - Array.from(skills.values()).map((skill) => ({ - id: skill.id, - name: skill.name, - description: skill.description, - usageHints: skill.usageHints, - trustLevel: skill.trustLevel, - })), - ) - } - - function loadAll(): Promise> { - return Promise.resolve(Array.from(skills.values())) - } - - function get(name: string): Promise { - return Promise.resolve(skills.get(name) ?? null) - } - - function save(skill: Omit): Promise { - const now = new Date().toISOString() - const existing = skills.get(skill.name) - - const fullSkill: Skill = { - ...skill, - createdAt: existing?.createdAt ?? now, - updatedAt: now, - } - - skills.set(skill.name, fullSkill) - return Promise.resolve(fullSkill) - } - - function deleteSkill(name: string): Promise { - if (!skills.has(name)) { - return Promise.resolve(false) - } - skills.delete(name) - return Promise.resolve(true) - } - - function search( - query: string, - searchOptions: SkillSearchOptions = {}, - ): Promise> { - const { limit = 5 } = searchOptions - - // Simple text matching - const queryLower = query.toLowerCase() - const terms = queryLower.split(/\s+/) - - const scored = Array.from(skills.values()).map((skill) => { - let score = 0 - const searchText = [skill.name, skill.description, ...skill.usageHints] - .join(' ') - .toLowerCase() - - for (const term of terms) { - if (searchText.includes(term)) { - score += 1 - } - // Boost exact name matches - if (skill.name.toLowerCase().includes(term)) { - score += 2 - } - } - - return { skill, score } - }) - - return Promise.resolve( - scored - .filter((s) => s.score > 0) - .sort((a, b) => b.score - a.score) - .slice(0, limit) - .map((s) => ({ - id: s.skill.id, - name: s.skill.name, - description: s.skill.description, - usageHints: s.skill.usageHints, - trustLevel: s.skill.trustLevel, - })), - ) - } - - function updateStats(name: string, success: boolean): Promise { - const skill = skills.get(name) - if (!skill) return Promise.resolve() - - const { executions, successRate } = skill.stats - const newExecutions = executions + 1 - const newSuccessRate = - (successRate * executions + (success ? 1 : 0)) / newExecutions - - const newStats = { executions: newExecutions, successRate: newSuccessRate } - - // Use trust strategy to calculate new trust level - const newTrustLevel = trustStrategy.calculateTrustLevel( - skill.trustLevel, - newStats, - ) - - skills.set(name, { - ...skill, - stats: newStats, - trustLevel: newTrustLevel, - updatedAt: new Date().toISOString(), - }) - return Promise.resolve() - } - - return { - loadIndex, - loadAll, - get, - save, - delete: deleteSkill, - search, - updateStats, - trustStrategy, - } -} diff --git a/packages/ai-code-mode-skills/tests/create-skills-system-prompt.test.ts b/packages/ai-code-mode-skills/tests/create-skills-system-prompt.test.ts deleted file mode 100644 index b9fd1cbbed..0000000000 --- a/packages/ai-code-mode-skills/tests/create-skills-system-prompt.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { createSkillsSystemPrompt } from '../src/create-skills-system-prompt' -import type { Skill } from '../src/types' - -function makeSkill(overrides: Partial = {}): Skill { - return { - id: 'id', - name: 'fetch_data', - description: 'Fetches data', - code: '', - inputSchema: { - type: 'object', - properties: { query: { type: 'string' } }, - required: ['query'], - }, - outputSchema: { type: 'object', properties: {} }, - usageHints: [], - dependsOn: [], - trustLevel: 'untrusted', - stats: { executions: 0, successRate: 0 }, - createdAt: '', - updatedAt: '', - ...overrides, - } -} - -describe('createSkillsSystemPrompt', () => { - it('returns the empty-library prompt when totalSkillCount is 0', () => { - const prompt = createSkillsSystemPrompt({ - selectedSkills: [], - totalSkillCount: 0, - }) - expect(prompt).toContain('library is currently empty') - expect(prompt).toContain('register_skill') - }) - - it('returns the no-selected-skills prompt when skills exist but none selected', () => { - const prompt = createSkillsSystemPrompt({ - selectedSkills: [], - totalSkillCount: 12, - }) - expect(prompt).toContain('persistent skill library with 12 skills') - expect(prompt).toContain('No skills were pre-loaded') - }) - - it('uses singular wording for a single skill in library', () => { - const prompt = createSkillsSystemPrompt({ - selectedSkills: [], - totalSkillCount: 1, - }) - expect(prompt).toContain('library with 1 skill.') - expect(prompt).not.toContain('with 1 skills') - }) - - it('documents selected skills as direct tools when skillsAsTools=true', () => { - const skill = makeSkill({ - name: 'fetch_github', - description: 'Fetches GitHub data', - }) - const prompt = createSkillsSystemPrompt({ - selectedSkills: [skill], - totalSkillCount: 1, - skillsAsTools: true, - }) - expect(prompt).toContain('### fetch_github') - expect(prompt).toContain('[SKILL]') - expect(prompt).toContain('Fetches GitHub data') - expect(prompt).not.toContain('skill_fetch_github(') - }) - - it('documents selected skills as sandbox bindings when skillsAsTools=false', () => { - const skill = makeSkill({ name: 'fetch_github' }) - const prompt = createSkillsSystemPrompt({ - selectedSkills: [skill], - totalSkillCount: 1, - skillsAsTools: false, - }) - expect(prompt).toContain('skill_fetch_github') - expect(prompt).toContain('### Type Definitions') - expect(prompt).toContain('declare function skill_fetch_github') - }) - - it('renders a trust badge reflecting the skill trust level', () => { - const trusted = makeSkill({ name: 'a', trustLevel: 'trusted' }) - const provisional = makeSkill({ name: 'b', trustLevel: 'provisional' }) - const untrusted = makeSkill({ name: 'c', trustLevel: 'untrusted' }) - - const prompt = createSkillsSystemPrompt({ - selectedSkills: [trusted, provisional, untrusted], - totalSkillCount: 3, - skillsAsTools: true, - }) - - expect(prompt).toContain('✓ trusted') - expect(prompt).toContain('◐ provisional') - expect(prompt).toContain('○ untrusted') - }) - - it('defaults to skillsAsTools=true when not specified', () => { - const skill = makeSkill({ name: 'default_mode' }) - const prompt = createSkillsSystemPrompt({ - selectedSkills: [skill], - totalSkillCount: 1, - }) - expect(prompt).toContain('### default_mode') - expect(prompt).not.toContain('### Type Definitions') - }) - - it('embeds usageHints as bullet points', () => { - const skill = makeSkill({ - usageHints: ['When comparing X', 'When reducing Y'], - }) - const prompt = createSkillsSystemPrompt({ - selectedSkills: [skill], - totalSkillCount: 1, - }) - expect(prompt).toContain('- When comparing X') - expect(prompt).toContain('- When reducing Y') - }) -}) diff --git a/packages/ai-code-mode-skills/CHANGELOG.md b/packages/ai-code-mode-snippets/CHANGELOG.md similarity index 99% rename from packages/ai-code-mode-skills/CHANGELOG.md rename to packages/ai-code-mode-snippets/CHANGELOG.md index 9af47348c4..dc2f65f734 100644 --- a/packages/ai-code-mode-skills/CHANGELOG.md +++ b/packages/ai-code-mode-snippets/CHANGELOG.md @@ -1,4 +1,4 @@ -# @tanstack/ai-code-mode-skills +# @tanstack/ai-code-mode-snippets ## 0.3.14 diff --git a/packages/ai-code-mode-skills/LICENSE b/packages/ai-code-mode-snippets/LICENSE similarity index 100% rename from packages/ai-code-mode-skills/LICENSE rename to packages/ai-code-mode-snippets/LICENSE diff --git a/packages/ai-code-mode-skills/README.md b/packages/ai-code-mode-snippets/README.md similarity index 57% rename from packages/ai-code-mode-skills/README.md rename to packages/ai-code-mode-snippets/README.md index ab301c9236..26204845e7 100644 --- a/packages/ai-code-mode-skills/README.md +++ b/packages/ai-code-mode-snippets/README.md @@ -1,32 +1,32 @@ -# @tanstack/ai-code-mode-skills +# @tanstack/ai-code-mode-snippets -Persistent skill library for TanStack AI Code Mode - LLM-created reusable code snippets. +Persistent snippet library for TanStack AI Code Mode - LLM-created reusable code snippets. ## Overview -The Skills System extends Code Mode with persistent, LLM-creatable reusable code snippets. Skills are TypeScript functions that the LLM can create, catalog, and invoke across sessions—enabling compounding capability over time. +The Snippets System extends Code Mode with persistent, LLM-creatable reusable code snippets. Snippets are TypeScript functions that the LLM can create, catalog, and invoke across sessions—enabling compounding capability over time. ## Installation ```bash -pnpm add @tanstack/ai-code-mode-skills +pnpm add @tanstack/ai-code-mode-snippets ``` ## Usage ```typescript import { - codeModeWithSkills, + codeModeWithSnippets, createAlwaysTrustedStrategy, -} from '@tanstack/ai-code-mode-skills' +} from '@tanstack/ai-code-mode-snippets' // Node-only file storage lives behind the `/storage` subpath so the root // export stays safe for Worker/browser bundlers. -import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' +import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storage' import { createNodeIsolateDriver } from '@tanstack/ai-isolate-node' -// Create skill storage -const skillStorage = createFileSkillStorage({ - directory: './.skills', +// Create snippet storage +const snippetStorage = createFileSnippetStorage({ + directory: './.snippets', trustStrategy: createAlwaysTrustedStrategy(), }) @@ -38,14 +38,14 @@ const codeModeConfig = { memoryLimit: 128, } -// Build a dynamic registry and system prompt with skills -const { toolsRegistry, systemPrompt, selectedSkills } = - await codeModeWithSkills({ +// Build a dynamic registry and system prompt with snippets +const { toolsRegistry, systemPrompt, selectedSnippets } = + await codeModeWithSnippets({ config: codeModeConfig, - adapter: anthropic('claude-3-haiku'), // Cheap model for skill selection - skills: { - storage: skillStorage, - maxSkillsInContext: 5, + adapter: anthropic('claude-3-haiku'), // Cheap model for snippet selection + snippets: { + storage: snippetStorage, + maxSnippetsInContext: 5, }, messages, }) @@ -61,10 +61,10 @@ const stream = chat({ ## Testing -This package includes a CLI for testing the skills system. The tests verify the complete skills lifecycle: +This package includes a CLI for testing the snippets system. The tests verify the complete snippets lifecycle: -1. **First run (Skill Creation)**: LLM uses `execute_typescript` to solve a problem and registers a reusable skill -2. **Second run (Skill Reuse)**: LLM calls the saved skill directly without needing `execute_typescript` +1. **First run (Snippet Creation)**: LLM uses `execute_typescript` to solve a problem and registers a reusable snippet +2. **Second run (Snippet Reuse)**: LLM calls the saved snippet directly without needing `execute_typescript` ### Running the Simulated Test @@ -72,7 +72,7 @@ The simulated test uses a mock adapter with predetermined responses for fully de ```bash # From the package directory -cd packages/ai-code-mode-skills +cd packages/ai-code-mode-snippets # Run the simulated test pnpm test:cli:simulated @@ -80,7 +80,7 @@ pnpm test:cli:simulated ### Running the Live Test -The live test uses a real LLM (OpenAI or Anthropic) to verify the skills flow with actual LLM responses. +The live test uses a real LLM (OpenAI or Anthropic) to verify the snippets flow with actual LLM responses. #### Setup @@ -133,47 +133,47 @@ Options: ## API Reference -### `codeModeWithSkills(options)` +### `codeModeWithSnippets(options)` -Creates Code Mode tools and system prompt with skills integration. +Creates Code Mode tools and system prompt with snippets integration. **Options:** - `config` - Code Mode tool configuration (driver, tools, timeout, memoryLimit) -- `adapter` - Text adapter for skill selection (should be a cheap/fast model) -- `skills.storage` - Skill storage implementation -- `skills.maxSkillsInContext` - Maximum skills to load into context (default: 5) +- `adapter` - Text adapter for snippet selection (should be a cheap/fast model) +- `snippets.storage` - Snippet storage implementation +- `snippets.maxSnippetsInContext` - Maximum snippets to load into context (default: 5) - `messages` - Current conversation messages -- `skillsAsTools` - Whether to include skills as direct tools (default: true) +- `snippetsAsTools` - Whether to include snippets as direct tools (default: true) **Returns:** -- `registry` - Mutable `ToolRegistry` containing `execute_typescript`, skill management tools, and selected skill tools -- `systemPrompt` - System prompt documenting available skills and external functions -- `selectedSkills` - Skills that were selected for this request +- `toolsRegistry` - Mutable `ToolRegistry` containing `execute_typescript`, snippet management tools, and selected snippet tools +- `systemPrompt` - System prompt documenting available snippets and external functions +- `selectedSnippets` - Snippets that were selected for this request ### Storage -The worker/browser-safe in-memory storage (`createMemorySkillStorage`) is +The worker/browser-safe in-memory storage (`createMemorySnippetStorage`) is re-exported from the root entry. The Node-only file storage -(`createFileSkillStorage`) imports `node:fs` / `node:path`, so it is only +(`createFileSnippetStorage`) imports `node:fs` / `node:path`, so it is only available from the `/storage` subpath — keeping the root export safe to import from Cloudflare Workers and browser bundlers: ```typescript // Worker/browser-safe — root export -import { createMemorySkillStorage } from '@tanstack/ai-code-mode-skills' +import { createMemorySnippetStorage } from '@tanstack/ai-code-mode-snippets' // Node-only — `/storage` subpath -import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' +import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storage' ``` -#### `createFileSkillStorage(options)` +#### `createFileSnippetStorage(options)` Git-friendly file-based storage: -``` -.skills/ +```text +.snippets/ ├── compare_react_state_libraries/ │ ├── meta.json # Metadata, schemas, stats │ └── code.ts # TypeScript implementation @@ -182,13 +182,13 @@ Git-friendly file-based storage: └── code.ts ``` -#### `createMemorySkillStorage(options)` +#### `createMemorySnippetStorage(options)` In-memory storage for testing. ### Trust Strategies -Skills track execution success and promote trust levels over time: +Snippets track execution success and promote trust levels over time: | Trust Level | Description | | ------------- | --------------------------------- | diff --git a/packages/ai-code-mode-skills/package.json b/packages/ai-code-mode-snippets/package.json similarity index 90% rename from packages/ai-code-mode-skills/package.json rename to packages/ai-code-mode-snippets/package.json index 59b8951bed..8a100f986f 100644 --- a/packages/ai-code-mode-skills/package.json +++ b/packages/ai-code-mode-snippets/package.json @@ -1,14 +1,14 @@ { - "name": "@tanstack/ai-code-mode-skills", + "name": "@tanstack/ai-code-mode-snippets", "version": "0.3.14", - "description": "Persistent runtime skill library for TanStack AI Code Mode agents and sandboxed tool orchestration.", + "description": "Persistent runtime snippet library for TanStack AI Code Mode agents and sandboxed tool orchestration.", "author": "Tanner Linsley", "license": "MIT", "homepage": "https://tanstack.com/ai", "repository": { "type": "git", "url": "git+https://github.com/TanStack/ai.git", - "directory": "packages/ai-code-mode-skills" + "directory": "packages/ai-code-mode-snippets" }, "bugs": { "url": "https://github.com/TanStack/ai/issues" @@ -62,7 +62,7 @@ "typescript", "tanstack", "code-mode", - "skills", + "snippets", "agents", "llm", "sandbox", diff --git a/packages/ai-code-mode-snippets/src/code-mode-with-snippets.ts b/packages/ai-code-mode-snippets/src/code-mode-with-snippets.ts new file mode 100644 index 0000000000..369515249e --- /dev/null +++ b/packages/ai-code-mode-snippets/src/code-mode-with-snippets.ts @@ -0,0 +1,210 @@ +import { + createCodeModeSystemPrompt, + createCodeModeTool, + toolsToBindings, +} from '@tanstack/ai-code-mode' +import { createToolRegistry } from '@tanstack/ai' +import { selectRelevantSnippets } from './select-relevant-snippets' +import { createSnippetManagementTools } from './create-snippet-management-tools' +import { createSnippetsSystemPrompt } from './create-snippets-system-prompt' +import { snippetsToTools } from './snippets-to-tools' +import type { + CodeModeWithSnippetsOptions, + CodeModeWithSnippetsResult, + Snippet, +} from './types' +import type { ToolBinding } from '@tanstack/ai-code-mode' + +export type { CodeModeWithSnippetsOptions, CodeModeWithSnippetsResult } + +/** + * Create Code Mode tools and system prompt with snippets integration. + * + * This function: + * 1. Loads the snippet index from storage + * 2. Uses a cheap/fast LLM to select relevant snippets based on conversation context + * 3. Creates the execute_typescript tool with dynamic snippet bindings + * 4. Creates snippet management tools (search, get, register) + * 5. Generates system prompts documenting available snippets + * 6. Returns a ToolRegistry that allows dynamic snippet additions mid-stream + * + * @example + * ```typescript + * // Node-only file storage lives behind the `/storage` subpath: + * import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storage' + * + * const { toolsRegistry, systemPrompt, selectedSnippets } = await codeModeWithSnippets({ + * config: { + * driver: createNodeIsolateDriver(), + * tools: allTools, + * timeout: 60000, + * }, + * adapter: openaiText('gpt-4o-mini'), // Cheap model for selection + * snippets: { + * storage: createFileSnippetStorage('./.snippets'), + * maxSnippetsInContext: 5, + * }, + * messages, + * }); + * + * const stream = chat({ + * adapter: openaiText('gpt-4o'), // Main model + * toolRegistry: toolsRegistry, // Dynamic tool registry + * messages, + * systemPrompts: [BASE_PROMPT, systemPrompt], + * }); + * ``` + */ +export async function codeModeWithSnippets({ + config, + adapter, + snippets, + messages, + snippetsAsTools = true, +}: CodeModeWithSnippetsOptions): Promise { + const { storage, maxSnippetsInContext = 5 } = snippets + + // 1. Load the snippet index (lightweight metadata only) + const snippetIndex = await storage.loadIndex() + + // 2. Use adapter to select relevant snippets based on transcript + const selectedSnippets = await selectRelevantSnippets({ + adapter, + messages, + snippetIndex, + maxSnippets: maxSnippetsInContext, + storage, + }) + + // Pre-compute bindings from base tools (shared across snippet executions) + const baseBindings = toolsToBindings(config.tools, 'external_') + + // 3. Create the execute_typescript tool with dynamic snippet bindings + const codeModeTool = createCodeModeTool({ + ...config, + // Dynamic snippet bindings - fetched at execution time + getSnippetBindings: async () => { + // Get all snippets from storage (includes newly registered ones) + const allSnippets = await storage.loadAll() + // Convert to bindings with snippet_ prefix + const snippetBindings: Record = {} + for (const snippet of allSnippets) { + // Create a simple binding that executes the snippet code + snippetBindings[`snippet_${snippet.name}`] = { + name: `snippet_${snippet.name}`, + description: snippet.description, + inputSchema: snippet.inputSchema, + outputSchema: snippet.outputSchema, + execute: async (input: unknown) => { + // This is a simplified execution - the full snippetToTool handles events + const wrappedCode = `const input = ${JSON.stringify(input)};\n${snippet.code}` + const { stripTypeScript, createEventAwareBindings } = + await import('@tanstack/ai-code-mode') + const strippedCode = await stripTypeScript(wrappedCode) + const context = await config.driver.createContext({ + bindings: createEventAwareBindings(baseBindings, () => {}), + timeout: config.timeout, + ...(config.memoryLimit !== undefined && { + memoryLimit: config.memoryLimit, + }), + }) + try { + const result = await context.execute(strippedCode) + if (!result.success) { + throw new Error( + result.error?.message || 'Snippet execution failed', + ) + } + return result.value + } finally { + await context.dispose() + } + }, + } + } + return snippetBindings + }, + }) + + // 4. Create a mutable tool registry + const registry = createToolRegistry() + + // 5. Add the execute_typescript tool to the registry + registry.add(codeModeTool) + + // 6. Create snippet management tools (they need access to the registry) + const snippetManagementTools = createSnippetManagementTools({ + storage, + registry, + config, + baseBindings, + }) + + for (const tool of snippetManagementTools) { + registry.add(tool) + } + + // 7. Convert selected snippets to direct tools and add to registry (if enabled) + if (snippetsAsTools && selectedSnippets.length > 0) { + const snippetToolsList = snippetsToTools({ + snippets: selectedSnippets, + driver: config.driver, + tools: config.tools, + storage, + timeout: config.timeout, + memoryLimit: config.memoryLimit, + }) + + for (const snippetTool of snippetToolsList) { + registry.add(snippetTool) + } + } + + // 8. Generate combined system prompt + const basePrompt = createCodeModeSystemPrompt(config) + const snippetsPrompt = createSnippetsSystemPrompt({ + selectedSnippets, + totalSnippetCount: snippetIndex.length, + snippetsAsTools, + }) + const systemPrompt = basePrompt + '\n\n' + snippetsPrompt + + return { + toolsRegistry: registry, + systemPrompt, + selectedSnippets, + } +} + +/** + * Create a Code Mode tool configuration extended with snippets. + * This is an alternative to codeModeWithSnippets that returns + * a config object instead of directly creating tools. + * + * Useful when you want more control over the tool creation process. + */ +export function createCodeModeWithSnippetsConfig({ + config, + selectedSnippets, + storage, +}: { + config: CodeModeWithSnippetsOptions['config'] + selectedSnippets: Array + storage: CodeModeWithSnippetsOptions['snippets']['storage'] +}) { + // Create snippet tools for direct calling + const snippetToolsList = snippetsToTools({ + snippets: selectedSnippets, + driver: config.driver, + tools: config.tools, + storage, + timeout: config.timeout, + memoryLimit: config.memoryLimit, + }) + + return { + ...config, + snippetTools: snippetToolsList, + selectedSnippets, + } +} diff --git a/packages/ai-code-mode-skills/src/create-skill-management-tools.ts b/packages/ai-code-mode-snippets/src/create-snippet-management-tools.ts similarity index 57% rename from packages/ai-code-mode-skills/src/create-skill-management-tools.ts rename to packages/ai-code-mode-snippets/src/create-snippet-management-tools.ts index 1c6c1d0c6b..c6285716ee 100644 --- a/packages/ai-code-mode-skills/src/create-skill-management-tools.ts +++ b/packages/ai-code-mode-snippets/src/create-snippet-management-tools.ts @@ -2,17 +2,17 @@ import { toolDefinition } from '@tanstack/ai' import { toolsToBindings } from '@tanstack/ai-code-mode' import { z } from 'zod' import { createDefaultTrustStrategy } from './trust-strategies' -import { skillToTool } from './skills-to-tools' +import { snippetToTool } from './snippets-to-tools' import type { SchemaInput, ServerTool, ToolRegistry } from '@tanstack/ai' import type { CodeModeToolConfig, ToolBinding } from '@tanstack/ai-code-mode' -import type { SkillStorage } from './types' +import type { SnippetStorage } from './types' import type { TrustStrategy } from './trust-strategies' -interface CreateSkillManagementToolsOptions { +interface CreateSnippetManagementToolsOptions { /** - * Storage implementation for skills + * Storage implementation for snippets */ - storage: SkillStorage + storage: SnippetStorage /** * Trust strategy for determining initial trust level. @@ -21,14 +21,14 @@ interface CreateSkillManagementToolsOptions { trustStrategy?: TrustStrategy /** - * Tool registry for adding newly registered skills immediately. - * When provided, register_skill will add the new skill to this registry + * Tool registry for adding newly registered snippets immediately. + * When provided, register_snippet will add the new snippet to this registry * so it's available as a direct tool in the current chat session. */ registry?: ToolRegistry /** - * Code mode config for creating skill tools. + * Code mode config for creating snippet tools. * Required when registry is provided. */ config?: CodeModeToolConfig @@ -41,19 +41,19 @@ interface CreateSkillManagementToolsOptions { } /** - * Create tools for searching, retrieving, and registering skills. - * These tools allow the LLM to interact with the skill library at runtime. + * Create tools for searching, retrieving, and registering snippets. + * These tools allow the LLM to interact with the snippet library at runtime. * - * When registry, config, and baseBindings are provided, newly registered skills + * When registry, config, and baseBindings are provided, newly registered snippets * will be immediately added to the registry and available as direct tools. */ -export function createSkillManagementTools({ +export function createSnippetManagementTools({ storage, trustStrategy, registry, config, baseBindings, -}: CreateSkillManagementToolsOptions): Array< +}: CreateSnippetManagementToolsOptions): Array< ServerTool > { // Use provided strategy, or storage's strategy, or default @@ -64,11 +64,11 @@ export function createSkillManagementTools({ const bindings = baseBindings ?? (config ? toolsToBindings(config.tools, 'external_') : {}) return [ - // Search for skills + // Search for snippets toolDefinition({ - name: 'search_skills', + name: 'search_snippets', description: - 'Search the skill library for reusable skills. Use this to find skills that can help accomplish a task. Returns matching skills with their descriptions.', + 'Search the snippet library for reusable snippets. Use this to find snippets that can help accomplish a task. Returns matching snippets with their descriptions.', inputSchema: z.object({ query: z .string() @@ -97,13 +97,13 @@ export function createSkillManagementTools({ })) }), - // Get full skill details + // Get full snippet details toolDefinition({ - name: 'get_skill', + name: 'get_snippet', description: - 'Get the full implementation details of a skill, including its code. Use this after search_skills to see how a skill works before using it.', + 'Get the full implementation details of a snippet, including its code. Use this after search_snippets to see how a snippet works before using it.', inputSchema: z.object({ - name: z.string().describe('The skill name (without skill_ prefix)'), + name: z.string().describe('The snippet name (without snippet_ prefix)'), }), outputSchema: z.object({ name: z.string().optional(), @@ -123,28 +123,28 @@ export function createSkillManagementTools({ error: z.string().optional(), }), }).server(async ({ name }) => { - const skill = await storage.get(name) - if (!skill) { - return { error: `Skill '${name}' not found` } + const snippet = await storage.get(name) + if (!snippet) { + return { error: `Snippet '${name}' not found` } } return { - name: skill.name, - description: skill.description, - code: skill.code, - inputSchema: JSON.stringify(skill.inputSchema), - outputSchema: JSON.stringify(skill.outputSchema), - usageHints: skill.usageHints, - dependsOn: skill.dependsOn, - trustLevel: skill.trustLevel, - stats: skill.stats, + name: snippet.name, + description: snippet.description, + code: snippet.code, + inputSchema: JSON.stringify(snippet.inputSchema), + outputSchema: JSON.stringify(snippet.outputSchema), + usageHints: snippet.usageHints, + dependsOn: snippet.dependsOn, + trustLevel: snippet.trustLevel, + stats: snippet.stats, } }), - // Register a new skill + // Register a new snippet toolDefinition({ - name: 'register_skill', + name: 'register_snippet', description: - 'Save working TypeScript code as a reusable skill for future use. Only register code that has been tested and works correctly. The skill becomes available as a callable tool immediately.', + 'Save working TypeScript code as a reusable snippet for future use. Only register code that has been tested and works correctly. The snippet becomes available as a callable tool immediately.', inputSchema: z.object({ name: z .string() @@ -153,15 +153,15 @@ export function createSkillManagementTools({ 'Must be snake_case starting with a letter', ) .describe( - 'Unique skill name in snake_case (e.g., fetch_github_stats)', + 'Unique snippet name in snake_case (e.g., fetch_github_stats)', ), description: z .string() - .describe('Clear description of what the skill does'), + .describe('Clear description of what the snippet does'), code: z .string() .describe( - 'The TypeScript code. Receives `input` variable, can call external_* and skill_* functions, should return a value.', + 'The TypeScript code. Receives `input` variable, can call external_* and snippet_* functions, should return a value.', ), inputSchema: z .string() @@ -176,27 +176,27 @@ export function createSkillManagementTools({ usageHints: z .array(z.string()) .describe( - 'Hints about when to use this skill, e.g. "Use when user asks about..."', + 'Hints about when to use this snippet, e.g. "Use when user asks about..."', ), dependsOn: z .array(z.string()) .optional() .default([]) - .describe('Names of other skills this skill calls'), + .describe('Names of other snippets this snippet calls'), }), outputSchema: z.object({ success: z.boolean().optional(), - skillId: z.string().optional(), + snippetId: z.string().optional(), name: z.string().optional(), message: z.string().optional(), error: z.string().optional(), }), - }).server(async (rawSkillDef, context) => { + }).server(async (rawSnippetDef, context) => { // Parse the JSON string schemas let inputSchema: Record let outputSchema: Record try { - inputSchema = JSON.parse(rawSkillDef.inputSchema) as Record< + inputSchema = JSON.parse(rawSnippetDef.inputSchema) as Record< string, unknown > @@ -204,7 +204,7 @@ export function createSkillManagementTools({ return { error: 'inputSchema must be a valid JSON string' } } try { - outputSchema = JSON.parse(rawSkillDef.outputSchema) as Record< + outputSchema = JSON.parse(rawSnippetDef.outputSchema) as Record< string, unknown > @@ -212,28 +212,28 @@ export function createSkillManagementTools({ return { error: 'outputSchema must be a valid JSON string' } } - const skillDef = { - ...rawSkillDef, + const snippetDef = { + ...rawSnippetDef, inputSchema, outputSchema, } try { - // Validate the skill name isn't reserved - if (skillDef.name.startsWith('external_')) { - return { error: "Skill names cannot start with 'external_'" } + // Validate the snippet name isn't reserved + if (snippetDef.name.startsWith('external_')) { + return { error: "Snippet names cannot start with 'external_'" } } - if (skillDef.name.startsWith('skill_')) { + if (snippetDef.name.startsWith('snippet_')) { return { error: - "Skill names should not include the 'skill_' prefix - it will be added automatically", + "Snippet names should not include the 'snippet_' prefix - it will be added automatically", } } - // Check if skill already exists - const existing = await storage.get(skillDef.name) + // Check if snippet already exists + const existing = await storage.get(snippetDef.name) if (existing) { return { - error: `Skill '${skillDef.name}' already exists. Use a different name or update the existing skill.`, + error: `Snippet '${snippetDef.name}' already exists. Use a different name or update the existing snippet.`, } } @@ -243,54 +243,54 @@ export function createSkillManagementTools({ // Get initial trust level from strategy const initialTrustLevel = strategy.getInitialTrustLevel() - // Save the skill - const skill = await storage.save({ + // Save the snippet + const snippet = await storage.save({ id, - name: skillDef.name, - description: skillDef.description, - code: skillDef.code, - inputSchema: skillDef.inputSchema, - outputSchema: skillDef.outputSchema, - usageHints: skillDef.usageHints, - dependsOn: skillDef.dependsOn ?? [], + name: snippetDef.name, + description: snippetDef.description, + code: snippetDef.code, + inputSchema: snippetDef.inputSchema, + outputSchema: snippetDef.outputSchema, + usageHints: snippetDef.usageHints, + dependsOn: snippetDef.dependsOn ?? [], trustLevel: initialTrustLevel, stats: { executions: 0, successRate: 0 }, }) - // If registry and config are available, add the skill as a tool immediately + // If registry and config are available, add the snippet as a tool immediately if (registry && config) { - const skillTool = skillToTool({ - skill, + const snippetTool = snippetToTool({ + snippet, driver: config.driver, bindings, storage, timeout: config.timeout, memoryLimit: config.memoryLimit, }) - registry.add(skillTool) + registry.add(snippetTool) console.log( - `[register_skill] Added skill '${skill.name}' to registry immediately`, + `[register_snippet] Added snippet '${snippet.name}' to registry immediately`, ) } // Emit event for UI notification - context?.emitCustomEvent('skill:registered', { - id: skill.id, - name: skill.name, - description: skill.description, + context?.emitCustomEvent('snippet:registered', { + id: snippet.id, + name: snippet.name, + description: snippet.description, timestamp: Date.now(), }) return { success: true, - skillId: skill.id, - name: skill.name, - message: `Skill '${skill.name}' registered successfully and is now available as the '${skill.name}' tool.`, + snippetId: snippet.id, + name: snippet.name, + message: `Snippet '${snippet.name}' registered successfully and is now available as the '${snippet.name}' tool.`, } } catch (error) { - console.error('[register_skill] Error:', error) + console.error('[register_snippet] Error:', error) return { - error: `Failed to register skill: ${error instanceof Error ? error.message : String(error)}`, + error: `Failed to register snippet: ${error instanceof Error ? error.message : String(error)}`, } } }), diff --git a/packages/ai-code-mode-snippets/src/create-snippets-system-prompt.ts b/packages/ai-code-mode-snippets/src/create-snippets-system-prompt.ts new file mode 100644 index 0000000000..a974cfc015 --- /dev/null +++ b/packages/ai-code-mode-snippets/src/create-snippets-system-prompt.ts @@ -0,0 +1,289 @@ +import { generateSnippetTypes } from './generate-snippet-types' +import type { Snippet } from './types' + +interface CreateSnippetsSystemPromptOptions { + /** + * Snippets that were selected for this request + */ + selectedSnippets: Array + + /** + * Total number of snippets in the library + */ + totalSnippetCount: number + + /** + * Whether snippets are exposed as direct tools (not just sandbox bindings) + * @default true + */ + snippetsAsTools?: boolean +} + +/** + * Generate example input from a JSON Schema + */ +function generateExampleFromSchema(schema: Record): string { + if (schema.type === 'object' && schema.properties) { + const props = schema.properties as Record + const example: Record = {} + + for (const [key, value] of Object.entries(props)) { + if (value.type === 'string') example[key] = `'example_${key}'` + else if (value.type === 'number') example[key] = 0 + else if (value.type === 'boolean') example[key] = true + else if (value.type === 'array') example[key] = [] + else example[key] = null + } + + return JSON.stringify(example).replace(/"/g, '') + } + return '{}' +} + +/** + * Create system prompt documentation for the snippet library. + * This is appended to the Code Mode system prompt. + */ +export function createSnippetsSystemPrompt({ + selectedSnippets, + totalSnippetCount, + snippetsAsTools = true, +}: CreateSnippetsSystemPromptOptions): string { + // No snippets in library + if (totalSnippetCount === 0) { + return `## Snippet Library + +You have access to a snippet library for storing reusable code. The library is currently empty. + +### Snippet Management Tools + +- \`search_snippets(query, limit?)\` - Search for snippets (currently empty) +- \`get_snippet(name)\` - Get full snippet details including code +- \`register_snippet(...)\` - Save working code as a reusable snippet + +When you write useful, reusable code, consider registering it as a snippet for future use. + +**Important**: Newly registered snippets become available as tools on the **next message**, not immediately in the current conversation turn. +` + } + + // No snippets selected for this conversation + if (selectedSnippets.length === 0) { + return `## Snippet Library + +You have access to a persistent snippet library with ${totalSnippetCount} snippet${totalSnippetCount === 1 ? '' : 's'}. No snippets were pre-loaded for this conversation based on context. + +### Snippet Management Tools + +- \`search_snippets(query, limit?)\` - Search for relevant snippets +- \`get_snippet(name)\` - Get full snippet details including code +- \`register_snippet(...)\` - Save working code as a reusable snippet + +When you write useful, reusable code, consider registering it as a snippet for future use. + +**Important**: Newly registered snippets become available as tools on the **next message**, not immediately in the current conversation turn. +` + } + + if (snippetsAsTools) { + // Snippets are available as direct tools + const snippetToolDocs = selectedSnippets + .map((snippet) => { + const inputExample = generateExampleFromSchema(snippet.inputSchema) + const trustBadge = + snippet.trustLevel === 'trusted' + ? '✓ trusted' + : snippet.trustLevel === 'provisional' + ? '◐ provisional' + : '○ untrusted' + + return ` +### ${snippet.name} [${trustBadge}] + +${snippet.description} + +${snippet.usageHints.map((h) => `- ${h}`).join('\n')} + +**Input Schema:** +\`\`\`json +${JSON.stringify(snippet.inputSchema, null, 2)} +\`\`\` + +**Output Schema:** +\`\`\`json +${JSON.stringify(snippet.outputSchema, null, 2)} +\`\`\` + +**Example:** +Call the \`${snippet.name}\` tool with: ${inputExample} +` + }) + .join('\n---\n') + + return `## Snippet Library + +${selectedSnippets.length} snippet${selectedSnippets.length === 1 ? '' : 's'} pre-loaded for this conversation (${totalSnippetCount} total in library). + +### Available Snippet Tools + +These snippets are available as **direct tools** you can call (marked with [SNIPPET] in description): + +${snippetToolDocs} + +### Snippet Management Tools + +- \`search_snippets(query, limit?)\` - Find additional snippets not pre-loaded +- \`get_snippet(name)\` - Get full details of any snippet +- \`register_snippet(...)\` - Save working code as a new snippet + +### Using Snippets + +Snippets are **regular tools** - call them directly like any other tool. No need to use \`execute_typescript\`. + +### Creating New Snippets + +When you write useful, reusable code with \`execute_typescript\`, register it: + +\`\`\`typescript +// After verifying code works, call the register_snippet tool +register_snippet({ + name: 'compare_npm_packages', + description: 'Compare download counts for multiple NPM packages', + code: \` + const { packages } = input; + const results = await Promise.all( + packages.map(pkg => external_getNpmDownloads({ package: pkg })) + ); + return packages.map((pkg, i) => ({ package: pkg, downloads: results[i].downloads })) + .sort((a, b) => b.downloads - a.downloads); + \`, + inputSchema: { + type: 'object', + properties: { packages: { type: 'array', items: { type: 'string' } } }, + required: ['packages'] + }, + outputSchema: { + type: 'array', + items: { type: 'object', properties: { package: { type: 'string' }, downloads: { type: 'number' } } } + }, + usageHints: ['Use when comparing popularity of NPM packages'], + dependsOn: [], +}); +\`\`\` + +**Important**: Newly registered snippets become available as tools on the **next message**, not immediately in the current conversation turn. +` + } + + // Snippets as sandbox bindings (legacy mode) + const snippetDocs = selectedSnippets + .map((snippet) => { + const inputExample = generateExampleFromSchema(snippet.inputSchema) + const trustBadge = + snippet.trustLevel === 'trusted' + ? '✓ trusted' + : snippet.trustLevel === 'provisional' + ? '◐ provisional' + : '○ untrusted' + + return ` +### snippet_${snippet.name} [${trustBadge}] + +${snippet.description} + +${snippet.usageHints.map((h) => `- ${h}`).join('\n')} + +**Input Schema:** +\`\`\`json +${JSON.stringify(snippet.inputSchema, null, 2)} +\`\`\` + +**Output Schema:** +\`\`\`json +${JSON.stringify(snippet.outputSchema, null, 2)} +\`\`\` + +**Example:** +\`\`\`typescript +const result = await snippet_${snippet.name}(${inputExample}); +\`\`\` +` + }) + .join('\n---\n') + + // Generate type stubs for selected snippets + const typeStubs = generateSnippetTypes(selectedSnippets) + + return `## Snippet Library + +${selectedSnippets.length} snippet${selectedSnippets.length === 1 ? '' : 's'} pre-loaded for this conversation (${totalSnippetCount} total in library). + +### Pre-loaded Snippets + +These are available as \`snippet_*\` functions in your TypeScript code: + +${snippetDocs} + +### Type Definitions + +\`\`\`typescript +${typeStubs} +\`\`\` + +### Snippet Management Tools + +- \`search_snippets(query, limit?)\` - Find additional snippets not pre-loaded +- \`get_snippet(name)\` - Get full details of any snippet +- \`register_snippet(...)\` - Save working code as a new snippet + +### Using Snippets + +Snippets work just like \`external_*\` functions inside \`execute_typescript\`: + +\`\`\`typescript +// Call a pre-loaded snippet +const stats = await snippet_fetch_github_stats({ owner: 'tanstack', repo: 'query' }); + +// Compose snippets with external tools +const repos = await external_searchRepositories({ query: 'react state' }); +const detailed = await Promise.all( + repos.items.slice(0, 5).map(r => + snippet_fetch_github_stats({ owner: r.owner.login, repo: r.name }) + ) +); +\`\`\` + +### Creating New Snippets + +When you write useful, reusable code, register it: + +\`\`\`typescript +// After verifying code works, call the register_snippet tool +register_snippet({ + name: 'compare_npm_packages', + description: 'Compare download counts for multiple NPM packages', + code: \` + const { packages } = input; + const results = await Promise.all( + packages.map(pkg => external_getNpmDownloads({ package: pkg })) + ); + return packages.map((pkg, i) => ({ package: pkg, downloads: results[i].downloads })) + .sort((a, b) => b.downloads - a.downloads); + \`, + inputSchema: { + type: 'object', + properties: { packages: { type: 'array', items: { type: 'string' } } }, + required: ['packages'] + }, + outputSchema: { + type: 'array', + items: { type: 'object', properties: { package: { type: 'string' }, downloads: { type: 'number' } } } + }, + usageHints: ['Use when comparing popularity of NPM packages'], + dependsOn: [], +}); +\`\`\` + +**Important**: Newly registered snippets become available as tools on the **next message**, not immediately in the current conversation turn. +` +} diff --git a/packages/ai-code-mode-skills/src/generate-skill-types.ts b/packages/ai-code-mode-snippets/src/generate-snippet-types.ts similarity index 71% rename from packages/ai-code-mode-skills/src/generate-skill-types.ts rename to packages/ai-code-mode-snippets/src/generate-snippet-types.ts index 0faf2795bd..b33e65e239 100644 --- a/packages/ai-code-mode-skills/src/generate-skill-types.ts +++ b/packages/ai-code-mode-snippets/src/generate-snippet-types.ts @@ -1,4 +1,4 @@ -import type { Skill } from './types' +import type { Snippet } from './types' /** * Convert a JSON Schema to a TypeScript type string @@ -99,62 +99,62 @@ function toPascalCase(str: string): string { } /** - * Generate TypeScript type stubs for skills. + * Generate TypeScript type stubs for snippets. * These are included in the system prompt so the LLM knows - * the exact type signatures of available skills. + * the exact type signatures of available snippets. */ -export function generateSkillTypes(skills: Array): string { +export function generateSnippetTypes(snippets: Array): string { const declarations: Array = [] - for (const skill of skills) { - const baseName = toPascalCase(skill.name) - const inputTypeName = `Skill${baseName}Input` - const outputTypeName = `Skill${baseName}Output` + for (const snippet of snippets) { + const baseName = toPascalCase(snippet.name) + const inputTypeName = `Snippet${baseName}Input` + const outputTypeName = `Snippet${baseName}Output` // Generate input type - const inputType = schemaToType(skill.inputSchema) + const inputType = schemaToType(snippet.inputSchema) if ( - skill.inputSchema.type === 'object' && - skill.inputSchema.properties && - Object.keys(skill.inputSchema.properties).length > 0 + snippet.inputSchema.type === 'object' && + snippet.inputSchema.properties && + Object.keys(snippet.inputSchema.properties).length > 0 ) { declarations.push(`interface ${inputTypeName} ${inputType}`) } // Generate output type - const outputType = schemaToType(skill.outputSchema) + const outputType = schemaToType(snippet.outputSchema) if ( - skill.outputSchema.type === 'object' && - skill.outputSchema.properties && - Object.keys(skill.outputSchema.properties).length > 0 + snippet.outputSchema.type === 'object' && + snippet.outputSchema.properties && + Object.keys(snippet.outputSchema.properties).length > 0 ) { declarations.push(`interface ${outputTypeName} ${outputType}`) } // Determine type references const inputRef = - skill.inputSchema.type === 'object' && - skill.inputSchema.properties && - Object.keys(skill.inputSchema.properties).length > 0 + snippet.inputSchema.type === 'object' && + snippet.inputSchema.properties && + Object.keys(snippet.inputSchema.properties).length > 0 ? inputTypeName : inputType const outputRef = - skill.outputSchema.type === 'object' && - skill.outputSchema.properties && - Object.keys(skill.outputSchema.properties).length > 0 + snippet.outputSchema.type === 'object' && + snippet.outputSchema.properties && + Object.keys(snippet.outputSchema.properties).length > 0 ? outputTypeName : outputType // Generate function declaration with JSDoc - const hintsDoc = skill.usageHints.map((h) => ` * @hint ${h}`).join('\n') + const hintsDoc = snippet.usageHints.map((h) => ` * @hint ${h}`).join('\n') declarations.push( `/** - * ${skill.description} + * ${snippet.description} ${hintsDoc} */ -declare function skill_${skill.name}(input: ${inputRef}): Promise<${outputRef}>;`, +declare function snippet_${snippet.name}(input: ${inputRef}): Promise<${outputRef}>;`, ) } diff --git a/packages/ai-code-mode-snippets/src/index.ts b/packages/ai-code-mode-snippets/src/index.ts new file mode 100644 index 0000000000..f2ad73861f --- /dev/null +++ b/packages/ai-code-mode-snippets/src/index.ts @@ -0,0 +1,60 @@ +// Main entry point +export { + codeModeWithSnippets, + createCodeModeWithSnippetsConfig, +} from './code-mode-with-snippets' +export type { + CodeModeWithSnippetsOptions, + CodeModeWithSnippetsResult, +} from './code-mode-with-snippets' + +// Trust strategies +export { + createDefaultTrustStrategy, + createAlwaysTrustedStrategy, + createRelaxedTrustStrategy, + createCustomTrustStrategy, +} from './trust-strategies' +export type { TrustStrategy } from './trust-strategies' + +// Snippet selection +export { selectRelevantSnippets } from './select-relevant-snippets' + +// Snippets to tools (for direct calling) +export { snippetsToTools, snippetToTool } from './snippets-to-tools' +export type { SnippetToToolOptions } from './snippets-to-tools' + +// Snippets to bindings (for sandbox injection - legacy) +export { + snippetsToBindings, + snippetsToSimpleBindings, +} from './snippets-to-bindings' + +// Snippet management tools +export { createSnippetManagementTools } from './create-snippet-management-tools' + +// System prompt generation +export { createSnippetsSystemPrompt } from './create-snippets-system-prompt' + +// Type generation +export { generateSnippetTypes } from './generate-snippet-types' + +// Storage implementations +// +// Only the worker/browser-safe in-memory storage is re-exported from the root +// entry. The Node-only file storage (`createFileSnippetStorage`) imports +// `node:fs` / `node:path`, so it lives behind the `@tanstack/ai-code-mode-snippets/storage` +// subpath to keep this root export safe for Cloudflare Workers and browser bundlers. +export { createMemorySnippetStorage } from './storage/memory-storage' +export type { MemorySnippetStorageOptions } from './storage/memory-storage' + +// All types +export type { + Snippet, + SnippetIndexEntry, + SnippetStorage, + SnippetsConfig, + SnippetStats, + TrustLevel, + SnippetBinding, +} from './types' diff --git a/packages/ai-code-mode-skills/src/select-relevant-skills.ts b/packages/ai-code-mode-snippets/src/select-relevant-snippets.ts similarity index 62% rename from packages/ai-code-mode-skills/src/select-relevant-skills.ts rename to packages/ai-code-mode-snippets/src/select-relevant-snippets.ts index 1987523dec..212b5d4863 100644 --- a/packages/ai-code-mode-skills/src/select-relevant-skills.ts +++ b/packages/ai-code-mode-snippets/src/select-relevant-snippets.ts @@ -1,10 +1,10 @@ import { chat } from '@tanstack/ai' import type { AnyTextAdapter, ModelMessage, StreamChunk } from '@tanstack/ai' -import type { Skill, SkillIndexEntry, SkillStorage } from './types' +import type { Snippet, SnippetIndexEntry, SnippetStorage } from './types' -interface SelectRelevantSkillsOptions { +interface SelectRelevantSnippetsOptions { /** - * Text adapter for skill selection (should be a cheap/fast model) + * Text adapter for snippet selection (should be a cheap/fast model) */ adapter: AnyTextAdapter @@ -14,33 +14,33 @@ interface SelectRelevantSkillsOptions { messages: Array /** - * Skill index (lightweight metadata) + * Snippet index (lightweight metadata) */ - skillIndex: Array + snippetIndex: Array /** - * Maximum number of skills to select + * Maximum number of snippets to select */ - maxSkills: number + maxSnippets: number /** - * Storage to load full skill data + * Storage to load full snippet data */ - storage: SkillStorage + storage: SnippetStorage } /** - * Use a cheap/fast LLM to select which skills are relevant for the current conversation + * Use a cheap/fast LLM to select which snippets are relevant for the current conversation */ -export async function selectRelevantSkills({ +export async function selectRelevantSnippets({ adapter, messages, - skillIndex, - maxSkills, + snippetIndex, + maxSnippets, storage, -}: SelectRelevantSkillsOptions): Promise> { +}: SelectRelevantSnippetsOptions): Promise> { // Early exit conditions - if (skillIndex.length === 0) return [] + if (snippetIndex.length === 0) return [] if (messages.length === 0) return [] // Build context from recent messages (last 5) @@ -67,26 +67,26 @@ export async function selectRelevantSkills({ }) .join('\n') - // Build skill catalog for selection prompt - const skillCatalog = skillIndex + // Build snippet catalog for selection prompt + const snippetCatalog = snippetIndex .map((s) => { const hints = s.usageHints.length > 0 ? ` (${s.usageHints[0]})` : '' return `- ${s.name}: ${s.description}${hints}` }) .join('\n') - // Ask cheap model to select relevant skills + // Ask cheap model to select relevant snippets const selectionPrompt = `Given this conversation context: --- ${recentContext} --- -Which of these skills (if any) would be useful for the next response? Return a JSON array of skill names, max ${maxSkills}. Return [] if none are relevant. +Which of these snippets (if any) would be useful for the next response? Return a JSON array of snippet names, max ${maxSnippets}. Return [] if none are relevant. -Available skills: -${skillCatalog} +Available snippets: +${snippetCatalog} -Respond with only the JSON array, no explanation. Example: ["skill_name_1", "skill_name_2"]` +Respond with only the JSON array, no explanation. Example: ["snippet_name_1", "snippet_name_2"]` try { // Use chat to get the selection @@ -122,15 +122,15 @@ Respond with only the JSON array, no explanation. Example: ["skill_name_1", "ski return [] } - // Load full skill data for selected skills - const selectedSkills = await Promise.all( - selectedNames.slice(0, maxSkills).map((name) => storage.get(name)), + // Load full snippet data for selected snippets + const selectedSnippets = await Promise.all( + selectedNames.slice(0, maxSnippets).map((name) => storage.get(name)), ) - return selectedSkills.filter((s): s is Skill => s !== null) + return selectedSnippets.filter((s): s is Snippet => s !== null) } catch (error) { // If parsing fails or any error occurs, return empty (safe fallback) - console.warn('Skill selection failed, returning empty selection:', error) + console.warn('Snippet selection failed, returning empty selection:', error) return [] } } diff --git a/packages/ai-code-mode-skills/src/skills-to-bindings.ts b/packages/ai-code-mode-snippets/src/snippets-to-bindings.ts similarity index 51% rename from packages/ai-code-mode-skills/src/skills-to-bindings.ts rename to packages/ai-code-mode-snippets/src/snippets-to-bindings.ts index 63c1d5dbf1..ca1f71e467 100644 --- a/packages/ai-code-mode-skills/src/skills-to-bindings.ts +++ b/packages/ai-code-mode-snippets/src/snippets-to-bindings.ts @@ -1,12 +1,12 @@ import type { ToolExecutionContext } from '@tanstack/ai' import type { ToolBinding } from '@tanstack/ai-code-mode' -import type { Skill, SkillStorage } from './types' +import type { Snippet, SnippetStorage } from './types' -interface SkillsToBindingsOptions { +interface SnippetsToBindingsOptions { /** - * Skills to convert to bindings + * Snippets to convert to bindings */ - skills: Array + snippets: Array /** * Tool execution context for emitting custom events @@ -14,67 +14,67 @@ interface SkillsToBindingsOptions { context?: ToolExecutionContext /** - * Function to execute skill code in the sandbox - * The skill code receives `input` as a variable + * Function to execute snippet code in the sandbox + * The snippet code receives `input` as a variable */ executeInSandbox: (code: string, input: unknown) => Promise /** * Storage for updating execution stats */ - storage: SkillStorage + storage: SnippetStorage } /** - * Convert skills to sandbox bindings with the skill_ prefix. - * Skills become callable functions inside the sandbox. + * Convert snippets to sandbox bindings with the snippet_ prefix. + * Snippets become callable functions inside the sandbox. */ -export function skillsToBindings({ - skills, +export function snippetsToBindings({ + snippets, context, executeInSandbox, storage, -}: SkillsToBindingsOptions): Record { +}: SnippetsToBindingsOptions): Record { const bindings: Record = {} - for (const skill of skills) { - const bindingName = `skill_${skill.name}` + for (const snippet of snippets) { + const bindingName = `snippet_${snippet.name}` bindings[bindingName] = { name: bindingName, - description: skill.description, - inputSchema: skill.inputSchema, - outputSchema: skill.outputSchema, + description: snippet.description, + inputSchema: snippet.inputSchema, + outputSchema: snippet.outputSchema, execute: async (input: unknown) => { const startTime = Date.now() - // Emit skill call event - context?.emitCustomEvent('code_mode:skill_call', { - skill: skill.name, + // Emit snippet call event + context?.emitCustomEvent('code_mode:snippet_call', { + snippet: snippet.name, input, timestamp: startTime, }) try { - // Wrap the skill code to receive input as a variable + // Wrap the snippet code to receive input as a variable const wrappedCode = ` const input = ${JSON.stringify(input)}; - ${skill.code} + ${snippet.code} ` const result = await executeInSandbox(wrappedCode, input) const duration = Date.now() - startTime // Emit success event - context?.emitCustomEvent('code_mode:skill_result', { - skill: skill.name, + context?.emitCustomEvent('code_mode:snippet_result', { + snippet: snippet.name, result, duration, timestamp: Date.now(), }) // Update stats (async, don't await to not block) - storage.updateStats(skill.name, true).catch(() => { + storage.updateStats(snippet.name, true).catch(() => { // Silently ignore stats update failures }) @@ -83,15 +83,15 @@ export function skillsToBindings({ const duration = Date.now() - startTime // Emit error event - context?.emitCustomEvent('code_mode:skill_error', { - skill: skill.name, + context?.emitCustomEvent('code_mode:snippet_error', { + snippet: snippet.name, error: error instanceof Error ? error.message : String(error), duration, timestamp: Date.now(), }) // Update stats (async, don't await) - storage.updateStats(skill.name, false).catch(() => { + storage.updateStats(snippet.name, false).catch(() => { // Silently ignore stats update failures }) @@ -105,27 +105,27 @@ export function skillsToBindings({ } /** - * Create a simple binding record for skills without full sandbox execution. - * This is used when skills are being documented in the system prompt + * Create a simple binding record for snippets without full sandbox execution. + * This is used when snippets are being documented in the system prompt * but not yet being executed. */ -export function skillsToSimpleBindings( - skills: Array, +export function snippetsToSimpleBindings( + snippets: Array, ): Record { const bindings: Record = {} - for (const skill of skills) { - const bindingName = `skill_${skill.name}` + for (const snippet of snippets) { + const bindingName = `snippet_${snippet.name}` bindings[bindingName] = { name: bindingName, - description: skill.description, - inputSchema: skill.inputSchema, - outputSchema: skill.outputSchema, + description: snippet.description, + inputSchema: snippet.inputSchema, + outputSchema: snippet.outputSchema, execute: () => Promise.reject( new Error( - `Skill ${skill.name} is not available for execution in this context`, + `Snippet ${snippet.name} is not available for execution in this context`, ), ), } diff --git a/packages/ai-code-mode-skills/src/skills-to-tools.ts b/packages/ai-code-mode-snippets/src/snippets-to-tools.ts similarity index 67% rename from packages/ai-code-mode-skills/src/skills-to-tools.ts rename to packages/ai-code-mode-snippets/src/snippets-to-tools.ts index 3699d9f58f..f65af3754f 100644 --- a/packages/ai-code-mode-skills/src/skills-to-tools.ts +++ b/packages/ai-code-mode-snippets/src/snippets-to-tools.ts @@ -15,19 +15,19 @@ import type { IsolateDriver, ToolBinding, } from '@tanstack/ai-code-mode' -import type { Skill, SkillStorage } from './types' +import type { Snippet, SnippetStorage } from './types' /** - * Options for converting a single skill to a tool + * Options for converting a single snippet to a tool */ -export interface SkillToToolOptions { +export interface SnippetToToolOptions { /** - * The skill to convert + * The snippet to convert */ - skill: Skill + snippet: Snippet /** - * Isolate driver for executing skill code + * Isolate driver for executing snippet code */ driver: IsolateDriver @@ -39,10 +39,10 @@ export interface SkillToToolOptions { /** * Storage for updating execution stats */ - storage: SkillStorage + storage: SnippetStorage /** - * Timeout for skill execution in ms + * Timeout for snippet execution in ms * @default 30000 */ timeout?: number @@ -54,30 +54,30 @@ export interface SkillToToolOptions { memoryLimit?: number } -interface SkillsToToolsOptions { +interface SnippetsToToolsOptions { /** - * Skills to convert to tools + * Snippets to convert to tools */ - skills: Array + snippets: Array /** - * Isolate driver for executing skill code + * Isolate driver for executing snippet code */ driver: IsolateDriver /** * Original tools that become external_* bindings - * (so skills can call external_* functions) + * (so snippets can call external_* functions) */ tools: Array /** * Storage for updating execution stats */ - storage: SkillStorage + storage: SnippetStorage /** - * Timeout for skill execution in ms + * Timeout for snippet execution in ms * @default 30000 */ timeout?: number @@ -149,33 +149,33 @@ function jsonSchemaToZod(schema: Record): z.ZodType { } /** - * Convert a single skill to a ServerTool that the LLM can call directly. - * The skill executes its code in the sandbox with access to external_* bindings. + * Convert a single snippet to a ServerTool that the LLM can call directly. + * The snippet executes its code in the sandbox with access to external_* bindings. */ -export function skillToTool({ - skill, +export function snippetToTool({ + snippet, driver, bindings, storage, timeout = 30000, memoryLimit = 128, -}: SkillToToolOptions): ServerTool { +}: SnippetToToolOptions): ServerTool { // Generate input and output schemas from JSON Schema - const inputSchema = jsonSchemaToZod(skill.inputSchema) - const outputSchema = jsonSchemaToZod(skill.outputSchema) + const inputSchema = jsonSchemaToZod(snippet.inputSchema) + const outputSchema = jsonSchemaToZod(snippet.outputSchema) return toolDefinition({ - name: skill.name, - description: `[SKILL] ${skill.description}`, + name: snippet.name, + description: `[SNIPPET] ${snippet.description}`, inputSchema, outputSchema, }).server(async (input: unknown, context?: ToolExecutionContext) => { const startTime = Date.now() const emitCustomEvent = context?.emitCustomEvent || (() => {}) - // Emit skill call event - emitCustomEvent('code_mode:skill_call', { - skill: skill.name, + // Emit snippet call event + emitCustomEvent('code_mode:snippet_call', { + snippet: snippet.name, input, timestamp: startTime, }) @@ -184,24 +184,24 @@ export function skillToTool({ try { console.log( - `[Skill:${skill.name}] Starting execution with input:`, + `[Snippet:${snippet.name}] Starting execution with input:`, JSON.stringify(input).substring(0, 200), ) - // Wrap the skill code to receive input as a variable + // Wrap the snippet code to receive input as a variable const wrappedCode = ` const input = ${JSON.stringify(input)}; - ${skill.code} + ${snippet.code} ` console.log( - `[Skill:${skill.name}] Wrapped code (first 500 chars):`, + `[Snippet:${snippet.name}] Wrapped code (first 500 chars):`, wrappedCode.substring(0, 500), ) // Strip TypeScript to JavaScript const strippedCode = await stripTypeScript(wrappedCode) console.log( - `[Skill:${skill.name}] Stripped code (first 500 chars):`, + `[Snippet:${snippet.name}] Stripped code (first 500 chars):`, strippedCode.substring(0, 500), ) @@ -211,23 +211,23 @@ export function skillToTool({ emitCustomEvent, ) console.log( - `[Skill:${skill.name}] Event-aware bindings:`, + `[Snippet:${snippet.name}] Event-aware bindings:`, Object.keys(eventAwareBindings), ) // Create sandbox context - console.log(`[Skill:${skill.name}] Creating sandbox context...`) + console.log(`[Snippet:${snippet.name}] Creating sandbox context...`) isolateContext = await driver.createContext({ bindings: eventAwareBindings, timeout, memoryLimit, }) - console.log(`[Skill:${skill.name}] Sandbox context created`) + console.log(`[Snippet:${snippet.name}] Sandbox context created`) // Execute the code - console.log(`[Skill:${skill.name}] Executing code...`) + console.log(`[Snippet:${snippet.name}] Executing code...`) const executionResult = await isolateContext.execute(strippedCode) - console.log(`[Skill:${skill.name}] Execution result:`, { + console.log(`[Snippet:${snippet.name}] Execution result:`, { success: executionResult.success, hasValue: 'value' in executionResult, error: executionResult.error, @@ -238,46 +238,46 @@ export function skillToTool({ if (!executionResult.success) { console.error( - `[Skill:${skill.name}] Execution failed:`, + `[Snippet:${snippet.name}] Execution failed:`, executionResult.error, ) throw new Error( - executionResult.error?.message || 'Skill execution failed', + executionResult.error?.message || 'Snippet execution failed', ) } // Emit success event - emitCustomEvent('code_mode:skill_result', { - skill: skill.name, + emitCustomEvent('code_mode:snippet_result', { + snippet: snippet.name, result: executionResult.value, duration, timestamp: Date.now(), }) // Update stats (async, don't await to not block) - storage.updateStats(skill.name, true).catch(() => { + storage.updateStats(snippet.name, true).catch(() => { // Silently ignore stats update failures }) return executionResult.value } catch (error) { const duration = Date.now() - startTime - console.error(`[Skill:${skill.name}] CAUGHT ERROR:`, { + console.error(`[Snippet:${snippet.name}] CAUGHT ERROR:`, { message: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, duration, }) // Emit error event - emitCustomEvent('code_mode:skill_error', { - skill: skill.name, + emitCustomEvent('code_mode:snippet_error', { + snippet: snippet.name, error: error instanceof Error ? error.message : String(error), duration, timestamp: Date.now(), }) // Update stats (async, don't await) - storage.updateStats(skill.name, false).catch(() => { + storage.updateStats(snippet.name, false).catch(() => { // Silently ignore stats update failures }) @@ -291,28 +291,30 @@ export function skillToTool({ } /** - * Convert multiple skills to ServerTools that the LLM can call directly. - * Skills become real tools that execute their code in the sandbox. + * Convert multiple snippets to ServerTools that the LLM can call directly. + * Snippets become real tools that execute their code in the sandbox. */ -export function skillsToTools({ - skills, +export function snippetsToTools({ + snippets, driver, tools, storage, timeout = 30000, memoryLimit = 128, -}: SkillsToToolsOptions): Array> { - // Pre-compute bindings from tools (these are shared across all skill executions) +}: SnippetsToToolsOptions): Array< + ServerTool +> { + // Pre-compute bindings from tools (these are shared across all snippet executions) console.log( - '[SkillsToTools] Creating bindings from tools:', + '[SnippetsToTools] Creating bindings from tools:', tools.map((t) => t.name), ) const bindings = toolsToBindings(tools, 'external_') - console.log('[SkillsToTools] Created bindings:', Object.keys(bindings)) + console.log('[SnippetsToTools] Created bindings:', Object.keys(bindings)) - return skills.map((skill) => - skillToTool({ - skill, + return snippets.map((snippet) => + snippetToTool({ + snippet, driver, bindings, storage, diff --git a/packages/ai-code-mode-snippets/src/storage/file-storage.ts b/packages/ai-code-mode-snippets/src/storage/file-storage.ts new file mode 100644 index 0000000000..d0aa288bfe --- /dev/null +++ b/packages/ai-code-mode-snippets/src/storage/file-storage.ts @@ -0,0 +1,275 @@ +import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { existsSync } from 'node:fs' +import { createDefaultTrustStrategy } from '../trust-strategies' +import type { + Snippet, + SnippetIndexEntry, + SnippetSearchOptions, + SnippetStorage, +} from '../types' +import type { TrustStrategy } from '../trust-strategies' + +export interface FileSnippetStorageOptions { + /** + * Directory path for storing snippets + */ + directory: string + + /** + * Trust strategy for determining snippet trust levels + * @default createDefaultTrustStrategy() + */ + trustStrategy?: TrustStrategy +} + +/** + * File-system based snippet storage + * + * Directory structure: + * .snippets/ + * _index.json # Fast catalog loading + * fetch_github_stats/ + * meta.json # Metadata (description, schemas, hints, stats) + * code.ts # The actual TypeScript code + * deploy_to_prod/ + * meta.json + * code.ts + */ +export function createFileSnippetStorage( + directoryOrOptions: string | FileSnippetStorageOptions, +): SnippetStorage { + const options = + typeof directoryOrOptions === 'string' + ? { directory: directoryOrOptions } + : directoryOrOptions + + const { directory, trustStrategy = createDefaultTrustStrategy() } = options + const indexPath = join(directory, '_index.json') + + console.log('[FileSnippetStorage] Initialized with directory:', directory) + + // Snippet names are used both as on-disk directory segments and as + // `snippet_` sandbox tool names, so they must be a single safe + // identifier segment. Rejecting anything else keeps an LLM-supplied name + // (e.g. `../../etc`) from escaping `directory` during read/write/delete. + const SAFE_SNIPPET_NAME = /^[A-Za-z0-9_-]+$/ + + function isSafeSnippetName(name: string): boolean { + return typeof name === 'string' && SAFE_SNIPPET_NAME.test(name) + } + + function assertSafeSnippetName(name: string): void { + if (!isSafeSnippetName(name)) { + throw new Error( + `Invalid snippet name ${JSON.stringify( + name, + )}: names must match /^[A-Za-z0-9_-]+$/ (no path separators or traversal).`, + ) + } + } + + async function ensureDirectory(): Promise { + if (!existsSync(directory)) { + console.log('[FileSnippetStorage] Creating directory:', directory) + await mkdir(directory, { recursive: true }) + } + } + + async function loadIndex(): Promise> { + await ensureDirectory() + + if (!existsSync(indexPath)) { + return [] + } + + const content = await readFile(indexPath, 'utf-8') + return JSON.parse(content) as Array + } + + async function loadAll(): Promise> { + const index = await loadIndex() + const snippets: Array = [] + + for (const entry of index) { + const snippet = await get(entry.name) + if (snippet) { + snippets.push(snippet) + } + } + + return snippets + } + + async function saveIndex(index: Array): Promise { + await writeFile(indexPath, JSON.stringify(index, null, 2)) + } + + async function get(name: string): Promise { + // Treat an unsafe name as "not found" rather than reading outside `directory`. + if (!isSafeSnippetName(name)) { + return null + } + + const snippetDir = join(directory, name) + const metaPath = join(snippetDir, 'meta.json') + const codePath = join(snippetDir, 'code.ts') + + if (!existsSync(metaPath)) { + return null + } + + const [metaContent, code] = await Promise.all([ + readFile(metaPath, 'utf-8'), + readFile(codePath, 'utf-8'), + ]) + + const meta = JSON.parse(metaContent) as Omit + return { ...meta, code } + } + + async function save( + snippet: Omit, + ): Promise { + assertSafeSnippetName(snippet.name) + await ensureDirectory() + + const snippetDir = join(directory, snippet.name) + const metaPath = join(snippetDir, 'meta.json') + const codePath = join(snippetDir, 'code.ts') + + const now = new Date().toISOString() + const existing = await get(snippet.name) + + const fullSnippet: Snippet = { + ...snippet, + createdAt: existing?.createdAt ?? now, + updatedAt: now, + } + + // Separate code from metadata + const { code, ...meta } = fullSnippet + + // Write snippet files + await mkdir(snippetDir, { recursive: true }) + await Promise.all([ + writeFile(metaPath, JSON.stringify(meta, null, 2)), + writeFile(codePath, code), + ]) + + // Update index + const index = await loadIndex() + const indexEntry: SnippetIndexEntry = { + id: fullSnippet.id, + name: fullSnippet.name, + description: fullSnippet.description, + usageHints: fullSnippet.usageHints, + trustLevel: fullSnippet.trustLevel, + } + + const existingIdx = index.findIndex((s) => s.name === snippet.name) + if (existingIdx >= 0) { + index[existingIdx] = indexEntry + } else { + index.push(indexEntry) + } + await saveIndex(index) + + return fullSnippet + } + + async function deleteSnippet(name: string): Promise { + assertSafeSnippetName(name) + + const snippetDir = join(directory, name) + + if (!existsSync(snippetDir)) { + return false + } + + await rm(snippetDir, { recursive: true }) + + // Update index + const index = await loadIndex() + const filtered = index.filter((s) => s.name !== name) + await saveIndex(filtered) + + return true + } + + async function search( + query: string, + searchOptions: SnippetSearchOptions = {}, + ): Promise> { + const { limit = 5 } = searchOptions + const index = await loadIndex() + + // Simple text matching - can be replaced with embeddings + const queryLower = query.toLowerCase() + const terms = queryLower.split(/\s+/) + + const scored = index.map((snippet) => { + let score = 0 + const searchText = [ + snippet.name, + snippet.description, + ...snippet.usageHints, + ] + .join(' ') + .toLowerCase() + + for (const term of terms) { + if (searchText.includes(term)) { + score += 1 + } + // Boost exact name matches + if (snippet.name.toLowerCase().includes(term)) { + score += 2 + } + } + + return { snippet, score } + }) + + return scored + .filter((s) => s.score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, limit) + .map((s) => s.snippet) + } + + async function updateStats(name: string, success: boolean): Promise { + const snippet = await get(name) + if (!snippet) return + + const { executions, successRate } = snippet.stats + const newExecutions = executions + 1 + const newSuccessRate = + (successRate * executions + (success ? 1 : 0)) / newExecutions + + const newStats = { executions: newExecutions, successRate: newSuccessRate } + + // Use trust strategy to calculate new trust level + const newTrustLevel = trustStrategy.calculateTrustLevel( + snippet.trustLevel, + newStats, + ) + + await save({ + ...snippet, + stats: newStats, + trustLevel: newTrustLevel, + }) + } + + return { + loadIndex, + loadAll, + get, + save, + delete: deleteSnippet, + search, + updateStats, + trustStrategy, + } +} diff --git a/packages/ai-code-mode-snippets/src/storage/index.ts b/packages/ai-code-mode-snippets/src/storage/index.ts new file mode 100644 index 0000000000..834bc3ec4d --- /dev/null +++ b/packages/ai-code-mode-snippets/src/storage/index.ts @@ -0,0 +1,6 @@ +// Storage implementations +export { createFileSnippetStorage } from './file-storage' +export { createMemorySnippetStorage } from './memory-storage' + +// Re-export types +export type { SnippetStorage, Snippet, SnippetIndexEntry } from '../types' diff --git a/packages/ai-code-mode-snippets/src/storage/memory-storage.ts b/packages/ai-code-mode-snippets/src/storage/memory-storage.ts new file mode 100644 index 0000000000..553d59d4cb --- /dev/null +++ b/packages/ai-code-mode-snippets/src/storage/memory-storage.ts @@ -0,0 +1,172 @@ +import { createDefaultTrustStrategy } from '../trust-strategies' +import type { + Snippet, + SnippetIndexEntry, + SnippetSearchOptions, + SnippetStorage, +} from '../types' +import type { TrustStrategy } from '../trust-strategies' + +export interface MemorySnippetStorageOptions { + /** + * Initial snippets to populate the storage with + */ + initialSnippets?: Array + + /** + * Trust strategy for determining snippet trust levels + * @default createDefaultTrustStrategy() + */ + trustStrategy?: TrustStrategy +} + +/** + * In-memory snippet storage for testing and demos + */ +export function createMemorySnippetStorage( + optionsOrSnippets: MemorySnippetStorageOptions | Array = [], +): SnippetStorage { + const options = Array.isArray(optionsOrSnippets) + ? { initialSnippets: optionsOrSnippets } + : optionsOrSnippets + + const { initialSnippets = [], trustStrategy = createDefaultTrustStrategy() } = + options + + // Store snippets in a Map for O(1) lookup + const snippets = new Map() + + // Initialize with any provided snippets + for (const snippet of initialSnippets) { + snippets.set(snippet.name, snippet) + } + + function loadIndex(): Promise> { + return Promise.resolve( + Array.from(snippets.values()).map((snippet) => ({ + id: snippet.id, + name: snippet.name, + description: snippet.description, + usageHints: snippet.usageHints, + trustLevel: snippet.trustLevel, + })), + ) + } + + function loadAll(): Promise> { + return Promise.resolve(Array.from(snippets.values())) + } + + function get(name: string): Promise { + return Promise.resolve(snippets.get(name) ?? null) + } + + function save( + snippet: Omit, + ): Promise { + const now = new Date().toISOString() + const existing = snippets.get(snippet.name) + + const fullSnippet: Snippet = { + ...snippet, + createdAt: existing?.createdAt ?? now, + updatedAt: now, + } + + snippets.set(snippet.name, fullSnippet) + return Promise.resolve(fullSnippet) + } + + function deleteSnippet(name: string): Promise { + if (!snippets.has(name)) { + return Promise.resolve(false) + } + snippets.delete(name) + return Promise.resolve(true) + } + + function search( + query: string, + searchOptions: SnippetSearchOptions = {}, + ): Promise> { + const { limit = 5 } = searchOptions + + // Simple text matching + const queryLower = query.toLowerCase() + const terms = queryLower.split(/\s+/) + + const scored = Array.from(snippets.values()).map((snippet) => { + let score = 0 + const searchText = [ + snippet.name, + snippet.description, + ...snippet.usageHints, + ] + .join(' ') + .toLowerCase() + + for (const term of terms) { + if (searchText.includes(term)) { + score += 1 + } + // Boost exact name matches + if (snippet.name.toLowerCase().includes(term)) { + score += 2 + } + } + + return { snippet, score } + }) + + return Promise.resolve( + scored + .filter((s) => s.score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, limit) + .map((s) => ({ + id: s.snippet.id, + name: s.snippet.name, + description: s.snippet.description, + usageHints: s.snippet.usageHints, + trustLevel: s.snippet.trustLevel, + })), + ) + } + + function updateStats(name: string, success: boolean): Promise { + const snippet = snippets.get(name) + if (!snippet) return Promise.resolve() + + const { executions, successRate } = snippet.stats + const newExecutions = executions + 1 + const newSuccessRate = + (successRate * executions + (success ? 1 : 0)) / newExecutions + + const newStats = { executions: newExecutions, successRate: newSuccessRate } + + // Use trust strategy to calculate new trust level + const newTrustLevel = trustStrategy.calculateTrustLevel( + snippet.trustLevel, + newStats, + ) + + snippets.set(name, { + ...snippet, + stats: newStats, + trustLevel: newTrustLevel, + updatedAt: new Date().toISOString(), + }) + return Promise.resolve() + } + + return { + loadIndex, + loadAll, + get, + save, + delete: deleteSnippet, + search, + updateStats, + trustStrategy, + } +} diff --git a/packages/ai-code-mode-skills/src/trust-strategies.ts b/packages/ai-code-mode-snippets/src/trust-strategies.ts similarity index 87% rename from packages/ai-code-mode-skills/src/trust-strategies.ts rename to packages/ai-code-mode-snippets/src/trust-strategies.ts index ef5067abf3..bcf3235362 100644 --- a/packages/ai-code-mode-skills/src/trust-strategies.ts +++ b/packages/ai-code-mode-snippets/src/trust-strategies.ts @@ -1,11 +1,11 @@ -import type { SkillStats, TrustLevel } from './types' +import type { SnippetStats, TrustLevel } from './types' /** - * Strategy for determining skill trust levels + * Strategy for determining snippet trust levels */ export interface TrustStrategy { /** - * Get the initial trust level for a newly created skill + * Get the initial trust level for a newly created snippet */ getInitialTrustLevel: () => TrustLevel @@ -14,14 +14,14 @@ export interface TrustStrategy { */ calculateTrustLevel: ( currentLevel: TrustLevel, - stats: SkillStats, + stats: SnippetStats, ) => TrustLevel } /** - * Default trust strategy - skills must earn trust through successful executions + * Default trust strategy - snippets must earn trust through successful executions * - * - untrusted: New skill (0 executions) + * - untrusted: New snippet (0 executions) * - provisional: 10+ executions with ≥90% success rate * - trusted: 100+ executions with ≥95% success rate */ @@ -54,7 +54,7 @@ export function createDefaultTrustStrategy(): TrustStrategy { } /** - * Always trusted strategy - skills are immediately trusted upon creation + * Always trusted strategy - snippets are immediately trusted upon creation * * Use this for development/testing or when you trust the LLM's code generation */ @@ -68,7 +68,7 @@ export function createAlwaysTrustedStrategy(): TrustStrategy { /** * Relaxed trust strategy - faster trust promotion for development * - * - untrusted: New skill (0 executions) + * - untrusted: New snippet (0 executions) * - provisional: 3+ executions with ≥80% success rate * - trusted: 10+ executions with ≥90% success rate */ diff --git a/packages/ai-code-mode-skills/src/types.ts b/packages/ai-code-mode-snippets/src/types.ts similarity index 55% rename from packages/ai-code-mode-skills/src/types.ts rename to packages/ai-code-mode-snippets/src/types.ts index 332c1484eb..1209d9d46e 100644 --- a/packages/ai-code-mode-skills/src/types.ts +++ b/packages/ai-code-mode-snippets/src/types.ts @@ -7,7 +7,7 @@ import type { TrustStrategy } from './trust-strategies' // ============================================================================ /** - * Trust level for a skill + * Trust level for a snippet * - untrusted: Newly created, not yet proven * - provisional: Has been successfully executed 10+ times with 90%+ success * - trusted: Has been successfully executed 100+ times with 95%+ success @@ -15,15 +15,15 @@ import type { TrustStrategy } from './trust-strategies' export type TrustLevel = 'untrusted' | 'provisional' | 'trusted' // ============================================================================ -// Skill Statistics +// Snippet Statistics // ============================================================================ /** - * Execution statistics for a skill + * Execution statistics for a snippet */ -export interface SkillStats { +export interface SnippetStats { /** - * Total number of times this skill has been executed + * Total number of times this snippet has been executed */ executions: number @@ -34,34 +34,34 @@ export interface SkillStats { } // ============================================================================ -// Skill Types +// Snippet Types // ============================================================================ /** - * A reusable skill that can be executed in the Code Mode sandbox + * A reusable snippet that can be executed in the Code Mode sandbox */ -export interface Skill { +export interface Snippet { /** - * Unique identifier for the skill + * Unique identifier for the snippet */ id: string /** * Unique name in snake_case (e.g., 'fetch_github_stats') - * This becomes the function name with skill_ prefix in the sandbox + * This becomes the function name with snippet_ prefix in the sandbox */ name: string /** - * Human-readable description of what the skill does + * Human-readable description of what the snippet does */ description: string /** - * TypeScript code that implements the skill + * TypeScript code that implements the snippet * The code receives `input` as a variable and can call: * - external_* functions (tools) - * - other skill_* functions (skills) + * - other snippet_* functions (snippets) * Should return a value */ code: string @@ -77,13 +77,13 @@ export interface Skill { outputSchema: Record /** - * Hints about when to use this skill + * Hints about when to use this snippet * e.g., "Use when comparing NPM package popularity" */ usageHints: Array /** - * Names of other skills this skill depends on/calls + * Names of other snippets this snippet depends on/calls */ dependsOn: Array @@ -95,29 +95,29 @@ export interface Skill { /** * Execution statistics */ - stats: SkillStats + stats: SnippetStats /** - * ISO timestamp when the skill was created + * ISO timestamp when the snippet was created */ createdAt: string /** - * ISO timestamp when the skill was last updated + * ISO timestamp when the snippet was last updated */ updatedAt: string } // ============================================================================ -// Skill Index Types +// Snippet Index Types // ============================================================================ /** - * Lightweight skill entry for the index (metadata only, no code) - * Used for fast loading and skill selection + * Lightweight snippet entry for the index (metadata only, no code) + * Used for fast loading and snippet selection */ -export type SkillIndexEntry = Pick< - Skill, +export type SnippetIndexEntry = Pick< + Snippet, 'id' | 'name' | 'description' | 'usageHints' | 'trustLevel' > @@ -126,9 +126,9 @@ export type SkillIndexEntry = Pick< // ============================================================================ /** - * Options for searching skills + * Options for searching snippets */ -export interface SkillSearchOptions { +export interface SnippetSearchOptions { /** * Maximum number of results to return * @default 5 @@ -137,49 +137,49 @@ export interface SkillSearchOptions { } /** - * Interface for skill storage implementations + * Interface for snippet storage implementations */ -export interface SkillStorage { +export interface SnippetStorage { /** - * Load the skill index (lightweight metadata for all skills) + * Load the snippet index (lightweight metadata for all snippets) */ - loadIndex: () => Promise> + loadIndex: () => Promise> /** - * Load all skills with full details (including code) + * Load all snippets with full details (including code) */ - loadAll: () => Promise> + loadAll: () => Promise> /** - * Get a skill by name + * Get a snippet by name */ - get: (name: string) => Promise + get: (name: string) => Promise /** - * Save a skill (create or update) + * Save a snippet (create or update) */ - save: (skill: Omit) => Promise + save: (snippet: Omit) => Promise /** - * Delete a skill by name + * Delete a snippet by name */ delete: (name: string) => Promise /** - * Search for skills by query + * Search for snippets by query */ search: ( query: string, - options?: SkillSearchOptions, - ) => Promise> + options?: SnippetSearchOptions, + ) => Promise> /** - * Update execution statistics for a skill + * Update execution statistics for a snippet */ updateStats: (name: string, success: boolean) => Promise /** - * Trust strategy used by this storage (optional, for creating new skills) + * Trust strategy used by this storage (optional, for creating new snippets) */ trustStrategy?: TrustStrategy } @@ -189,101 +189,101 @@ export interface SkillStorage { // ============================================================================ /** - * Configuration for the skills system + * Configuration for the snippets system */ -export interface SkillsConfig { +export interface SnippetsConfig { /** - * Storage implementation for skills + * Storage implementation for snippets */ - storage: SkillStorage + storage: SnippetStorage /** - * Maximum number of skills to load into context per request + * Maximum number of snippets to load into context per request * @default 5 */ - maxSkillsInContext?: number + maxSnippetsInContext?: number /** - * Trust strategy for determining skill trust levels + * Trust strategy for determining snippet trust levels * @default createDefaultTrustStrategy() */ trustStrategy?: TrustStrategy } /** - * Options for codeModeWithSkills + * Options for codeModeWithSnippets */ -export interface CodeModeWithSkillsOptions { +export interface CodeModeWithSnippetsOptions { /** * Code Mode tool configuration (driver, tools, timeout, memoryLimit) */ config: CodeModeToolConfig /** - * Text adapter for skill selection (should be a cheap/fast model) + * Text adapter for snippet selection (should be a cheap/fast model) */ adapter: AnyTextAdapter /** - * Skills configuration + * Snippets configuration */ - skills: SkillsConfig + snippets: SnippetsConfig /** - * Current conversation messages (used for context-aware skill selection) + * Current conversation messages (used for context-aware snippet selection) */ messages: Array /** - * Whether to include skills as direct tools (not just sandbox bindings). - * When true, skills become first-class tools the LLM can call directly. + * Whether to include snippets as direct tools (not just sandbox bindings). + * When true, snippets become first-class tools the LLM can call directly. * @default true */ - skillsAsTools?: boolean + snippetsAsTools?: boolean } /** - * Result from codeModeWithSkills + * Result from codeModeWithSnippets */ -export interface CodeModeWithSkillsResult { +export interface CodeModeWithSnippetsResult { /** * Tool registry for dynamic tool management. * Pass this to chat() via the toolRegistry option. - * Skills registered mid-stream will be added to this registry. + * Snippets registered mid-stream will be added to this registry. */ toolsRegistry: ToolRegistry /** - * System prompt documenting available skills and external functions + * System prompt documenting available snippets and external functions */ systemPrompt: string /** - * Skills that were selected for this request + * Snippets that were selected for this request */ - selectedSkills: Array + selectedSnippets: Array } // ============================================================================ -// Skill Binding Types (internal) +// Snippet Binding Types (internal) // ============================================================================ /** - * A skill transformed into a format suitable for sandbox injection + * A snippet transformed into a format suitable for sandbox injection */ -export interface SkillBinding { +export interface SnippetBinding { /** - * Function name with skill_ prefix + * Function name with snippet_ prefix */ name: string /** - * The skill this binding wraps + * The snippet this binding wraps */ - skill: Skill + snippet: Snippet /** - * Execute function that runs the skill code + * Execute function that runs the snippet code */ execute: (input: unknown) => Promise } diff --git a/packages/ai-code-mode-skills/test-cli/adapters.ts b/packages/ai-code-mode-snippets/test-cli/adapters.ts similarity index 100% rename from packages/ai-code-mode-skills/test-cli/adapters.ts rename to packages/ai-code-mode-snippets/test-cli/adapters.ts diff --git a/packages/ai-code-mode-skills/test-cli/cli.ts b/packages/ai-code-mode-snippets/test-cli/cli.ts similarity index 97% rename from packages/ai-code-mode-skills/test-cli/cli.ts rename to packages/ai-code-mode-snippets/test-cli/cli.ts index 44fef8407b..3a050d16f4 100644 --- a/packages/ai-code-mode-skills/test-cli/cli.ts +++ b/packages/ai-code-mode-snippets/test-cli/cli.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node /** - * CLI for testing the TanStack AI Code Mode Skills system + * CLI for testing the TanStack AI Code Mode Snippets system * * Commands: * - run: Run tests across multiple adapters (like smoke-tests) @@ -341,7 +341,7 @@ async function runCommand(options: { }) { console.log(`${colors.bright}${colors.cyan}`) console.log('╔═══════════════════════════════════════════════════════════╗') - console.log('║ TanStack AI Code Mode Skills - Multi-Adapter Tests ║') + console.log('║ TanStack AI Code Mode Snippets - Multi-Adapter Tests ║') console.log('╚═══════════════════════════════════════════════════════════╝') console.log(colors.reset) @@ -394,7 +394,7 @@ async function runCommand(options: { testsToRun = getDefaultTests() } - console.log('🚀 Starting skills tests') + console.log('🚀 Starting snippets tests') console.log(` Adapters: ${adaptersToRun.map((a) => a.name).join(', ')}`) console.log(` Tests: ${testsToRun.map((t) => t.id).join(', ')}`) console.log(` Parallel: ${parallel}`) @@ -441,8 +441,8 @@ async function runCommand(options: { } const program = new Command() - .name('skills-test') - .description('Test the TanStack AI Code Mode Skills system') + .name('snippets-test') + .description('Test the TanStack AI Code Mode Snippets system') .version('0.0.1') // Run command (primary) @@ -455,7 +455,7 @@ program ) .option( '--tests ', - 'Comma-separated list of test IDs (e.g., SIM,SKL,STR)', + 'Comma-separated list of test IDs (e.g., SIM,SNP,STR)', ) .option( '--parallel ', @@ -480,7 +480,9 @@ program .action(async () => { console.log(`${colors.bright}${colors.cyan}`) console.log('╔═══════════════════════════════════════════════════════════╗') - console.log('║ TanStack AI Code Mode Skills - Simulated Test ║') + console.log( + '║ TanStack AI Code Mode Snippets - Simulated Test ║', + ) console.log('╚═══════════════════════════════════════════════════════════╝') console.log(colors.reset) @@ -507,7 +509,9 @@ program .action(async () => { console.log(`${colors.bright}${colors.cyan}`) console.log('╔═══════════════════════════════════════════════════════════╗') - console.log('║ TanStack AI Code Mode Skills - Registry Test ║') + console.log( + '║ TanStack AI Code Mode Snippets - Registry Test ║', + ) console.log('╚═══════════════════════════════════════════════════════════╝') console.log(colors.reset) @@ -541,7 +545,9 @@ program .action(async (options) => { console.log(`${colors.bright}${colors.cyan}`) console.log('╔═══════════════════════════════════════════════════════════╗') - console.log('║ TanStack AI Code Mode Skills - Live Test ║') + console.log( + '║ TanStack AI Code Mode Snippets - Live Test ║', + ) console.log('╚═══════════════════════════════════════════════════════════╝') console.log(colors.reset) diff --git a/packages/ai-code-mode-skills/test-cli/env.example b/packages/ai-code-mode-snippets/test-cli/env.example similarity index 100% rename from packages/ai-code-mode-skills/test-cli/env.example rename to packages/ai-code-mode-snippets/test-cli/env.example diff --git a/packages/ai-code-mode-skills/test-cli/index.ts b/packages/ai-code-mode-snippets/test-cli/index.ts similarity index 95% rename from packages/ai-code-mode-skills/test-cli/index.ts rename to packages/ai-code-mode-snippets/test-cli/index.ts index eba2422712..32ce8394d2 100644 --- a/packages/ai-code-mode-skills/test-cli/index.ts +++ b/packages/ai-code-mode-snippets/test-cli/index.ts @@ -1,5 +1,5 @@ /** - * CLI module exports for testing the skills system + * CLI module exports for testing the snippets system */ export { runSimulatedTest } from './simulated-test' diff --git a/packages/ai-code-mode-skills/test-cli/live-test.ts b/packages/ai-code-mode-snippets/test-cli/live-test.ts similarity index 64% rename from packages/ai-code-mode-skills/test-cli/live-test.ts rename to packages/ai-code-mode-snippets/test-cli/live-test.ts index 0f55473569..adca2c3374 100644 --- a/packages/ai-code-mode-skills/test-cli/live-test.ts +++ b/packages/ai-code-mode-snippets/test-cli/live-test.ts @@ -1,14 +1,14 @@ /** - * Live test for the skills system + * Live test for the snippets system * * Uses a real LLM adapter (OpenAI or Anthropic) to test: - * 1. First run: Create a skill using code mode - * 2. Second run: Use the saved skill + * 1. First run: Create a snippet using code mode + * 2. Second run: Use the saved snippet */ import { chat, maxIterations } from '@tanstack/ai' import { createNodeIsolateDriver } from '@tanstack/ai-isolate-node' -import { codeModeWithSkills } from '../src/code-mode-with-skills' +import { codeModeWithSnippets } from '../src/code-mode-with-snippets' import { addNumbersTool, createTestStorage, @@ -45,7 +45,7 @@ export async function runLiveTest( ): Promise { const { adapter, verbose = false } = options - logSection('Live Skills Test') + logSection('Live Snippets Test') logInfo(`Using adapter: ${adapter.name} with model: ${adapter.model}`) // Create shared storage that persists between phases @@ -61,16 +61,16 @@ export async function runLiveTest( phase1: { success: false }, phase2: { success: false }, }, - skillCreated: false, - skillUsed: false, + snippetCreated: false, + snippetUsed: false, } // ========================================================================= - // Phase 1: First run - Create skill using code mode + // Phase 1: First run - Create snippet using code mode // ========================================================================= - logSection('Phase 1: Skill Creation') - logStep(1, 'Running code mode with real LLM (no existing skills)') + logSection('Phase 1: Snippet Creation') + logStep(1, 'Running code mode with real LLM (no existing snippets)') try { const messages1: Array = [ @@ -80,16 +80,16 @@ export async function runLiveTest( IMPORTANT INSTRUCTIONS: 1. Use the execute_typescript tool to call external_add_numbers({ a: 5, b: 3 }) to get the answer -2. After getting the result, use register_skill to save a reusable skill called "add_two_numbers" that wraps this pattern -3. The skill should accept { a: number, b: number } as input and return the result from external_add_numbers +2. After getting the result, use register_snippet to save a reusable snippet called "add_two_numbers" that wraps this pattern +3. The snippet should accept { a: number, b: number } as input and return the result from external_add_numbers -Please complete all three steps: execute the code, register the skill, and tell me the answer.`, +Please complete all three steps: execute the code, register the snippet, and tell me the answer.`, }, ] - // Get tools and system prompt with skills integration + // Get tools and system prompt with snippets integration const { tools: tools1, systemPrompt: systemPrompt1 } = - await codeModeWithSkills({ + await codeModeWithSnippets({ config: { driver, tools: [addNumbersTool], @@ -97,9 +97,9 @@ Please complete all three steps: execute the code, register the skill, and tell memoryLimit: 128, }, adapter, - skills: { + snippets: { storage, - maxSkillsInContext: 5, + maxSnippetsInContext: 5, }, messages: messages1, }) @@ -124,7 +124,7 @@ Please complete all three steps: execute the code, register the skill, and tell let toolCallCount1 = 0 let executeTypescriptCalled = false - let registerSkillCalled = false + let registerSnippetCalled = false let fullContent = '' for await (const chunk of stream1 as AsyncIterable) { @@ -140,8 +140,8 @@ Please complete all three steps: execute the code, register the skill, and tell if (toolName === 'execute_typescript') { executeTypescriptCalled = true } - if (toolName === 'register_skill') { - registerSkillCalled = true + if (toolName === 'register_snippet') { + registerSnippetCalled = true } } else if (chunk.type === 'tool_result') { if (verbose) { @@ -158,24 +158,26 @@ Please complete all three steps: execute the code, register the skill, and tell logInfo(`LLM response: ${fullContent.substring(0, 500)}...`) } - // Verify skill was created - const skillIndex = await storage.loadIndex() - const skillCreated = skillIndex.some((s) => s.name === 'add_two_numbers') + // Verify snippet was created + const snippetIndex = await storage.loadIndex() + const snippetCreated = snippetIndex.some( + (s) => s.name === 'add_two_numbers', + ) - if (skillCreated) { - result.skillCreated = true - logSuccess('Skill "add_two_numbers" was created successfully') + if (snippetCreated) { + result.snippetCreated = true + logSuccess('Snippet "add_two_numbers" was created successfully') - // Log the created skill - const skill = await storage.get('add_two_numbers') - if (skill && verbose) { - logInfo(`Skill details:`) - logInfo(` Description: ${skill.description}`) - logInfo(` Code: ${skill.code.substring(0, 100)}...`) + // Log the created snippet + const snippet = await storage.get('add_two_numbers') + if (snippet && verbose) { + logInfo(`Snippet details:`) + logInfo(` Description: ${snippet.description}`) + logInfo(` Code: ${snippet.code.substring(0, 100)}...`) } } else { logWarning( - 'Skill was not created - LLM may not have followed instructions', + 'Snippet was not created - LLM may not have followed instructions', ) } @@ -184,9 +186,9 @@ Please complete all three steps: execute the code, register the skill, and tell details: { toolCallCount: toolCallCount1, executeTypescriptCalled, - registerSkillCalled, - skillCreated, - skillsInStorage: skillIndex.length, + registerSnippetCalled, + snippetCreated, + snippetsInStorage: snippetIndex.length, }, } @@ -204,20 +206,20 @@ Please complete all three steps: execute the code, register the skill, and tell } // ========================================================================= - // Phase 2: Second run - Use the saved skill (if created) + // Phase 2: Second run - Use the saved snippet (if created) // ========================================================================= - logSection('Phase 2: Skill Reuse') + logSection('Phase 2: Snippet Reuse') - // Only run phase 2 if a skill was created - if (!result.skillCreated) { - logWarning('Skipping Phase 2 - no skill was created in Phase 1') + // Only run phase 2 if a snippet was created + if (!result.snippetCreated) { + logWarning('Skipping Phase 2 - no snippet was created in Phase 1') result.phases.phase2 = { success: false, - error: 'No skill was created in Phase 1', + error: 'No snippet was created in Phase 1', } } else { - logStep(1, 'Running code mode with real LLM (skill should be available)') + logStep(1, 'Running code mode with real LLM (snippet should be available)') try { const messages2: Array = [ @@ -225,16 +227,16 @@ Please complete all three steps: execute the code, register the skill, and tell role: 'user', content: `What is 10 + 20? -If you have a skill called "add_two_numbers" available, please use it directly instead of execute_typescript.`, +If you have a snippet called "add_two_numbers" available, please use it directly instead of execute_typescript.`, }, ] - // Get tools and system prompt with skills integration + // Get tools and system prompt with snippets integration const { tools: tools2, systemPrompt: systemPrompt2, - selectedSkills, - } = await codeModeWithSkills({ + selectedSnippets, + } = await codeModeWithSnippets({ config: { driver, tools: [addNumbersTool], @@ -242,9 +244,9 @@ If you have a skill called "add_two_numbers" available, please use it directly i memoryLimit: 128, }, adapter, - skills: { + snippets: { storage, - maxSkillsInContext: 5, + maxSnippetsInContext: 5, }, messages: messages2, }) @@ -253,18 +255,18 @@ If you have a skill called "add_two_numbers" available, please use it directly i `Phase 2 tools available: ${tools2.map((t: any) => t.name).join(', ')}`, ) logInfo( - `Selected skills: ${selectedSkills.map((s) => s.name).join(', ') || 'none'}`, + `Selected snippets: ${selectedSnippets.map((s) => s.name).join(', ') || 'none'}`, ) - // Check if skill is now available as a tool - const skillToolAvailable = tools2.some( + // Check if snippet is now available as a tool + const snippetToolAvailable = tools2.some( (t: any) => t.name === 'add_two_numbers', ) - if (skillToolAvailable) { - logSuccess('Skill "add_two_numbers" is available as a tool') + if (snippetToolAvailable) { + logSuccess('Snippet "add_two_numbers" is available as a tool') } else { logWarning( - 'Skill is not available as a tool (may not have been selected by LLM)', + 'Snippet is not available as a tool (may not have been selected by LLM)', ) } @@ -280,9 +282,9 @@ If you have a skill called "add_two_numbers" available, please use it directly i }) let toolCallCount2 = 0 - let skillCalled = false - let skillExecutedSuccessfully = false - let skillExecutionError: string | undefined + let snippetCalled = false + let snippetExecutedSuccessfully = false + let snippetExecutionError: string | undefined let executeTypescriptCalledPhase2 = false let fullContent2 = '' @@ -292,26 +294,26 @@ If you have a skill called "add_two_numbers" available, please use it directly i const toolName = chunk.toolCall.function.name logInfo(`Tool called: ${toolName}`) if (toolName === 'add_two_numbers') { - skillCalled = true + snippetCalled = true } if (toolName === 'execute_typescript') { executeTypescriptCalledPhase2 = true } } else if (chunk.type === 'tool_result') { - // Check if this is the skill result and if it succeeded - if (skillCalled && chunk.toolCallId) { + // Check if this is the snippet result and if it succeeded + if (snippetCalled && chunk.toolCallId) { // Check if the result contains an error const resultContent = chunk.content if ( resultContent.includes('error') || resultContent.includes('Error') ) { - skillExecutionError = resultContent.substring(0, 200) - logError(`Skill execution failed: ${skillExecutionError}`) + snippetExecutionError = resultContent.substring(0, 200) + logError(`Snippet execution failed: ${snippetExecutionError}`) } else { - skillExecutedSuccessfully = true + snippetExecutedSuccessfully = true if (verbose) { - logInfo(`Skill result: ${resultContent.substring(0, 200)}`) + logInfo(`Snippet result: ${resultContent.substring(0, 200)}`) } } } @@ -326,39 +328,39 @@ If you have a skill called "add_two_numbers" available, please use it directly i logInfo(`LLM response: ${fullContent2.substring(0, 500)}...`) } - result.skillUsed = skillCalled && skillExecutedSuccessfully + result.snippetUsed = snippetCalled && snippetExecutedSuccessfully // Consider phase 2 successful if either: - // 1. The skill was called directly AND executed successfully, OR - // 2. The skill wasn't available (selection issue) but execute_typescript worked + // 1. The snippet was called directly AND executed successfully, OR + // 2. The snippet wasn't available (selection issue) but execute_typescript worked const phase2Success = - (skillCalled && skillExecutedSuccessfully) || + (snippetCalled && snippetExecutedSuccessfully) || executeTypescriptCalledPhase2 result.phases.phase2 = { success: phase2Success, details: { toolCallCount: toolCallCount2, - skillCalled, - skillExecutedSuccessfully, - skillExecutionError, + snippetCalled, + snippetExecutedSuccessfully, + snippetExecutionError, executeTypescriptCalled: executeTypescriptCalledPhase2, - skillToolAvailable, - selectedSkillCount: selectedSkills.length, + snippetToolAvailable, + selectedSnippetCount: selectedSnippets.length, }, } - if (skillCalled && skillExecutedSuccessfully) { + if (snippetCalled && snippetExecutedSuccessfully) { logSuccess( - 'Phase 2 completed successfully - skill was called and executed correctly!', + 'Phase 2 completed successfully - snippet was called and executed correctly!', ) - } else if (skillCalled && !skillExecutedSuccessfully) { + } else if (snippetCalled && !snippetExecutedSuccessfully) { logError( - `Phase 2 failed - skill was called but execution failed: ${skillExecutionError}`, + `Phase 2 failed - snippet was called but execution failed: ${snippetExecutionError}`, ) } else if (executeTypescriptCalledPhase2) { logWarning( - 'Phase 2 completed but LLM used execute_typescript instead of the skill', + 'Phase 2 completed but LLM used execute_typescript instead of the snippet', ) } else { logError('Phase 2 failed - no tool was called') @@ -387,8 +389,8 @@ If you have a skill called "add_two_numbers" available, please use it directly i logInfo( `✓ execute_typescript used in Phase 1: ${result.phases.phase1.details?.executeTypescriptCalled}`, ) - logInfo(`✓ Skill created: ${result.skillCreated}`) - logInfo(`✓ Skill used successfully in Phase 2: ${result.skillUsed}`) + logInfo(`✓ Snippet created: ${result.snippetCreated}`) + logInfo(`✓ Snippet used successfully in Phase 2: ${result.snippetUsed}`) } else { logError('Test failed') if (!result.phases.phase1.success) { @@ -398,13 +400,13 @@ If you have a skill called "add_two_numbers" available, please use it directly i const phase2Details = result.phases.phase2.details as | Record | undefined - if (phase2Details?.skillExecutionError) { + if (phase2Details?.snippetExecutionError) { logError( - `Phase 2 (Skill Reuse): Skill execution failed - ${phase2Details.skillExecutionError}`, + `Phase 2 (Snippet Reuse): Snippet execution failed - ${phase2Details.snippetExecutionError}`, ) } else { logError( - `Phase 2 (Skill Reuse): ${result.phases.phase2.error || 'Failed'}`, + `Phase 2 (Snippet Reuse): ${result.phases.phase2.error || 'Failed'}`, ) } } diff --git a/packages/ai-code-mode-skills/test-cli/mock-adapter.ts b/packages/ai-code-mode-snippets/test-cli/mock-adapter.ts similarity index 98% rename from packages/ai-code-mode-skills/test-cli/mock-adapter.ts rename to packages/ai-code-mode-snippets/test-cli/mock-adapter.ts index 0926dfea91..43c84c8fb8 100644 --- a/packages/ai-code-mode-skills/test-cli/mock-adapter.ts +++ b/packages/ai-code-mode-snippets/test-cli/mock-adapter.ts @@ -2,7 +2,7 @@ * Mock Text Adapter for deterministic testing * * This adapter returns predetermined responses based on a response sequence, - * allowing for fully deterministic testing of the skills system. + * allowing for fully deterministic testing of the snippets system. */ import type { diff --git a/packages/ai-code-mode-skills/test-cli/registry-test.ts b/packages/ai-code-mode-snippets/test-cli/registry-test.ts similarity index 68% rename from packages/ai-code-mode-skills/test-cli/registry-test.ts rename to packages/ai-code-mode-snippets/test-cli/registry-test.ts index eb3ab11fe8..ad4276c095 100644 --- a/packages/ai-code-mode-skills/test-cli/registry-test.ts +++ b/packages/ai-code-mode-snippets/test-cli/registry-test.ts @@ -2,24 +2,24 @@ * Simulated test for dynamic ToolRegistry functionality * * Tests that: - * 1. codeModeWithSkills returns a ToolRegistry instead of a tools array + * 1. codeModeWithSnippets returns a ToolRegistry instead of a tools array * 2. The registry contains expected initial tools - * 3. Skills registered mid-stream are immediately added to the registry - * 4. The newly registered skill becomes available as a callable tool + * 3. Snippets registered mid-stream are immediately added to the registry + * 4. The newly registered snippet becomes available as a callable tool */ import { chat, maxIterations } from '@tanstack/ai' import { createNodeIsolateDriver } from '@tanstack/ai-isolate-node' -import { codeModeWithSkills } from '../src/code-mode-with-skills' +import { codeModeWithSnippets } from '../src/code-mode-with-snippets' import { createMockTextAdapter, singleToolCall, textResponse, } from './mock-adapter' import { - EXPECTED_SKILL_CODE, - EXPECTED_SKILL_INPUT_SCHEMA, - EXPECTED_SKILL_OUTPUT_SCHEMA, + EXPECTED_SNIPPET_CODE, + EXPECTED_SNIPPET_INPUT_SCHEMA, + EXPECTED_SNIPPET_OUTPUT_SCHEMA, addNumbersTool, createTestStorage, logError, @@ -55,11 +55,11 @@ export interface RegistryTestResult { } /** - * Create a mock adapter for skill selection (no skills initially) + * Create a mock adapter for snippet selection (no snippets initially) */ -function createSkillSelectionAdapter(skillNames: Array) { +function createSnippetSelectionAdapter(snippetNames: Array) { return createMockTextAdapter({ - responses: [textResponse(JSON.stringify(skillNames))], + responses: [textResponse(JSON.stringify(snippetNames))], }) } @@ -79,7 +79,9 @@ return result; */ export async function runRegistryTest(): Promise { logSection('ToolRegistry Dynamic Registration Test') - logInfo('Testing that skills registered mid-stream are immediately available') + logInfo( + 'Testing that snippets registered mid-stream are immediately available', + ) const storage = createTestStorage() const driver = createNodeIsolateDriver({ @@ -101,17 +103,17 @@ export async function runRegistryTest(): Promise { // ========================================================================= logSection('Phase 1: Registry Setup Verification') - logStep(1, 'Calling codeModeWithSkills to get ToolRegistry') + logStep(1, 'Calling codeModeWithSnippets to get ToolRegistry') let registry: ToolRegistry try { - const selectionAdapter = createSkillSelectionAdapter([]) + const selectionAdapter = createSnippetSelectionAdapter([]) const messages: Array = [ { role: 'user', content: 'What is 5 + 3?' }, ] - const codeWithSkillsResult = await codeModeWithSkills({ + const codeWithSnippetsResult = await codeModeWithSnippets({ config: { driver, tools: [addNumbersTool], @@ -119,14 +121,14 @@ export async function runRegistryTest(): Promise { memoryLimit: 128, }, adapter: selectionAdapter, - skills: { + snippets: { storage, - maxSkillsInContext: 5, + maxSnippetsInContext: 5, }, messages, }) - registry = codeWithSkillsResult.toolsRegistry + registry = codeWithSnippetsResult.toolsRegistry // Verify registry is returned (not a tools array) const hasGetTools = typeof registry.getTools === 'function' @@ -134,10 +136,12 @@ export async function runRegistryTest(): Promise { const hasHas = typeof registry.has === 'function' if (!hasGetTools || !hasAdd || !hasHas) { - throw new Error('codeModeWithSkills did not return a valid ToolRegistry') + throw new Error( + 'codeModeWithSnippets did not return a valid ToolRegistry', + ) } - logSuccess('ToolRegistry returned from codeModeWithSkills') + logSuccess('ToolRegistry returned from codeModeWithSnippets') // Check initial tools const initialTools = registry.getTools() @@ -145,42 +149,45 @@ export async function runRegistryTest(): Promise { logInfo(`Initial tools: ${toolNames.join(', ')}`) const hasExecuteTypescript = registry.has('execute_typescript') - const hasSearchSkills = registry.has('search_skills') - const hasGetSkill = registry.has('get_skill') - const hasRegisterSkill = registry.has('register_skill') + const hasSearchSnippets = registry.has('search_snippets') + const hasGetSnippet = registry.has('get_snippet') + const hasRegisterSnippet = registry.has('register_snippet') if (!hasExecuteTypescript) { logError('Missing execute_typescript tool') } - if (!hasSearchSkills) { - logError('Missing search_skills tool') + if (!hasSearchSnippets) { + logError('Missing search_snippets tool') } - if (!hasGetSkill) { - logError('Missing get_skill tool') + if (!hasGetSnippet) { + logError('Missing get_snippet tool') } - if (!hasRegisterSkill) { - logError('Missing register_skill tool') + if (!hasRegisterSnippet) { + logError('Missing register_snippet tool') } const hasAllExpectedTools = - hasExecuteTypescript && hasSearchSkills && hasGetSkill && hasRegisterSkill - - // Verify NO skill tools exist yet (since no skills in storage) - const skillToolsBefore = toolNames.filter( - (n) => n.startsWith('skill_') || n === 'add_two_numbers', + hasExecuteTypescript && + hasSearchSnippets && + hasGetSnippet && + hasRegisterSnippet + + // Verify NO snippet tools exist yet (since no snippets in storage) + const snippetToolsBefore = toolNames.filter( + (n) => n.startsWith('snippet_') || n === 'add_two_numbers', ) - const noSkillToolsYet = skillToolsBefore.length === 0 + const noSnippetToolsYet = snippetToolsBefore.length === 0 result.phases.setup = { - success: hasAllExpectedTools && noSkillToolsYet, + success: hasAllExpectedTools && noSnippetToolsYet, details: { toolCount: initialTools.length, toolNames, hasExecuteTypescript, - hasSearchSkills, - hasGetSkill, - hasRegisterSkill, - noSkillToolsYet, + hasSearchSnippets, + hasGetSnippet, + hasRegisterSnippet, + noSnippetToolsYet, }, } @@ -188,7 +195,7 @@ export async function runRegistryTest(): Promise { logSuccess('Phase 1 passed: Registry has all expected initial tools') } else { logError( - 'Phase 1 failed: Missing expected tools or unexpected skill tools', + 'Phase 1 failed: Missing expected tools or unexpected snippet tools', ) logInfo( `Details: ${JSON.stringify(result.phases.setup.details, null, 2)}`, @@ -204,11 +211,11 @@ export async function runRegistryTest(): Promise { } // ========================================================================= - // Phase 2: Registration - Register a skill mid-stream and verify it's added + // Phase 2: Registration - Register a snippet mid-stream and verify it's added // ========================================================================= - logSection('Phase 2: Mid-Stream Skill Registration') - logStep(1, 'Setting up mock adapter for skill registration') + logSection('Phase 2: Mid-Stream Snippet Registration') + logStep(1, 'Setting up mock adapter for snippet registration') try { const chatAdapter = createMockTextAdapter({ @@ -222,15 +229,15 @@ export async function runRegistryTest(): Promise { 'call_execute_1', ), - // Second: Register the skill + // Second: Register the snippet singleToolCall( - 'register_skill', + 'register_snippet', { name: 'add_two_numbers', description: 'Add two numbers together using the add_numbers tool', - code: EXPECTED_SKILL_CODE, - inputSchema: JSON.stringify(EXPECTED_SKILL_INPUT_SCHEMA), - outputSchema: JSON.stringify(EXPECTED_SKILL_OUTPUT_SCHEMA), + code: EXPECTED_SNIPPET_CODE, + inputSchema: JSON.stringify(EXPECTED_SNIPPET_INPUT_SCHEMA), + outputSchema: JSON.stringify(EXPECTED_SNIPPET_OUTPUT_SCHEMA), usageHints: ['Use when the user wants to add two numbers'], dependsOn: [], }, @@ -238,7 +245,7 @@ export async function runRegistryTest(): Promise { ), // Third: Final response - textResponse('Done! I created an add_two_numbers skill.'), + textResponse('Done! I created an add_two_numbers snippet.'), ], onResponse: (index, response) => { logInfo( @@ -250,7 +257,7 @@ export async function runRegistryTest(): Promise { const messages: Array = [ { role: 'user', - content: 'Please add 5 + 3 and create a reusable skill for it.', + content: 'Please add 5 + 3 and create a reusable snippet for it.', }, ] @@ -268,23 +275,23 @@ export async function runRegistryTest(): Promise { agentLoopStrategy: maxIterations(10), }) - let registerSkillCalled = false - let registerSkillResult: any = null + let registerSnippetCalled = false + let registerSnippetResult: any = null for await (const chunk of stream as AsyncIterable) { if (chunk.type === 'tool_call') { const toolName = chunk.toolCall.function.name logInfo(`Tool called: ${toolName}`) - if (toolName === 'register_skill') { - registerSkillCalled = true + if (toolName === 'register_snippet') { + registerSnippetCalled = true } } else if (chunk.type === 'tool_result') { logInfo(`Tool result for: ${chunk.toolCallId}`) - // Check if this is the register_skill result + // Check if this is the register_snippet result if (chunk.toolCallId === 'call_register_1') { - registerSkillResult = chunk.result + registerSnippetResult = chunk.result logInfo( - `register_skill result: ${JSON.stringify(registerSkillResult)}`, + `register_snippet result: ${JSON.stringify(registerSnippetResult)}`, ) } } else if (chunk.type === 'done') { @@ -297,26 +304,26 @@ export async function runRegistryTest(): Promise { logInfo(`Tools after chat: ${toolsAfter}`) const toolsIncreased = toolsAfter > toolsBefore - const hasNewSkillTool = registry.has('add_two_numbers') + const hasNewSnippetTool = registry.has('add_two_numbers') result.phases.registration = { - success: registerSkillCalled && toolsIncreased && hasNewSkillTool, + success: registerSnippetCalled && toolsIncreased && hasNewSnippetTool, details: { - registerSkillCalled, + registerSnippetCalled, toolsBefore, toolsAfter, toolsIncreased, - hasNewSkillTool, - registerSkillResult, + hasNewSnippetTool, + registerSnippetResult, }, } if (result.phases.registration.success) { logSuccess( - 'Phase 2 passed: Skill registered and added to registry mid-stream', + 'Phase 2 passed: Snippet registered and added to registry mid-stream', ) } else { - logError('Phase 2 failed: Skill was not properly added to registry') + logError('Phase 2 failed: Snippet was not properly added to registry') logInfo( `Details: ${JSON.stringify(result.phases.registration.details, null, 2)}`, ) @@ -331,14 +338,14 @@ export async function runRegistryTest(): Promise { } // ========================================================================= - // Phase 3: Verification - Confirm the new skill tool is callable + // Phase 3: Verification - Confirm the new snippet tool is callable // ========================================================================= - logSection('Phase 3: Skill Tool Verification') - logStep(1, 'Verifying the newly registered skill is a callable tool') + logSection('Phase 3: Snippet Tool Verification') + logStep(1, 'Verifying the newly registered snippet is a callable tool') try { - // Get the skill tool from the registry + // Get the snippet tool from the registry const addTwoNumbersTool = registry.get('add_two_numbers') if (!addTwoNumbersTool) { @@ -358,19 +365,19 @@ export async function runRegistryTest(): Promise { logInfo(`Has inputSchema: ${hasInputSchema}`) logInfo(`Has execute function: ${hasExecute}`) - // Now run a second chat that uses the skill directly - logStep(2, 'Running a second chat that calls the skill directly') + // Now run a second chat that uses the snippet directly + logStep(2, 'Running a second chat that calls the snippet directly') const secondChatAdapter = createMockTextAdapter({ responses: [ - // Directly call the newly registered skill + // Directly call the newly registered snippet singleToolCall( 'add_two_numbers', { a: 10, b: 20, }, - 'call_skill_1', + 'call_snippet_1', ), // Final response @@ -395,21 +402,21 @@ export async function runRegistryTest(): Promise { agentLoopStrategy: maxIterations(5), }) - let skillToolCalled = false - let skillToolResultReceived = false + let snippetToolCalled = false + let snippetToolResultReceived = false for await (const chunk of stream2 as AsyncIterable) { if (chunk.type === 'tool_call') { const toolName = chunk.toolCall.function.name logInfo(`Tool called: ${toolName}`) if (toolName === 'add_two_numbers') { - skillToolCalled = true + snippetToolCalled = true } } else if (chunk.type === 'tool_result') { - if (chunk.toolCallId === 'call_skill_1') { - // The skill executed and returned a result (value is in the execution context) - skillToolResultReceived = true - logInfo('Skill tool execution completed') + if (chunk.toolCallId === 'call_snippet_1') { + // The snippet executed and returned a result (value is in the execution context) + snippetToolResultReceived = true + logInfo('Snippet tool execution completed') } } else if (chunk.type === 'done') { logInfo(`Second chat done: ${chunk.finishReason}`) @@ -417,31 +424,34 @@ export async function runRegistryTest(): Promise { } // The key verification is that: - // 1. The skill tool exists in the registry + // 1. The snippet tool exists in the registry // 2. The chat function called it successfully // 3. The tool_result was received (execution happened) // The actual execution logs above show "[add_numbers] Adding 10 + 20" which proves - // the skill code ran and called the external tool correctly. + // the snippet code ran and called the external tool correctly. result.phases.verification = { success: - hasName && hasDescription && skillToolCalled && skillToolResultReceived, + hasName && + hasDescription && + snippetToolCalled && + snippetToolResultReceived, details: { hasName, hasDescription, hasInputSchema, hasExecute, - skillToolCalled, - skillToolResultReceived, + snippetToolCalled, + snippetToolResultReceived, }, } if (result.phases.verification.success) { logSuccess( - 'Phase 3 passed: Skill tool is callable and returns correct result', + 'Phase 3 passed: Snippet tool is callable and returns correct result', ) } else { - logError('Phase 3 failed: Skill tool verification failed') + logError('Phase 3 failed: Snippet tool verification failed') logInfo( `Details: ${JSON.stringify(result.phases.verification.details, null, 2)}`, ) @@ -467,9 +477,9 @@ export async function runRegistryTest(): Promise { if (result.passed) { logSuccess('All ToolRegistry tests passed!') - logInfo('✓ Registry returned from codeModeWithSkills') - logInfo('✓ Skills registered mid-stream are added to registry') - logInfo('✓ Newly registered skills are callable as tools') + logInfo('✓ Registry returned from codeModeWithSnippets') + logInfo('✓ Snippets registered mid-stream are added to registry') + logInfo('✓ Newly registered snippets are callable as tools') } else { logError('Some ToolRegistry tests failed') if (!result.phases.setup.success) { diff --git a/packages/ai-code-mode-skills/test-cli/simulated-test.ts b/packages/ai-code-mode-snippets/test-cli/simulated-test.ts similarity index 64% rename from packages/ai-code-mode-skills/test-cli/simulated-test.ts rename to packages/ai-code-mode-snippets/test-cli/simulated-test.ts index 15bd58150f..5d06bb96a4 100644 --- a/packages/ai-code-mode-skills/test-cli/simulated-test.ts +++ b/packages/ai-code-mode-snippets/test-cli/simulated-test.ts @@ -1,23 +1,23 @@ /** - * Simulated test for the skills system + * Simulated test for the snippets system * * Uses a mock adapter with predetermined responses to test: - * 1. First run: Create a skill using code mode - * 2. Second run: Use the saved skill + * 1. First run: Create a snippet using code mode + * 2. Second run: Use the saved snippet */ import { chat, maxIterations } from '@tanstack/ai' import { createNodeIsolateDriver } from '@tanstack/ai-isolate-node' -import { codeModeWithSkills } from '../src/code-mode-with-skills' +import { codeModeWithSnippets } from '../src/code-mode-with-snippets' import { createMockTextAdapter, singleToolCall, textResponse, } from './mock-adapter' import { - EXPECTED_SKILL_CODE, - EXPECTED_SKILL_INPUT_SCHEMA, - EXPECTED_SKILL_OUTPUT_SCHEMA, + EXPECTED_SNIPPET_CODE, + EXPECTED_SNIPPET_INPUT_SCHEMA, + EXPECTED_SNIPPET_OUTPUT_SCHEMA, addNumbersTool, createTestStorage, logError, @@ -30,14 +30,14 @@ import type { ModelMessage, StreamChunk, ToolRegistry } from '@tanstack/ai' import type { TestResult } from './test-utils' /** - * Create a mock adapter specifically for skill selection - * Returns JSON arrays of skill names based on the skill index + * Create a mock adapter specifically for snippet selection + * Returns JSON arrays of snippet names based on the snippet index */ -function createSkillSelectionAdapter(skillNames: Array) { +function createSnippetSelectionAdapter(snippetNames: Array) { return createMockTextAdapter({ responses: [ - // Always return the skill names as JSON array - textResponse(JSON.stringify(skillNames)), + // Always return the snippet names as JSON array + textResponse(JSON.stringify(snippetNames)), ], }) } @@ -57,8 +57,8 @@ return result; * Run the simulated test */ export async function runSimulatedTest(): Promise { - logSection('Simulated Skills Test') - logInfo('Testing skill creation and reuse with mock adapter') + logSection('Simulated Snippets Test') + logInfo('Testing snippet creation and reuse with mock adapter') // Create shared storage that persists between phases const storage = createTestStorage() @@ -73,23 +73,23 @@ export async function runSimulatedTest(): Promise { phase1: { success: false }, phase2: { success: false }, }, - skillCreated: false, - skillUsed: false, + snippetCreated: false, + snippetUsed: false, } // ========================================================================= - // Phase 1: First run - Create skill using code mode + // Phase 1: First run - Create snippet using code mode // ========================================================================= - logSection('Phase 1: Skill Creation') - logStep(1, 'Setting up mock adapter with skill creation responses') + logSection('Phase 1: Snippet Creation') + logStep(1, 'Setting up mock adapter with snippet creation responses') - // Mock adapter for skill selection in Phase 1 (no skills exist yet) - const phase1SelectionAdapter = createSkillSelectionAdapter([]) + // Mock adapter for snippet selection in Phase 1 (no snippets exist yet) + const phase1SelectionAdapter = createSnippetSelectionAdapter([]) // Mock responses for Phase 1 main chat: // 1. First, use execute_typescript to solve the problem - // 2. Then, register the skill for future use + // 2. Then, register the snippet for future use // 3. Finally, provide the answer const phase1ChatAdapter = createMockTextAdapter({ responses: [ @@ -102,15 +102,15 @@ export async function runSimulatedTest(): Promise { 'call_execute_1', ), - // Response 2: Register the skill + // Response 2: Register the snippet singleToolCall( - 'register_skill', + 'register_snippet', { name: 'add_two_numbers', description: 'Add two numbers together using the add_numbers tool', - code: EXPECTED_SKILL_CODE, - inputSchema: JSON.stringify(EXPECTED_SKILL_INPUT_SCHEMA), - outputSchema: JSON.stringify(EXPECTED_SKILL_OUTPUT_SCHEMA), + code: EXPECTED_SNIPPET_CODE, + inputSchema: JSON.stringify(EXPECTED_SNIPPET_INPUT_SCHEMA), + outputSchema: JSON.stringify(EXPECTED_SNIPPET_OUTPUT_SCHEMA), usageHints: ['Use when the user wants to add two numbers'], dependsOn: [], }, @@ -119,7 +119,7 @@ export async function runSimulatedTest(): Promise { // Response 3: Final answer textResponse( - 'The answer is 8. I have also saved this as a skill called "add_two_numbers" for future use.', + 'The answer is 8. I have also saved this as a snippet called "add_two_numbers" for future use.', ), ], onResponse: (index, response) => { @@ -130,30 +130,33 @@ export async function runSimulatedTest(): Promise { }) try { - logStep(2, 'Running code mode with skills (first run, no existing skills)') + logStep( + 2, + 'Running code mode with snippets (first run, no existing snippets)', + ) const messages1: Array = [ { role: 'user', content: - 'What is 5 + 3? Please create a skill for adding numbers after solving this.', + 'What is 5 + 3? Please create a snippet for adding numbers after solving this.', }, ] - // Get registry and system prompt with skills integration - // Note: We use the selection adapter for skill selection, then the chat adapter for the actual chat + // Get registry and system prompt with snippets integration + // Note: We use the selection adapter for snippet selection, then the chat adapter for the actual chat const { toolsRegistry: registry1, systemPrompt: systemPrompt1 } = - await codeModeWithSkills({ + await codeModeWithSnippets({ config: { driver, tools: [addNumbersTool], timeout: 30000, memoryLimit: 128, }, - adapter: phase1SelectionAdapter, // Used for skill selection (returns []) - skills: { + adapter: phase1SelectionAdapter, // Used for snippet selection (returns []) + snippets: { storage, - maxSkillsInContext: 5, + maxSnippetsInContext: 5, }, messages: messages1, }) @@ -177,7 +180,7 @@ export async function runSimulatedTest(): Promise { let toolCallCount1 = 0 let executeTypescriptCalled = false - let registerSkillCalled = false + let registerSnippetCalled = false for await (const chunk of stream1 as AsyncIterable) { if (chunk.type === 'tool_call') { @@ -187,8 +190,8 @@ export async function runSimulatedTest(): Promise { if (toolName === 'execute_typescript') { executeTypescriptCalled = true } - if (toolName === 'register_skill') { - registerSkillCalled = true + if (toolName === 'register_snippet') { + registerSnippetCalled = true } } else if (chunk.type === 'tool_result') { logInfo(`Tool result received for: ${chunk.toolCallId}`) @@ -199,25 +202,28 @@ export async function runSimulatedTest(): Promise { } } - // Verify skill was created - const skillIndex = await storage.loadIndex() - const skillCreated = skillIndex.some((s) => s.name === 'add_two_numbers') + // Verify snippet was created + const snippetIndex = await storage.loadIndex() + const snippetCreated = snippetIndex.some( + (s) => s.name === 'add_two_numbers', + ) - if (skillCreated) { - result.skillCreated = true - logSuccess('Skill "add_two_numbers" was created successfully') + if (snippetCreated) { + result.snippetCreated = true + logSuccess('Snippet "add_two_numbers" was created successfully') } else { - logError('Skill was not created') + logError('Snippet was not created') } result.phases.phase1 = { - success: executeTypescriptCalled && registerSkillCalled && skillCreated, + success: + executeTypescriptCalled && registerSnippetCalled && snippetCreated, details: { toolCallCount: toolCallCount1, executeTypescriptCalled, - registerSkillCalled, - skillCreated, - skillsInStorage: skillIndex.length, + registerSnippetCalled, + snippetCreated, + snippetsInStorage: snippetIndex.length, }, } @@ -238,35 +244,35 @@ export async function runSimulatedTest(): Promise { } // ========================================================================= - // Phase 2: Second run - Use the saved skill + // Phase 2: Second run - Use the saved snippet // ========================================================================= - logSection('Phase 2: Skill Reuse') - logStep(1, 'Setting up mock adapter with skill usage responses') + logSection('Phase 2: Snippet Reuse') + logStep(1, 'Setting up mock adapter with snippet usage responses') - // Mock adapter for skill selection in Phase 2 - returns the skill we created - const phase2SelectionAdapter = createSkillSelectionAdapter([ + // Mock adapter for snippet selection in Phase 2 - returns the snippet we created + const phase2SelectionAdapter = createSnippetSelectionAdapter([ 'add_two_numbers', ]) // Mock responses for Phase 2 main chat: - // 1. Call the add_two_numbers skill directly (not execute_typescript) + // 1. Call the add_two_numbers snippet directly (not execute_typescript) // 2. Provide the final answer const phase2ChatAdapter = createMockTextAdapter({ responses: [ - // Response 1: Call the skill directly + // Response 1: Call the snippet directly singleToolCall( 'add_two_numbers', { a: 10, b: 20, }, - 'call_skill_1', + 'call_snippet_1', ), // Response 2: Final answer textResponse( - 'The answer is 30. I used the add_two_numbers skill to calculate this.', + 'The answer is 30. I used the add_two_numbers snippet to calculate this.', ), ], onResponse: (index, response) => { @@ -279,30 +285,30 @@ export async function runSimulatedTest(): Promise { try { logStep( 2, - 'Running code mode with skills (second run, skill should be available)', + 'Running code mode with snippets (second run, snippet should be available)', ) const messages2: Array = [ { role: 'user', content: 'What is 10 + 20?' }, ] - // Get registry and system prompt with skills integration - // Note: We use the selection adapter for skill selection (returns ['add_two_numbers']) + // Get registry and system prompt with snippets integration + // Note: We use the selection adapter for snippet selection (returns ['add_two_numbers']) const { registry: registry2, systemPrompt: systemPrompt2, - selectedSkills, - } = await codeModeWithSkills({ + selectedSnippets, + } = await codeModeWithSnippets({ config: { driver, tools: [addNumbersTool], timeout: 30000, memoryLimit: 128, }, - adapter: phase2SelectionAdapter, // Used for skill selection (returns ['add_two_numbers']) - skills: { + adapter: phase2SelectionAdapter, // Used for snippet selection (returns ['add_two_numbers']) + snippets: { storage, - maxSkillsInContext: 5, + maxSnippetsInContext: 5, }, messages: messages2, }) @@ -312,17 +318,17 @@ export async function runSimulatedTest(): Promise { `Phase 2 tools available: ${tools2.map((t: any) => t.name).join(', ')}`, ) logInfo( - `Selected skills: ${selectedSkills.map((s) => s.name).join(', ') || 'none'}`, + `Selected snippets: ${selectedSnippets.map((s) => s.name).join(', ') || 'none'}`, ) logInfo(`System prompt length: ${systemPrompt2.length} chars`) - // Check if skill is now available as a tool - const skillToolAvailable = registry2.has('add_two_numbers') - if (skillToolAvailable) { - logSuccess('Skill "add_two_numbers" is now available as a tool') + // Check if snippet is now available as a tool + const snippetToolAvailable = registry2.has('add_two_numbers') + if (snippetToolAvailable) { + logSuccess('Snippet "add_two_numbers" is now available as a tool') } else { logWarning( - 'Skill is not available as a tool (may not have been selected)', + 'Snippet is not available as a tool (may not have been selected)', ) } @@ -338,7 +344,7 @@ export async function runSimulatedTest(): Promise { }) let toolCallCount2 = 0 - let skillCalled = false + let snippetCalled = false let executeTypescriptCalledPhase2 = false for await (const chunk of stream2 as AsyncIterable) { @@ -347,7 +353,7 @@ export async function runSimulatedTest(): Promise { const toolName = chunk.toolCall.function.name logInfo(`Tool called: ${toolName}`) if (toolName === 'add_two_numbers') { - skillCalled = true + snippetCalled = true } if (toolName === 'execute_typescript') { executeTypescriptCalledPhase2 = true @@ -359,16 +365,16 @@ export async function runSimulatedTest(): Promise { } } - result.skillUsed = skillCalled && !executeTypescriptCalledPhase2 + result.snippetUsed = snippetCalled && !executeTypescriptCalledPhase2 result.phases.phase2 = { - success: skillCalled, + success: snippetCalled, details: { toolCallCount: toolCallCount2, - skillCalled, + snippetCalled, executeTypescriptCalled: executeTypescriptCalledPhase2, - skillToolAvailable, - selectedSkillCount: selectedSkills.length, + snippetToolAvailable, + selectedSnippetCount: selectedSnippets.length, }, } @@ -398,18 +404,18 @@ export async function runSimulatedTest(): Promise { if (result.passed) { logSuccess('All tests passed!') - logInfo(`✓ Skill created: ${result.skillCreated}`) - logInfo(`✓ Skill used: ${result.skillUsed}`) + logInfo(`✓ Snippet created: ${result.snippetCreated}`) + logInfo(`✓ Snippet used: ${result.snippetUsed}`) } else { logError('Some tests failed') if (!result.phases.phase1.success) { logError( - `Phase 1 (Skill Creation): ${result.phases.phase1.error || 'Failed'}`, + `Phase 1 (Snippet Creation): ${result.phases.phase1.error || 'Failed'}`, ) } if (!result.phases.phase2.success) { logError( - `Phase 2 (Skill Reuse): ${result.phases.phase2.error || 'Failed'}`, + `Phase 2 (Snippet Reuse): ${result.phases.phase2.error || 'Failed'}`, ) } } diff --git a/packages/ai-code-mode-skills/test-cli/structured-output-test.ts b/packages/ai-code-mode-snippets/test-cli/structured-output-test.ts similarity index 100% rename from packages/ai-code-mode-skills/test-cli/structured-output-test.ts rename to packages/ai-code-mode-snippets/test-cli/structured-output-test.ts diff --git a/packages/ai-code-mode-skills/test-cli/test-utils.ts b/packages/ai-code-mode-snippets/test-cli/test-utils.ts similarity index 81% rename from packages/ai-code-mode-skills/test-cli/test-utils.ts rename to packages/ai-code-mode-snippets/test-cli/test-utils.ts index 8f94f0d464..8ba96b1dc8 100644 --- a/packages/ai-code-mode-skills/test-cli/test-utils.ts +++ b/packages/ai-code-mode-snippets/test-cli/test-utils.ts @@ -1,12 +1,12 @@ /** - * Test utilities for the skills CLI tests + * Test utilities for the snippets CLI tests */ import { toolDefinition } from '@tanstack/ai' import { z } from 'zod' -import { createMemorySkillStorage } from '../src/storage/memory-storage' +import { createMemorySnippetStorage } from '../src/storage/memory-storage' import { createAlwaysTrustedStrategy } from '../src/trust-strategies' -import type { SkillStorage } from '../src/types' +import type { SnippetStorage } from '../src/types' /** * A simple add_numbers tool for testing @@ -29,9 +29,9 @@ export const addNumbersTool = toolDefinition({ /** * Create a fresh memory storage for testing */ -export function createTestStorage(): SkillStorage { - return createMemorySkillStorage({ - initialSkills: [], +export function createTestStorage(): SnippetStorage { + return createMemorySnippetStorage({ + initialSnippets: [], trustStrategy: createAlwaysTrustedStrategy(), }) } @@ -54,8 +54,8 @@ export interface TestResult { phase1: TestPhaseResult phase2: TestPhaseResult } - skillCreated: boolean - skillUsed: boolean + snippetCreated: boolean + snippetUsed: boolean } /** @@ -117,18 +117,18 @@ export function logStep(step: number, message: string) { } /** - * Expected code for the skill that wraps add_numbers + * Expected code for the snippet that wraps add_numbers */ -export const EXPECTED_SKILL_CODE = ` +export const EXPECTED_SNIPPET_CODE = ` const { a, b } = input; const result = await external_add_numbers({ a, b }); return result; `.trim() /** - * Expected input schema for the add_two_numbers skill + * Expected input schema for the add_two_numbers snippet */ -export const EXPECTED_SKILL_INPUT_SCHEMA = { +export const EXPECTED_SNIPPET_INPUT_SCHEMA = { type: 'object', properties: { a: { type: 'number', description: 'First number to add' }, @@ -138,9 +138,9 @@ export const EXPECTED_SKILL_INPUT_SCHEMA = { } /** - * Expected output schema for the add_two_numbers skill + * Expected output schema for the add_two_numbers snippet */ -export const EXPECTED_SKILL_OUTPUT_SCHEMA = { +export const EXPECTED_SNIPPET_OUTPUT_SCHEMA = { type: 'object', properties: { result: { type: 'number', description: 'The sum of a and b' }, diff --git a/packages/ai-code-mode-skills/test-cli/tests.ts b/packages/ai-code-mode-snippets/test-cli/tests.ts similarity index 86% rename from packages/ai-code-mode-skills/test-cli/tests.ts rename to packages/ai-code-mode-snippets/test-cli/tests.ts index f794536683..484f234f91 100644 --- a/packages/ai-code-mode-skills/test-cli/tests.ts +++ b/packages/ai-code-mode-snippets/test-cli/tests.ts @@ -50,9 +50,9 @@ async function runSimulatedWrapper( } /** - * Wrapper for the skills live test + * Wrapper for the snippets live test */ -async function runSkillsLiveWrapper( +async function runSnippetsLiveWrapper( adapter: AnyTextAdapter | null, verbose: boolean, ): Promise { @@ -64,7 +64,7 @@ async function runSkillsLiveWrapper( const result = await runLiveTest({ adapter, verbose }) return { passed: result.passed, - error: result.passed ? undefined : 'Skills live test failed', + error: result.passed ? undefined : 'Snippets live test failed', } } @@ -110,23 +110,23 @@ export const TESTS: Array = [ id: 'SIM', name: 'Simulated', description: - 'Deterministic test with mock adapter (skill creation + reuse)', + 'Deterministic test with mock adapter (snippet creation + reuse)', requiresAdapter: false, run: runSimulatedWrapper, }, { id: 'REG', name: 'Registry', - description: 'Test ToolRegistry dynamic skill registration mid-stream', + description: 'Test ToolRegistry dynamic snippet registration mid-stream', requiresAdapter: false, run: runRegistryWrapper, }, { - id: 'SKL', - name: 'Skills Live', - description: 'Live test of skill creation and direct skill tool call', + id: 'SNP', + name: 'Snippets Live', + description: 'Live test of snippet creation and direct snippet tool call', requiresAdapter: true, - run: runSkillsLiveWrapper, + run: runSnippetsLiveWrapper, }, { id: 'STR', @@ -152,7 +152,7 @@ export function getTestIds(): Array { } /** - * Get all tests (no skip-by-default logic for skills tests) + * Get all tests (no skip-by-default logic for snippets tests) */ export function getDefaultTests(): Array { return TESTS diff --git a/packages/ai-code-mode-skills/tests/create-skill-management-tools.test.ts b/packages/ai-code-mode-snippets/tests/create-snippet-management-tools.test.ts similarity index 65% rename from packages/ai-code-mode-skills/tests/create-skill-management-tools.test.ts rename to packages/ai-code-mode-snippets/tests/create-snippet-management-tools.test.ts index ebab79e825..ae21f70c75 100644 --- a/packages/ai-code-mode-skills/tests/create-skill-management-tools.test.ts +++ b/packages/ai-code-mode-snippets/tests/create-snippet-management-tools.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' -import { createSkillManagementTools } from '../src/create-skill-management-tools' -import { createMemorySkillStorage } from '../src/storage/memory-storage' +import { createSnippetManagementTools } from '../src/create-snippet-management-tools' +import { createMemorySnippetStorage } from '../src/storage/memory-storage' import { createAlwaysTrustedStrategy, createDefaultTrustStrategy, @@ -9,7 +9,7 @@ import { const mockContext = () => ({ emitCustomEvent: vi.fn() }) function getTool( - tools: ReturnType, + tools: ReturnType, name: string, ) { const tool = tools.find((t) => t.name === name) @@ -30,7 +30,7 @@ function validRegisterInput( ) { return { name: 'fetch_data', - description: 'A skill', + description: 'A snippet', code: 'return input;', inputSchema: '{"type":"object","properties":{}}', outputSchema: '{"type":"object","properties":{}}', @@ -40,20 +40,20 @@ function validRegisterInput( } } -describe('createSkillManagementTools', () => { - it('exposes search_skills, get_skill, and register_skill', () => { - const storage = createMemorySkillStorage([]) - const tools = createSkillManagementTools({ storage }) +describe('createSnippetManagementTools', () => { + it('exposes search_snippets, get_snippet, and register_snippet', () => { + const storage = createMemorySnippetStorage([]) + const tools = createSnippetManagementTools({ storage }) expect(tools.map((t) => t.name).sort()).toEqual([ - 'get_skill', - 'register_skill', - 'search_skills', + 'get_snippet', + 'register_snippet', + 'search_snippets', ]) }) - describe('search_skills', () => { + describe('search_snippets', () => { it('returns lightweight matching entries', async () => { - const storage = createMemorySkillStorage([ + const storage = createMemorySnippetStorage([ { id: '1', name: 'github_stats', @@ -69,8 +69,8 @@ describe('createSkillManagementTools', () => { updatedAt: '', }, ]) - const tools = createSkillManagementTools({ storage }) - const tool = getTool(tools, 'search_skills') + const tools = createSnippetManagementTools({ storage }) + const tool = getTool(tools, 'search_snippets') const results = (await tool.execute!( { query: 'github', limit: 5 }, mockContext() as any, @@ -81,7 +81,7 @@ describe('createSkillManagementTools', () => { }) it('respects the limit parameter', async () => { - const storage = createMemorySkillStorage([ + const storage = createMemorySnippetStorage([ { id: 'a', name: 'data_one', @@ -111,8 +111,8 @@ describe('createSkillManagementTools', () => { updatedAt: '', }, ]) - const tools = createSkillManagementTools({ storage }) - const tool = getTool(tools, 'search_skills') + const tools = createSnippetManagementTools({ storage }) + const tool = getTool(tools, 'search_snippets') const results = (await tool.execute!( { query: 'data', limit: 1 }, mockContext() as any, @@ -121,11 +121,11 @@ describe('createSkillManagementTools', () => { }) }) - describe('get_skill', () => { - it('returns an error object for a missing skill', async () => { - const storage = createMemorySkillStorage([]) - const tools = createSkillManagementTools({ storage }) - const tool = getTool(tools, 'get_skill') + describe('get_snippet', () => { + it('returns an error object for a missing snippet', async () => { + const storage = createMemorySnippetStorage([]) + const tools = createSnippetManagementTools({ storage }) + const tool = getTool(tools, 'get_snippet') const result = (await tool.execute!( { name: 'missing' }, mockContext() as any, @@ -133,8 +133,8 @@ describe('createSkillManagementTools', () => { expect(result.error).toContain('not found') }) - it('returns the full skill including code when found', async () => { - const storage = createMemorySkillStorage([ + it('returns the full snippet including code when found', async () => { + const storage = createMemorySnippetStorage([ { id: '1', name: 'alpha', @@ -150,8 +150,8 @@ describe('createSkillManagementTools', () => { updatedAt: '', }, ]) - const tools = createSkillManagementTools({ storage }) - const tool = getTool(tools, 'get_skill') + const tools = createSnippetManagementTools({ storage }) + const tool = getTool(tools, 'get_snippet') const result = (await tool.execute!( { name: 'alpha' }, mockContext() as any, @@ -166,11 +166,11 @@ describe('createSkillManagementTools', () => { }) }) - describe('register_skill', () => { + describe('register_snippet', () => { it('rejects names starting with external_', async () => { - const storage = createMemorySkillStorage([]) - const tools = createSkillManagementTools({ storage }) - const tool = getTool(tools, 'register_skill') + const storage = createMemorySnippetStorage([]) + const tools = createSnippetManagementTools({ storage }) + const tool = getTool(tools, 'register_snippet') const result = (await tool.execute!( validRegisterInput({ name: 'external_evil' }), mockContext() as any, @@ -178,21 +178,21 @@ describe('createSkillManagementTools', () => { expect(result.error).toContain("cannot start with 'external_'") }) - it('rejects names starting with skill_ (redundant prefix)', async () => { - const storage = createMemorySkillStorage([]) - const tools = createSkillManagementTools({ storage }) - const tool = getTool(tools, 'register_skill') + it('rejects names starting with snippet_ (redundant prefix)', async () => { + const storage = createMemorySnippetStorage([]) + const tools = createSnippetManagementTools({ storage }) + const tool = getTool(tools, 'register_snippet') const result = (await tool.execute!( - validRegisterInput({ name: 'skill_duplicate' }), + validRegisterInput({ name: 'snippet_duplicate' }), mockContext() as any, )) as { error?: string } - expect(result.error).toContain("should not include the 'skill_' prefix") + expect(result.error).toContain("should not include the 'snippet_' prefix") }) it('rejects malformed JSON inputSchema', async () => { - const storage = createMemorySkillStorage([]) - const tools = createSkillManagementTools({ storage }) - const tool = getTool(tools, 'register_skill') + const storage = createMemorySnippetStorage([]) + const tools = createSnippetManagementTools({ storage }) + const tool = getTool(tools, 'register_snippet') const result = (await tool.execute!( validRegisterInput({ inputSchema: 'not valid json' }), mockContext() as any, @@ -201,9 +201,9 @@ describe('createSkillManagementTools', () => { }) it('rejects malformed JSON outputSchema', async () => { - const storage = createMemorySkillStorage([]) - const tools = createSkillManagementTools({ storage }) - const tool = getTool(tools, 'register_skill') + const storage = createMemorySnippetStorage([]) + const tools = createSnippetManagementTools({ storage }) + const tool = getTool(tools, 'register_snippet') const result = (await tool.execute!( validRegisterInput({ outputSchema: '{' }), mockContext() as any, @@ -212,7 +212,7 @@ describe('createSkillManagementTools', () => { }) it('rejects a duplicate name', async () => { - const storage = createMemorySkillStorage([ + const storage = createMemorySnippetStorage([ { id: '1', name: 'existing', @@ -228,8 +228,8 @@ describe('createSkillManagementTools', () => { updatedAt: '', }, ]) - const tools = createSkillManagementTools({ storage }) - const tool = getTool(tools, 'register_skill') + const tools = createSnippetManagementTools({ storage }) + const tool = getTool(tools, 'register_snippet') const result = (await tool.execute!( validRegisterInput({ name: 'existing' }), mockContext() as any, @@ -237,29 +237,29 @@ describe('createSkillManagementTools', () => { expect(result.error).toContain('already exists') }) - it('persists a valid skill with defaults', async () => { - const storage = createMemorySkillStorage([]) - const tools = createSkillManagementTools({ storage }) - const tool = getTool(tools, 'register_skill') + it('persists a valid snippet with defaults', async () => { + const storage = createMemorySnippetStorage([]) + const tools = createSnippetManagementTools({ storage }) + const tool = getTool(tools, 'register_snippet') const result = (await tool.execute!( - validRegisterInput({ name: 'valid_skill' }), + validRegisterInput({ name: 'valid_snippet' }), mockContext() as any, - )) as { success?: boolean; skillId?: string } + )) as { success?: boolean; snippetId?: string } expect(result.success).toBe(true) - expect(result.skillId).toMatch(/^[0-9a-f-]{36}$/) + expect(result.snippetId).toMatch(/^[0-9a-f-]{36}$/) - const saved = await storage.get('valid_skill') + const saved = await storage.get('valid_snippet') expect(saved).not.toBeNull() expect(saved!.stats).toEqual({ executions: 0, successRate: 0 }) }) it('applies the trust strategy to set initial trust level', async () => { - const storage = createMemorySkillStorage([]) - const tools = createSkillManagementTools({ + const storage = createMemorySnippetStorage([]) + const tools = createSnippetManagementTools({ storage, trustStrategy: createAlwaysTrustedStrategy(), }) - const tool = getTool(tools, 'register_skill') + const tool = getTool(tools, 'register_snippet') await tool.execute!( validRegisterInput({ name: 's1' }), mockContext() as any, @@ -269,14 +269,14 @@ describe('createSkillManagementTools', () => { }) it('prefers explicit trustStrategy over storage.trustStrategy', async () => { - const storage = createMemorySkillStorage({ + const storage = createMemorySnippetStorage({ trustStrategy: createAlwaysTrustedStrategy(), }) - const tools = createSkillManagementTools({ + const tools = createSnippetManagementTools({ storage, trustStrategy: createDefaultTrustStrategy(), }) - const tool = getTool(tools, 'register_skill') + const tool = getTool(tools, 'register_snippet') await tool.execute!( validRegisterInput({ name: 's1' }), mockContext() as any, @@ -286,11 +286,11 @@ describe('createSkillManagementTools', () => { }) it('falls back to storage.trustStrategy when none provided', async () => { - const storage = createMemorySkillStorage({ + const storage = createMemorySnippetStorage({ trustStrategy: createAlwaysTrustedStrategy(), }) - const tools = createSkillManagementTools({ storage }) - const tool = getTool(tools, 'register_skill') + const tools = createSnippetManagementTools({ storage }) + const tool = getTool(tools, 'register_snippet') await tool.execute!( validRegisterInput({ name: 's1' }), mockContext() as any, diff --git a/packages/ai-code-mode-snippets/tests/create-snippets-system-prompt.test.ts b/packages/ai-code-mode-snippets/tests/create-snippets-system-prompt.test.ts new file mode 100644 index 0000000000..737448d30b --- /dev/null +++ b/packages/ai-code-mode-snippets/tests/create-snippets-system-prompt.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it } from 'vitest' +import { createSnippetsSystemPrompt } from '../src/create-snippets-system-prompt' +import type { Snippet } from '../src/types' + +function makeSnippet(overrides: Partial = {}): Snippet { + return { + id: 'id', + name: 'fetch_data', + description: 'Fetches data', + code: '', + inputSchema: { + type: 'object', + properties: { query: { type: 'string' } }, + required: ['query'], + }, + outputSchema: { type: 'object', properties: {} }, + usageHints: [], + dependsOn: [], + trustLevel: 'untrusted', + stats: { executions: 0, successRate: 0 }, + createdAt: '', + updatedAt: '', + ...overrides, + } +} + +describe('createSnippetsSystemPrompt', () => { + it('returns the empty-library prompt when totalSnippetCount is 0', () => { + const prompt = createSnippetsSystemPrompt({ + selectedSnippets: [], + totalSnippetCount: 0, + }) + expect(prompt).toContain('library is currently empty') + expect(prompt).toContain('register_snippet') + }) + + it('returns the no-selected-snippets prompt when snippets exist but none selected', () => { + const prompt = createSnippetsSystemPrompt({ + selectedSnippets: [], + totalSnippetCount: 12, + }) + expect(prompt).toContain('persistent snippet library with 12 snippets') + expect(prompt).toContain('No snippets were pre-loaded') + }) + + it('uses singular wording for a single snippet in library', () => { + const prompt = createSnippetsSystemPrompt({ + selectedSnippets: [], + totalSnippetCount: 1, + }) + expect(prompt).toContain('library with 1 snippet.') + expect(prompt).not.toContain('with 1 snippets') + }) + + it('documents selected snippets as direct tools when snippetsAsTools=true', () => { + const snippet = makeSnippet({ + name: 'fetch_github', + description: 'Fetches GitHub data', + }) + const prompt = createSnippetsSystemPrompt({ + selectedSnippets: [snippet], + totalSnippetCount: 1, + snippetsAsTools: true, + }) + expect(prompt).toContain('### fetch_github') + expect(prompt).toContain('[SNIPPET]') + expect(prompt).toContain('Fetches GitHub data') + expect(prompt).not.toContain('snippet_fetch_github(') + }) + + it('documents selected snippets as sandbox bindings when snippetsAsTools=false', () => { + const snippet = makeSnippet({ name: 'fetch_github' }) + const prompt = createSnippetsSystemPrompt({ + selectedSnippets: [snippet], + totalSnippetCount: 1, + snippetsAsTools: false, + }) + expect(prompt).toContain('snippet_fetch_github') + expect(prompt).toContain('### Type Definitions') + expect(prompt).toContain('declare function snippet_fetch_github') + }) + + it('renders a trust badge reflecting the snippet trust level', () => { + const trusted = makeSnippet({ name: 'a', trustLevel: 'trusted' }) + const provisional = makeSnippet({ name: 'b', trustLevel: 'provisional' }) + const untrusted = makeSnippet({ name: 'c', trustLevel: 'untrusted' }) + + const prompt = createSnippetsSystemPrompt({ + selectedSnippets: [trusted, provisional, untrusted], + totalSnippetCount: 3, + snippetsAsTools: true, + }) + + expect(prompt).toContain('✓ trusted') + expect(prompt).toContain('◐ provisional') + expect(prompt).toContain('○ untrusted') + }) + + it('defaults to snippetsAsTools=true when not specified', () => { + const snippet = makeSnippet({ name: 'default_mode' }) + const prompt = createSnippetsSystemPrompt({ + selectedSnippets: [snippet], + totalSnippetCount: 1, + }) + expect(prompt).toContain('### default_mode') + expect(prompt).not.toContain('### Type Definitions') + }) + + it('embeds usageHints as bullet points', () => { + const snippet = makeSnippet({ + usageHints: ['When comparing X', 'When reducing Y'], + }) + const prompt = createSnippetsSystemPrompt({ + selectedSnippets: [snippet], + totalSnippetCount: 1, + }) + expect(prompt).toContain('- When comparing X') + expect(prompt).toContain('- When reducing Y') + }) +}) diff --git a/packages/ai-code-mode-skills/tests/file-storage.test.ts b/packages/ai-code-mode-snippets/tests/file-storage.test.ts similarity index 54% rename from packages/ai-code-mode-skills/tests/file-storage.test.ts rename to packages/ai-code-mode-snippets/tests/file-storage.test.ts index 0f7d176326..179916afad 100644 --- a/packages/ai-code-mode-skills/tests/file-storage.test.ts +++ b/packages/ai-code-mode-snippets/tests/file-storage.test.ts @@ -1,12 +1,13 @@ import { mkdtemp, rm } from 'node:fs/promises' +import { existsSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createFileSkillStorage } from '../src/storage/file-storage' +import { createFileSnippetStorage } from '../src/storage/file-storage' import { createAlwaysTrustedStrategy } from '../src/trust-strategies' -import type { SkillStorage } from '../src/types' +import type { SnippetStorage } from '../src/types' -function makeSkillInput(overrides: Partial> = {}) { +function makeSnippetInput(overrides: Partial> = {}) { return { id: 'id-1', name: 'fetch_data', @@ -19,16 +20,16 @@ function makeSkillInput(overrides: Partial> = {}) { trustLevel: 'untrusted' as const, stats: { executions: 0, successRate: 0 }, ...overrides, - } as Parameters[0] + } as Parameters[0] } -describe('createFileSkillStorage', () => { +describe('createFileSnippetStorage', () => { let dir: string - let storage: SkillStorage + let storage: SnippetStorage beforeEach(async () => { - dir = await mkdtemp(join(tmpdir(), 'skills-test-')) - storage = createFileSkillStorage(dir) + dir = await mkdtemp(join(tmpdir(), 'snippets-test-')) + storage = createFileSnippetStorage(dir) }) afterEach(async () => { @@ -41,12 +42,12 @@ describe('createFileSkillStorage', () => { it('creates the directory if it does not exist yet', async () => { const nested = join(dir, 'nested', 'deep') - const deepStorage = createFileSkillStorage(nested) + const deepStorage = createFileSnippetStorage(nested) await expect(deepStorage.loadIndex()).resolves.toEqual([]) }) - it('saves a skill and round-trips it via get', async () => { - const saved = await storage.save(makeSkillInput({ name: 'alpha' })) + it('saves a snippet and round-trips it via get', async () => { + const saved = await storage.save(makeSnippetInput({ name: 'alpha' })) expect(saved.createdAt).toBeTruthy() expect(saved.updatedAt).toBeTruthy() @@ -55,17 +56,17 @@ describe('createFileSkillStorage', () => { expect(fetched!.code).toBe('return input;') }) - it('persists skills across independent storage instances pointing at the same dir', async () => { - await storage.save(makeSkillInput({ name: 'persistent' })) + it('persists snippets across independent storage instances pointing at the same dir', async () => { + await storage.save(makeSnippetInput({ name: 'persistent' })) - const second = createFileSkillStorage(dir) + const second = createFileSnippetStorage(dir) const reloaded = await second.get('persistent') expect(reloaded).not.toBeNull() expect(reloaded!.name).toBe('persistent') }) it('separates code from metadata on disk', async () => { - await storage.save(makeSkillInput({ name: 'x', code: 'return 42;' })) + await storage.save(makeSnippetInput({ name: 'x', code: 'return 42;' })) const { readFile } = await import('node:fs/promises') const meta = JSON.parse( await readFile(join(dir, 'x', 'meta.json'), 'utf-8'), @@ -75,17 +76,17 @@ describe('createFileSkillStorage', () => { expect(code).toBe('return 42;') }) - it('preserves createdAt when updating an existing skill', async () => { + it('preserves createdAt when updating an existing snippet', async () => { // Deterministic clock: real timer sleeps are flaky because // Date.prototype.toISOString() has millisecond resolution and on fast // machines two saves can land in the same millisecond. vi.useFakeTimers() try { vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z')) - const first = await storage.save(makeSkillInput({ name: 'x' })) + const first = await storage.save(makeSnippetInput({ name: 'x' })) vi.setSystemTime(new Date('2026-01-01T00:00:01.000Z')) const second = await storage.save( - makeSkillInput({ name: 'x', description: 'updated' }), + makeSnippetInput({ name: 'x', description: 'updated' }), ) expect(second.createdAt).toBe(first.createdAt) expect(second.updatedAt).not.toBe(first.updatedAt) @@ -95,21 +96,21 @@ describe('createFileSkillStorage', () => { } }) - it('deletes a skill including its directory and index entry', async () => { - await storage.save(makeSkillInput({ name: 'doomed' })) + it('deletes a snippet including its directory and index entry', async () => { + await storage.save(makeSnippetInput({ name: 'doomed' })) expect(await storage.delete('doomed')).toBe(true) expect(await storage.get('doomed')).toBeNull() expect(await storage.loadIndex()).toEqual([]) }) - it('returns false when deleting a missing skill', async () => { + it('returns false when deleting a missing snippet', async () => { expect(await storage.delete('missing')).toBe(false) }) it('searches via matching and respects limit', async () => { - await storage.save(makeSkillInput({ id: '1', name: 'github_stats' })) - await storage.save(makeSkillInput({ id: '2', name: 'npm_search' })) - await storage.save(makeSkillInput({ id: '3', name: 'other_github_tool' })) + await storage.save(makeSnippetInput({ id: '1', name: 'github_stats' })) + await storage.save(makeSnippetInput({ id: '2', name: 'npm_search' })) + await storage.save(makeSnippetInput({ id: '3', name: 'other_github_tool' })) const results = await storage.search('github', { limit: 2 }) expect(results).toHaveLength(2) @@ -119,12 +120,12 @@ describe('createFileSkillStorage', () => { }) it('updateStats increments and applies the trust strategy', async () => { - const alwaysTrusted = createFileSkillStorage({ + const alwaysTrusted = createFileSnippetStorage({ directory: dir, trustStrategy: createAlwaysTrustedStrategy(), }) await alwaysTrusted.save( - makeSkillInput({ name: 'x', trustLevel: 'untrusted' }), + makeSnippetInput({ name: 'x', trustLevel: 'untrusted' }), ) await alwaysTrusted.updateStats('x', true) const after = await alwaysTrusted.get('x') @@ -132,16 +133,55 @@ describe('createFileSkillStorage', () => { expect(after!.trustLevel).toBe('trusted') }) - it('updateStats is a no-op when skill does not exist', async () => { + it('updateStats is a no-op when snippet does not exist', async () => { await expect(storage.updateStats('missing', true)).resolves.toBeUndefined() }) it('exposes the configured trust strategy', () => { const strategy = createAlwaysTrustedStrategy() - const s = createFileSkillStorage({ + const s = createFileSnippetStorage({ directory: dir, trustStrategy: strategy, }) expect(s.trustStrategy).toBe(strategy) }) + + describe('rejects path traversal in snippet names', () => { + const unsafeNames = [ + '..', + '../escape', + 'a/b', + 'foo/../bar', + '/abs', + 'win\\seg', + ] + + it('save() throws and writes nothing outside the directory', async () => { + for (const name of unsafeNames) { + await expect(storage.save(makeSnippetInput({ name }))).rejects.toThrow( + /Invalid snippet name/, + ) + } + // The `../escape` attempt must not have created a sibling of `dir`. + expect(existsSync(join(dir, '..', 'escape'))).toBe(false) + }) + + it('delete() throws for unsafe names', async () => { + await expect(storage.delete('../escape')).rejects.toThrow( + /Invalid snippet name/, + ) + }) + + it('get() returns null for unsafe names', async () => { + expect(await storage.get('../escape')).toBeNull() + }) + + it('still accepts ordinary identifier names', async () => { + const saved = await storage.save( + makeSnippetInput({ name: 'fetch_github-stats2' }), + ) + expect(saved.name).toBe('fetch_github-stats2') + expect(await storage.get('fetch_github-stats2')).not.toBeNull() + }) + }) }) diff --git a/packages/ai-code-mode-skills/tests/generate-skill-types.test.ts b/packages/ai-code-mode-snippets/tests/generate-snippet-types.test.ts similarity index 69% rename from packages/ai-code-mode-skills/tests/generate-skill-types.test.ts rename to packages/ai-code-mode-snippets/tests/generate-snippet-types.test.ts index 101f6921b5..951d4b228f 100644 --- a/packages/ai-code-mode-skills/tests/generate-skill-types.test.ts +++ b/packages/ai-code-mode-snippets/tests/generate-snippet-types.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it } from 'vitest' -import { generateSkillTypes } from '../src/generate-skill-types' -import type { Skill } from '../src/types' +import { generateSnippetTypes } from '../src/generate-snippet-types' +import type { Snippet } from '../src/types' -function makeSkill(overrides: Partial = {}): Skill { +function makeSnippet(overrides: Partial = {}): Snippet { return { id: 'id', - name: 'skill_name', - description: 'A skill', + name: 'snippet_name', + description: 'A snippet', code: '', inputSchema: { type: 'object', properties: {} }, outputSchema: { type: 'object', properties: {} }, @@ -20,34 +20,34 @@ function makeSkill(overrides: Partial = {}): Skill { } } -describe('generateSkillTypes', () => { - it('returns empty string for empty skills array', () => { - expect(generateSkillTypes([])).toBe('') +describe('generateSnippetTypes', () => { + it('returns empty string for empty snippets array', () => { + expect(generateSnippetTypes([])).toBe('') }) it('generates declare function with snake_case name preserved', () => { - const skill = makeSkill({ + const snippet = makeSnippet({ name: 'fetch_stats', inputSchema: { type: 'string' }, outputSchema: { type: 'number' }, }) - const result = generateSkillTypes([skill]) - expect(result).toContain('declare function skill_fetch_stats') + const result = generateSnippetTypes([snippet]) + expect(result).toContain('declare function snippet_fetch_stats') expect(result).toContain('Promise') }) it('inlines primitive input/output types', () => { - const skill = makeSkill({ + const snippet = makeSnippet({ inputSchema: { type: 'string' }, outputSchema: { type: 'boolean' }, }) - const result = generateSkillTypes([skill]) + const result = generateSnippetTypes([snippet]) expect(result).toContain('input: string') expect(result).toContain('Promise') }) it('creates interface for object input with properties', () => { - const skill = makeSkill({ + const snippet = makeSnippet({ name: 'fetch_data', inputSchema: { type: 'object', @@ -59,15 +59,15 @@ describe('generateSkillTypes', () => { }, outputSchema: { type: 'string' }, }) - const result = generateSkillTypes([skill]) - expect(result).toContain('interface SkillFetchDataInput') + const result = generateSnippetTypes([snippet]) + expect(result).toContain('interface SnippetFetchDataInput') expect(result).toContain('owner: string') expect(result).toContain('repo: string') - expect(result).toContain('input: SkillFetchDataInput') + expect(result).toContain('input: SnippetFetchDataInput') }) it('marks non-required properties as optional', () => { - const skill = makeSkill({ + const snippet = makeSnippet({ inputSchema: { type: 'object', properties: { @@ -78,13 +78,13 @@ describe('generateSkillTypes', () => { }, outputSchema: { type: 'string' }, }) - const result = generateSkillTypes([skill]) + const result = generateSnippetTypes([snippet]) expect(result).toContain('required_field: string') expect(result).toContain('optional_field?: number') }) it('quotes property names that are not valid identifiers', () => { - const skill = makeSkill({ + const snippet = makeSnippet({ inputSchema: { type: 'object', properties: { @@ -95,86 +95,86 @@ describe('generateSkillTypes', () => { }, outputSchema: { type: 'string' }, }) - const result = generateSkillTypes([skill]) + const result = generateSnippetTypes([snippet]) expect(result).toContain('"with-dash"') expect(result).toContain('"123numeric"') }) it('converts array schemas to Array', () => { - const skill = makeSkill({ + const snippet = makeSnippet({ inputSchema: { type: 'array', items: { type: 'string' } }, outputSchema: { type: 'array' }, }) - const result = generateSkillTypes([skill]) + const result = generateSnippetTypes([snippet]) expect(result).toContain('input: Array') expect(result).toContain('Promise>') }) it('converts enum schemas to a union of string literals', () => { - const skill = makeSkill({ + const snippet = makeSnippet({ inputSchema: { enum: ['red', 'green', 'blue'] }, outputSchema: { type: 'string' }, }) - const result = generateSkillTypes([skill]) + const result = generateSnippetTypes([snippet]) expect(result).toContain('"red" | "green" | "blue"') }) it('converts anyOf / oneOf to a union type', () => { - const skill = makeSkill({ + const snippet = makeSnippet({ inputSchema: { anyOf: [{ type: 'string' }, { type: 'number' }], }, outputSchema: { type: 'string' }, }) - const result = generateSkillTypes([skill]) + const result = generateSnippetTypes([snippet]) expect(result).toContain('string | number') }) it('handles type arrays like ["string", "null"]', () => { - const skill = makeSkill({ + const snippet = makeSnippet({ inputSchema: { type: ['string', 'null'] }, outputSchema: { type: 'string' }, }) - const result = generateSkillTypes([skill]) + const result = generateSnippetTypes([snippet]) expect(result).toContain('string | null') }) it('embeds usageHints as @hint JSDoc tags', () => { - const skill = makeSkill({ + const snippet = makeSnippet({ usageHints: ['Use when searching', 'Also good for filtering'], inputSchema: { type: 'string' }, outputSchema: { type: 'string' }, }) - const result = generateSkillTypes([skill]) + const result = generateSnippetTypes([snippet]) expect(result).toContain('@hint Use when searching') expect(result).toContain('@hint Also good for filtering') }) it('falls back to unknown for schemas it cannot represent', () => { - const skill = makeSkill({ + const snippet = makeSnippet({ inputSchema: { mystery: true } as Record, outputSchema: { type: 'string' }, }) - const result = generateSkillTypes([skill]) + const result = generateSnippetTypes([snippet]) expect(result).toContain('input: unknown') }) - it('handles multiple skills in order', () => { - const skills = [ - makeSkill({ + it('handles multiple snippets in order', () => { + const snippets = [ + makeSnippet({ name: 'first', inputSchema: { type: 'string' }, outputSchema: { type: 'string' }, }), - makeSkill({ + makeSnippet({ name: 'second', inputSchema: { type: 'number' }, outputSchema: { type: 'number' }, }), ] - const result = generateSkillTypes(skills) - const firstIdx = result.indexOf('skill_first') - const secondIdx = result.indexOf('skill_second') + const result = generateSnippetTypes(snippets) + const firstIdx = result.indexOf('snippet_first') + const secondIdx = result.indexOf('snippet_second') expect(firstIdx).toBeGreaterThan(-1) expect(secondIdx).toBeGreaterThan(firstIdx) }) diff --git a/packages/ai-code-mode-skills/tests/memory-storage.test.ts b/packages/ai-code-mode-snippets/tests/memory-storage.test.ts similarity index 65% rename from packages/ai-code-mode-skills/tests/memory-storage.test.ts rename to packages/ai-code-mode-snippets/tests/memory-storage.test.ts index da227a1344..ba572bac48 100644 --- a/packages/ai-code-mode-skills/tests/memory-storage.test.ts +++ b/packages/ai-code-mode-snippets/tests/memory-storage.test.ts @@ -1,14 +1,14 @@ import { describe, expect, it } from 'vitest' -import { createMemorySkillStorage } from '../src/storage/memory-storage' +import { createMemorySnippetStorage } from '../src/storage/memory-storage' import { createAlwaysTrustedStrategy, createRelaxedTrustStrategy, } from '../src/trust-strategies' -import type { Skill } from '../src/types' +import type { Snippet } from '../src/types' -function makeSkill(overrides: Partial = {}): Skill { +function makeSnippet(overrides: Partial = {}): Snippet { return { - id: overrides.id ?? 'skill-1', + id: overrides.id ?? 'snippet-1', name: overrides.name ?? 'fetch_data', description: overrides.description ?? 'Fetches data from an API', code: overrides.code ?? 'return { ok: true };', @@ -23,63 +23,63 @@ function makeSkill(overrides: Partial = {}): Skill { } } -describe('createMemorySkillStorage', () => { +describe('createMemorySnippetStorage', () => { describe('initialization', () => { it('accepts an empty array', async () => { - const storage = createMemorySkillStorage([]) + const storage = createMemorySnippetStorage([]) expect(await storage.loadAll()).toEqual([]) }) - it('accepts an array of initial skills', async () => { - const skill = makeSkill() - const storage = createMemorySkillStorage([skill]) + it('accepts an array of initial snippets', async () => { + const snippet = makeSnippet() + const storage = createMemorySnippetStorage([snippet]) expect(await storage.loadAll()).toHaveLength(1) }) - it('accepts an options object with initialSkills', async () => { - const skill = makeSkill() - const storage = createMemorySkillStorage({ initialSkills: [skill] }) + it('accepts an options object with initialSnippets', async () => { + const snippet = makeSnippet() + const storage = createMemorySnippetStorage({ initialSnippets: [snippet] }) expect(await storage.loadAll()).toHaveLength(1) }) it('exposes configured trust strategy', () => { const strategy = createAlwaysTrustedStrategy() - const storage = createMemorySkillStorage({ trustStrategy: strategy }) + const storage = createMemorySnippetStorage({ trustStrategy: strategy }) expect(storage.trustStrategy).toBe(strategy) }) }) describe('loadIndex', () => { it('returns lightweight entries without code', async () => { - const skill = makeSkill({ code: 'return secret_value;' }) - const storage = createMemorySkillStorage([skill]) + const snippet = makeSnippet({ code: 'return secret_value;' }) + const storage = createMemorySnippetStorage([snippet]) const index = await storage.loadIndex() expect(index).toHaveLength(1) expect(index[0]).not.toHaveProperty('code') - expect(index[0]).toHaveProperty('name', skill.name) - expect(index[0]).toHaveProperty('trustLevel', skill.trustLevel) + expect(index[0]).toHaveProperty('name', snippet.name) + expect(index[0]).toHaveProperty('trustLevel', snippet.trustLevel) }) }) describe('get', () => { - it('returns null for a missing skill', async () => { - const storage = createMemorySkillStorage([]) + it('returns null for a missing snippet', async () => { + const storage = createMemorySnippetStorage([]) expect(await storage.get('nonexistent')).toBeNull() }) - it('returns the skill when it exists', async () => { - const skill = makeSkill({ name: 'alpha' }) - const storage = createMemorySkillStorage([skill]) - expect(await storage.get('alpha')).toEqual(skill) + it('returns the snippet when it exists', async () => { + const snippet = makeSnippet({ name: 'alpha' }) + const storage = createMemorySnippetStorage([snippet]) + expect(await storage.get('alpha')).toEqual(snippet) }) }) describe('save', () => { - it('creates a new skill with timestamps', async () => { - const storage = createMemorySkillStorage([]) + it('creates a new snippet with timestamps', async () => { + const storage = createMemorySnippetStorage([]) const saved = await storage.save({ id: 'x', - name: 'new_skill', + name: 'new_snippet', description: 'd', code: 'c', inputSchema: {}, @@ -93,12 +93,12 @@ describe('createMemorySkillStorage', () => { expect(saved.updatedAt).toBeTruthy() }) - it('preserves createdAt when updating an existing skill', async () => { - const existing = makeSkill({ + it('preserves createdAt when updating an existing snippet', async () => { + const existing = makeSnippet({ name: 'x', createdAt: '2020-01-01T00:00:00.000Z', }) - const storage = createMemorySkillStorage([existing]) + const storage = createMemorySnippetStorage([existing]) const updated = await storage.save({ id: existing.id, @@ -120,34 +120,34 @@ describe('createMemorySkillStorage', () => { }) describe('delete', () => { - it('returns false when skill does not exist', async () => { - const storage = createMemorySkillStorage([]) + it('returns false when snippet does not exist', async () => { + const storage = createMemorySnippetStorage([]) expect(await storage.delete('nothing')).toBe(false) }) - it('returns true and removes the skill when it exists', async () => { - const storage = createMemorySkillStorage([makeSkill({ name: 'x' })]) + it('returns true and removes the snippet when it exists', async () => { + const storage = createMemorySnippetStorage([makeSnippet({ name: 'x' })]) expect(await storage.delete('x')).toBe(true) expect(await storage.get('x')).toBeNull() }) }) describe('search', () => { - it('returns empty array when no skills match', async () => { - const storage = createMemorySkillStorage([makeSkill()]) + it('returns empty array when no snippets match', async () => { + const storage = createMemorySnippetStorage([makeSnippet()]) const results = await storage.search('completely unrelated query') expect(results).toEqual([]) }) it('matches on name, description, and usageHints', async () => { - const storage = createMemorySkillStorage([ - makeSkill({ + const storage = createMemorySnippetStorage([ + makeSnippet({ name: 'github_stats', description: 'Fetches GitHub repository statistics', usageHints: ['Use for repo analysis'], }), - makeSkill({ - id: 'skill-2', + makeSnippet({ + id: 'snippet-2', name: 'npm_search', description: 'Search the npm registry', usageHints: ['Use for packages'], @@ -160,13 +160,13 @@ describe('createMemorySkillStorage', () => { }) it('boosts exact name matches over description-only matches', async () => { - const storage = createMemorySkillStorage([ - makeSkill({ + const storage = createMemorySnippetStorage([ + makeSnippet({ id: 'a', name: 'widget', description: 'Just a description', }), - makeSkill({ + makeSnippet({ id: 'b', name: 'processor', description: 'Processes widget data', @@ -178,10 +178,10 @@ describe('createMemorySkillStorage', () => { }) it('respects the limit option', async () => { - const storage = createMemorySkillStorage([ - makeSkill({ id: '1', name: 'data_one' }), - makeSkill({ id: '2', name: 'data_two' }), - makeSkill({ id: '3', name: 'data_three' }), + const storage = createMemorySnippetStorage([ + makeSnippet({ id: '1', name: 'data_one' }), + makeSnippet({ id: '2', name: 'data_two' }), + makeSnippet({ id: '3', name: 'data_three' }), ]) const results = await storage.search('data', { limit: 2 }) expect(results).toHaveLength(2) @@ -189,16 +189,16 @@ describe('createMemorySkillStorage', () => { }) describe('updateStats', () => { - it('is a no-op when the skill does not exist', async () => { - const storage = createMemorySkillStorage([]) + it('is a no-op when the snippet does not exist', async () => { + const storage = createMemorySnippetStorage([]) await expect( storage.updateStats('nothing', true), ).resolves.toBeUndefined() }) it('increments execution count and recalculates success rate', async () => { - const storage = createMemorySkillStorage([ - makeSkill({ name: 'x', stats: { executions: 0, successRate: 0 } }), + const storage = createMemorySnippetStorage([ + makeSnippet({ name: 'x', stats: { executions: 0, successRate: 0 } }), ]) await storage.updateStats('x', true) const after = await storage.get('x') @@ -207,8 +207,8 @@ describe('createMemorySkillStorage', () => { }) it('computes a running success rate across failures and successes', async () => { - const storage = createMemorySkillStorage([ - makeSkill({ name: 'x', stats: { executions: 0, successRate: 0 } }), + const storage = createMemorySnippetStorage([ + makeSnippet({ name: 'x', stats: { executions: 0, successRate: 0 } }), ]) await storage.updateStats('x', true) await storage.updateStats('x', false) @@ -218,9 +218,9 @@ describe('createMemorySkillStorage', () => { }) it('promotes trust level when stats cross the strategy threshold', async () => { - const storage = createMemorySkillStorage({ - initialSkills: [ - makeSkill({ + const storage = createMemorySnippetStorage({ + initialSnippets: [ + makeSnippet({ name: 'x', trustLevel: 'untrusted', stats: { executions: 0, successRate: 0 }, @@ -236,8 +236,8 @@ describe('createMemorySkillStorage', () => { }) it('updates the updatedAt timestamp', async () => { - const storage = createMemorySkillStorage([ - makeSkill({ + const storage = createMemorySnippetStorage([ + makeSnippet({ name: 'x', updatedAt: '2020-01-01T00:00:00.000Z', }), diff --git a/packages/ai-code-mode-skills/tests/root-export-worker-safe.test.ts b/packages/ai-code-mode-snippets/tests/root-export-worker-safe.test.ts similarity index 96% rename from packages/ai-code-mode-skills/tests/root-export-worker-safe.test.ts rename to packages/ai-code-mode-snippets/tests/root-export-worker-safe.test.ts index f04bd8bacb..d409c947c1 100644 --- a/packages/ai-code-mode-skills/tests/root-export-worker-safe.test.ts +++ b/packages/ai-code-mode-snippets/tests/root-export-worker-safe.test.ts @@ -107,9 +107,9 @@ describe('root export worker/browser safety (#486)', () => { it('re-exports the browser-safe memory storage but not the Node-only file storage from root', () => { // The public contract issue #486 is about: the root entry must expose the // in-memory storage and must NOT expose the Node-only file storage. - expect(typeof rootEntry.createMemorySkillStorage).toBe('function') + expect(typeof rootEntry.createMemorySnippetStorage).toBe('function') expect( - (rootEntry as Record).createFileSkillStorage, + (rootEntry as Record).createFileSnippetStorage, ).toBeUndefined() }) }) diff --git a/packages/ai-code-mode-skills/tests/select-relevant-skills.test.ts b/packages/ai-code-mode-snippets/tests/select-relevant-snippets.test.ts similarity index 53% rename from packages/ai-code-mode-skills/tests/select-relevant-skills.test.ts rename to packages/ai-code-mode-snippets/tests/select-relevant-snippets.test.ts index a8bb3492cc..7cb568147e 100644 --- a/packages/ai-code-mode-skills/tests/select-relevant-skills.test.ts +++ b/packages/ai-code-mode-snippets/tests/select-relevant-snippets.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from 'vitest' -import { selectRelevantSkills } from '../src/select-relevant-skills' -import { createMemorySkillStorage } from '../src/storage/memory-storage' +import { selectRelevantSnippets } from '../src/select-relevant-snippets' +import { createMemorySnippetStorage } from '../src/storage/memory-storage' import type { AnyTextAdapter, ModelMessage } from '@tanstack/ai' -import type { Skill } from '../src/types' +import type { Snippet } from '../src/types' const chatMock = vi.hoisted(() => vi.fn()) vi.mock('@tanstack/ai', async (importOriginal) => { @@ -10,7 +10,7 @@ vi.mock('@tanstack/ai', async (importOriginal) => { return { ...actual, chat: chatMock } }) -function makeSkill(overrides: Partial = {}): Skill { +function makeSnippet(overrides: Partial = {}): Snippet { return { id: 'id', name: 'fetch_data', @@ -40,14 +40,14 @@ const userMessage: ModelMessage = { content: 'please use github tool', } -describe('selectRelevantSkills', () => { - it('returns empty array when the skill index is empty', async () => { - const storage = createMemorySkillStorage([]) - const result = await selectRelevantSkills({ +describe('selectRelevantSnippets', () => { + it('returns empty array when the snippet index is empty', async () => { + const storage = createMemorySnippetStorage([]) + const result = await selectRelevantSnippets({ adapter: dummyAdapter, messages: [userMessage], - skillIndex: [], - maxSkills: 5, + snippetIndex: [], + maxSnippets: 5, storage, }) expect(result).toEqual([]) @@ -55,28 +55,28 @@ describe('selectRelevantSkills', () => { }) it('returns empty array when there are no messages', async () => { - const storage = createMemorySkillStorage([makeSkill({ name: 'x' })]) - const result = await selectRelevantSkills({ + const storage = createMemorySnippetStorage([makeSnippet({ name: 'x' })]) + const result = await selectRelevantSnippets({ adapter: dummyAdapter, messages: [], - skillIndex: await storage.loadIndex(), - maxSkills: 5, + snippetIndex: await storage.loadIndex(), + maxSnippets: 5, storage, }) expect(result).toEqual([]) expect(chatMock).not.toHaveBeenCalled() }) - it('returns skills whose names were selected by the model', async () => { - const skill = makeSkill({ name: 'github_stats' }) - const storage = createMemorySkillStorage([skill]) + it('returns snippets whose names were selected by the model', async () => { + const snippet = makeSnippet({ name: 'github_stats' }) + const storage = createMemorySnippetStorage([snippet]) chatMock.mockReturnValueOnce(streamChunks('["github_stats"]')) - const result = await selectRelevantSkills({ + const result = await selectRelevantSnippets({ adapter: dummyAdapter, messages: [userMessage], - skillIndex: await storage.loadIndex(), - maxSkills: 5, + snippetIndex: await storage.loadIndex(), + maxSnippets: 5, storage, }) expect(result).toHaveLength(1) @@ -84,80 +84,80 @@ describe('selectRelevantSkills', () => { }) it('strips markdown code fences around the JSON response', async () => { - const skill = makeSkill({ name: 'github_stats' }) - const storage = createMemorySkillStorage([skill]) + const snippet = makeSnippet({ name: 'github_stats' }) + const storage = createMemorySnippetStorage([snippet]) chatMock.mockReturnValueOnce(streamChunks('```json\n["github_stats"]\n```')) - const result = await selectRelevantSkills({ + const result = await selectRelevantSnippets({ adapter: dummyAdapter, messages: [userMessage], - skillIndex: await storage.loadIndex(), - maxSkills: 5, + snippetIndex: await storage.loadIndex(), + maxSnippets: 5, storage, }) expect(result).toHaveLength(1) }) it('returns an empty array when the model response is not an array', async () => { - const skill = makeSkill({ name: 'github_stats' }) - const storage = createMemorySkillStorage([skill]) + const snippet = makeSnippet({ name: 'github_stats' }) + const storage = createMemorySnippetStorage([snippet]) chatMock.mockReturnValueOnce(streamChunks('{"not": "an array"}')) - const result = await selectRelevantSkills({ + const result = await selectRelevantSnippets({ adapter: dummyAdapter, messages: [userMessage], - skillIndex: await storage.loadIndex(), - maxSkills: 5, + snippetIndex: await storage.loadIndex(), + maxSnippets: 5, storage, }) expect(result).toEqual([]) }) it('returns empty array when JSON parsing fails (safe fallback)', async () => { - const skill = makeSkill({ name: 'github_stats' }) - const storage = createMemorySkillStorage([skill]) + const snippet = makeSnippet({ name: 'github_stats' }) + const storage = createMemorySnippetStorage([snippet]) chatMock.mockReturnValueOnce(streamChunks('not json at all')) - const result = await selectRelevantSkills({ + const result = await selectRelevantSnippets({ adapter: dummyAdapter, messages: [userMessage], - skillIndex: await storage.loadIndex(), - maxSkills: 5, + snippetIndex: await storage.loadIndex(), + maxSnippets: 5, storage, }) expect(result).toEqual([]) }) - it('truncates model selections to maxSkills', async () => { - const storage = createMemorySkillStorage([ - makeSkill({ id: '1', name: 'a' }), - makeSkill({ id: '2', name: 'b' }), - makeSkill({ id: '3', name: 'c' }), + it('truncates model selections to maxSnippets', async () => { + const storage = createMemorySnippetStorage([ + makeSnippet({ id: '1', name: 'a' }), + makeSnippet({ id: '2', name: 'b' }), + makeSnippet({ id: '3', name: 'c' }), ]) chatMock.mockReturnValueOnce(streamChunks('["a","b","c"]')) - const result = await selectRelevantSkills({ + const result = await selectRelevantSnippets({ adapter: dummyAdapter, messages: [userMessage], - skillIndex: await storage.loadIndex(), - maxSkills: 2, + snippetIndex: await storage.loadIndex(), + maxSnippets: 2, storage, }) expect(result).toHaveLength(2) }) - it('filters out skill names that no longer resolve in storage', async () => { - const skill = makeSkill({ name: 'still_exists' }) - const storage = createMemorySkillStorage([skill]) + it('filters out snippet names that no longer resolve in storage', async () => { + const snippet = makeSnippet({ name: 'still_exists' }) + const storage = createMemorySnippetStorage([snippet]) chatMock.mockReturnValueOnce( - streamChunks('["still_exists","deleted_skill"]'), + streamChunks('["still_exists","deleted_snippet"]'), ) - const result = await selectRelevantSkills({ + const result = await selectRelevantSnippets({ adapter: dummyAdapter, messages: [userMessage], - skillIndex: await storage.loadIndex(), - maxSkills: 5, + snippetIndex: await storage.loadIndex(), + maxSnippets: 5, storage, }) expect(result).toHaveLength(1) @@ -165,15 +165,15 @@ describe('selectRelevantSkills', () => { }) it('returns empty array when the chat stream throws', async () => { - const storage = createMemorySkillStorage([makeSkill({ name: 'x' })]) + const storage = createMemorySnippetStorage([makeSnippet({ name: 'x' })]) chatMock.mockImplementationOnce(() => { throw new Error('network down') }) - const result = await selectRelevantSkills({ + const result = await selectRelevantSnippets({ adapter: dummyAdapter, messages: [userMessage], - skillIndex: await storage.loadIndex(), - maxSkills: 5, + snippetIndex: await storage.loadIndex(), + maxSnippets: 5, storage, }) expect(result).toEqual([]) diff --git a/packages/ai-code-mode-skills/tests/skills-to-bindings.test.ts b/packages/ai-code-mode-snippets/tests/snippets-to-bindings.test.ts similarity index 52% rename from packages/ai-code-mode-skills/tests/skills-to-bindings.test.ts rename to packages/ai-code-mode-snippets/tests/snippets-to-bindings.test.ts index 50f4cb410e..2c5f94fb88 100644 --- a/packages/ai-code-mode-skills/tests/skills-to-bindings.test.ts +++ b/packages/ai-code-mode-snippets/tests/snippets-to-bindings.test.ts @@ -1,16 +1,16 @@ import { describe, expect, it, vi } from 'vitest' import { - skillsToBindings, - skillsToSimpleBindings, -} from '../src/skills-to-bindings' -import { createMemorySkillStorage } from '../src/storage/memory-storage' -import type { Skill } from '../src/types' + snippetsToBindings, + snippetsToSimpleBindings, +} from '../src/snippets-to-bindings' +import { createMemorySnippetStorage } from '../src/storage/memory-storage' +import type { Snippet } from '../src/types' -function makeSkill(overrides: Partial = {}): Skill { +function makeSnippet(overrides: Partial = {}): Snippet { return { id: 'id', name: 'sample', - description: 'Sample skill', + description: 'Sample snippet', code: 'return input.value * 2;', inputSchema: { type: 'object', properties: {} }, outputSchema: { type: 'object', properties: {} }, @@ -24,57 +24,57 @@ function makeSkill(overrides: Partial = {}): Skill { } } -describe('skillsToBindings', () => { - it('prefixes binding names with skill_', () => { - const storage = createMemorySkillStorage([]) - const bindings = skillsToBindings({ - skills: [makeSkill({ name: 'alpha' })], +describe('snippetsToBindings', () => { + it('prefixes binding names with snippet_', () => { + const storage = createMemorySnippetStorage([]) + const bindings = snippetsToBindings({ + snippets: [makeSnippet({ name: 'alpha' })], executeInSandbox: async () => undefined, storage, }) - expect(Object.keys(bindings)).toEqual(['skill_alpha']) + expect(Object.keys(bindings)).toEqual(['snippet_alpha']) }) it('serializes input via JSON.stringify into the wrapped code', async () => { - const storage = createMemorySkillStorage([]) + const storage = createMemorySnippetStorage([]) const executeInSandbox = vi.fn(async () => 'ok') - const bindings = skillsToBindings({ - skills: [makeSkill({ name: 'x', code: 'return input;' })], + const bindings = snippetsToBindings({ + snippets: [makeSnippet({ name: 'x', code: 'return input;' })], executeInSandbox, storage, }) - await bindings['skill_x']!.execute({ value: 42 }) + await bindings['snippet_x']!.execute({ value: 42 }) const call = executeInSandbox.mock.calls[0] as unknown as [string, unknown] expect(call[0]).toContain('const input = {"value":42}') expect(call[0]).toContain('return input;') expect(call[1]).toEqual({ value: 42 }) }) - it('emits skill_call then skill_result events on success', async () => { - const storage = createMemorySkillStorage([]) + it('emits snippet_call then snippet_result events on success', async () => { + const storage = createMemorySnippetStorage([]) const emitCustomEvent = vi.fn() - const bindings = skillsToBindings({ - skills: [makeSkill({ name: 'x' })], + const bindings = snippetsToBindings({ + snippets: [makeSnippet({ name: 'x' })], executeInSandbox: async () => 42, storage, context: { emitCustomEvent } as any, }) - await bindings['skill_x']!.execute({}) + await bindings['snippet_x']!.execute({}) const eventNames = emitCustomEvent.mock.calls.map(([name]) => name) expect(eventNames).toEqual([ - 'code_mode:skill_call', - 'code_mode:skill_result', + 'code_mode:snippet_call', + 'code_mode:snippet_result', ]) }) - it('emits skill_error when sandbox execution throws, and re-throws', async () => { - const storage = createMemorySkillStorage([]) + it('emits snippet_error when sandbox execution throws, and re-throws', async () => { + const storage = createMemorySnippetStorage([]) const emitCustomEvent = vi.fn() - const bindings = skillsToBindings({ - skills: [makeSkill({ name: 'x' })], + const bindings = snippetsToBindings({ + snippets: [makeSnippet({ name: 'x' })], executeInSandbox: async () => { throw new Error('boom') }, @@ -82,67 +82,67 @@ describe('skillsToBindings', () => { context: { emitCustomEvent } as any, }) - await expect(bindings['skill_x']!.execute({})).rejects.toThrow('boom') + await expect(bindings['snippet_x']!.execute({})).rejects.toThrow('boom') const eventNames = emitCustomEvent.mock.calls.map(([name]) => name) - expect(eventNames).toContain('code_mode:skill_error') + expect(eventNames).toContain('code_mode:snippet_error') }) it('updates storage stats with success=true on success', async () => { - const storage = createMemorySkillStorage([ - makeSkill({ name: 'x', stats: { executions: 0, successRate: 0 } }), + const storage = createMemorySnippetStorage([ + makeSnippet({ name: 'x', stats: { executions: 0, successRate: 0 } }), ]) const updateStats = vi.spyOn(storage, 'updateStats') - const bindings = skillsToBindings({ - skills: [makeSkill({ name: 'x' })], + const bindings = snippetsToBindings({ + snippets: [makeSnippet({ name: 'x' })], executeInSandbox: async () => 1, storage, }) - await bindings['skill_x']!.execute({}) + await bindings['snippet_x']!.execute({}) expect(updateStats).toHaveBeenCalledWith('x', true) }) it('updates storage stats with success=false on failure', async () => { - const storage = createMemorySkillStorage([makeSkill({ name: 'x' })]) + const storage = createMemorySnippetStorage([makeSnippet({ name: 'x' })]) const updateStats = vi.spyOn(storage, 'updateStats') - const bindings = skillsToBindings({ - skills: [makeSkill({ name: 'x' })], + const bindings = snippetsToBindings({ + snippets: [makeSnippet({ name: 'x' })], executeInSandbox: async () => { throw new Error('fail') }, storage, }) - await expect(bindings['skill_x']!.execute({})).rejects.toThrow() + await expect(bindings['snippet_x']!.execute({})).rejects.toThrow() expect(updateStats).toHaveBeenCalledWith('x', false) }) it('does not reject if storage.updateStats fails', async () => { - const storage = createMemorySkillStorage([makeSkill({ name: 'x' })]) + const storage = createMemorySnippetStorage([makeSnippet({ name: 'x' })]) storage.updateStats = async () => { throw new Error('stats broke') } - const bindings = skillsToBindings({ - skills: [makeSkill({ name: 'x' })], + const bindings = snippetsToBindings({ + snippets: [makeSnippet({ name: 'x' })], executeInSandbox: async () => 'ok', storage, }) - await expect(bindings['skill_x']!.execute({})).resolves.toBe('ok') + await expect(bindings['snippet_x']!.execute({})).resolves.toBe('ok') }) it('serializes string inputs as JSON strings (prevents code injection via input)', async () => { - const storage = createMemorySkillStorage([]) + const storage = createMemorySnippetStorage([]) const executeInSandbox = vi.fn(async () => null) - const bindings = skillsToBindings({ - skills: [makeSkill({ name: 'x', code: 'return input;' })], + const bindings = snippetsToBindings({ + snippets: [makeSnippet({ name: 'x', code: 'return input;' })], executeInSandbox, storage, }) // Adversarial payload: attempts to escape the wrapping const-declaration const malicious = `"); throw new Error("escaped"); ("` - await bindings['skill_x']!.execute(malicious) + await bindings['snippet_x']!.execute(malicious) const wrappedCode = ( executeInSandbox.mock.calls[0] as unknown as [string, unknown] @@ -156,45 +156,45 @@ describe('skillsToBindings', () => { }) it('forwards the configured input through to executeInSandbox unchanged', async () => { - const storage = createMemorySkillStorage([]) + const storage = createMemorySnippetStorage([]) const executeInSandbox = vi.fn(async () => 'ok') - const bindings = skillsToBindings({ - skills: [makeSkill({ name: 'x' })], + const bindings = snippetsToBindings({ + snippets: [makeSnippet({ name: 'x' })], executeInSandbox, storage, }) const input = { complex: { nested: [1, 2] } } - await bindings['skill_x']!.execute(input) + await bindings['snippet_x']!.execute(input) expect( (executeInSandbox.mock.calls[0] as unknown as [string, unknown])[1], ).toBe(input) }) }) -describe('skillsToSimpleBindings', () => { - it('prefixes names with skill_', () => { - const bindings = skillsToSimpleBindings([makeSkill({ name: 'alpha' })]) - expect(Object.keys(bindings)).toEqual(['skill_alpha']) +describe('snippetsToSimpleBindings', () => { + it('prefixes names with snippet_', () => { + const bindings = snippetsToSimpleBindings([makeSnippet({ name: 'alpha' })]) + expect(Object.keys(bindings)).toEqual(['snippet_alpha']) }) it('exposes metadata without executing anything', () => { - const skill = makeSkill({ + const snippet = makeSnippet({ name: 'meta', description: 'desc', inputSchema: { type: 'string' }, outputSchema: { type: 'number' }, }) - const bindings = skillsToSimpleBindings([skill]) - expect(bindings['skill_meta']!.name).toBe('skill_meta') - expect(bindings['skill_meta']!.description).toBe('desc') - expect(bindings['skill_meta']!.inputSchema).toEqual({ type: 'string' }) - expect(bindings['skill_meta']!.outputSchema).toEqual({ type: 'number' }) + const bindings = snippetsToSimpleBindings([snippet]) + expect(bindings['snippet_meta']!.name).toBe('snippet_meta') + expect(bindings['snippet_meta']!.description).toBe('desc') + expect(bindings['snippet_meta']!.inputSchema).toEqual({ type: 'string' }) + expect(bindings['snippet_meta']!.outputSchema).toEqual({ type: 'number' }) }) it('execute() throws because execution is not available in this mode', async () => { - const bindings = skillsToSimpleBindings([makeSkill({ name: 'x' })]) - await expect(bindings['skill_x']!.execute({})).rejects.toThrow( + const bindings = snippetsToSimpleBindings([makeSnippet({ name: 'x' })]) + await expect(bindings['snippet_x']!.execute({})).rejects.toThrow( /not available for execution/, ) }) diff --git a/packages/ai-code-mode-skills/tests/skills-to-tools.test.ts b/packages/ai-code-mode-snippets/tests/snippets-to-tools.test.ts similarity index 69% rename from packages/ai-code-mode-skills/tests/skills-to-tools.test.ts rename to packages/ai-code-mode-snippets/tests/snippets-to-tools.test.ts index c5444a190d..751c97a4c1 100644 --- a/packages/ai-code-mode-skills/tests/skills-to-tools.test.ts +++ b/packages/ai-code-mode-snippets/tests/snippets-to-tools.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it, vi } from 'vitest' import { z } from 'zod' import { toolDefinition } from '@tanstack/ai' -import { skillToTool, skillsToTools } from '../src/skills-to-tools' -import { createMemorySkillStorage } from '../src/storage/memory-storage' +import { snippetToTool, snippetsToTools } from '../src/snippets-to-tools' +import { createMemorySnippetStorage } from '../src/storage/memory-storage' import type { IsolateContext, IsolateDriver } from '@tanstack/ai-code-mode' -import type { Skill } from '../src/types' +import type { Snippet } from '../src/types' -function makeSkill(overrides: Partial = {}): Skill { +function makeSnippet(overrides: Partial = {}): Snippet { return { id: 'id', name: 'do_thing', @@ -55,25 +55,25 @@ function createMockDriver( const mockContext = () => ({ emitCustomEvent: vi.fn() }) -describe('skillToTool', () => { - it('prefixes the tool description with [SKILL]', () => { +describe('snippetToTool', () => { + it('prefixes the tool description with [SNIPPET]', () => { const { driver } = createMockDriver() - const storage = createMemorySkillStorage([]) - const tool = skillToTool({ - skill: makeSkill({ description: 'Fetches data' }), + const storage = createMemorySnippetStorage([]) + const tool = snippetToTool({ + snippet: makeSnippet({ description: 'Fetches data' }), driver, bindings: {}, storage, }) - expect(tool.description).toContain('[SKILL]') + expect(tool.description).toContain('[SNIPPET]') expect(tool.description).toContain('Fetches data') }) - it('exposes the skill name as the tool name', () => { + it('exposes the snippet name as the tool name', () => { const { driver } = createMockDriver() - const storage = createMemorySkillStorage([]) - const tool = skillToTool({ - skill: makeSkill({ name: 'custom_name' }), + const storage = createMemorySnippetStorage([]) + const tool = snippetToTool({ + snippet: makeSnippet({ name: 'custom_name' }), driver, bindings: {}, storage, @@ -86,9 +86,9 @@ describe('skillToTool', () => { success: true, value: 84, }) - const storage = createMemorySkillStorage([]) - const tool = skillToTool({ - skill: makeSkill(), + const storage = createMemorySnippetStorage([]) + const tool = snippetToTool({ + snippet: makeSnippet(), driver, bindings: {}, storage, @@ -105,9 +105,9 @@ describe('skillToTool', () => { success: false, error: { message: 'sandbox error' }, }) - const storage = createMemorySkillStorage([]) - const tool = skillToTool({ - skill: makeSkill(), + const storage = createMemorySnippetStorage([]) + const tool = snippetToTool({ + snippet: makeSnippet(), driver, bindings: {}, storage, @@ -119,11 +119,11 @@ describe('skillToTool', () => { expect(disposeSpy).toHaveBeenCalledOnce() }) - it('emits skill_call then skill_result events on success', async () => { + it('emits snippet_call then snippet_result events on success', async () => { const { driver } = createMockDriver({ success: true, value: 'ok' }) - const storage = createMemorySkillStorage([]) - const tool = skillToTool({ - skill: makeSkill({ name: 'x' }), + const storage = createMemorySnippetStorage([]) + const tool = snippetToTool({ + snippet: makeSnippet({ name: 'x' }), driver, bindings: {}, storage, @@ -134,19 +134,19 @@ describe('skillToTool', () => { ([name]: [string]) => name, ) expect(eventNames).toEqual([ - 'code_mode:skill_call', - 'code_mode:skill_result', + 'code_mode:snippet_call', + 'code_mode:snippet_result', ]) }) - it('emits skill_error when execution fails', async () => { + it('emits snippet_error when execution fails', async () => { const { driver } = createMockDriver({ success: false, error: { message: 'boom' }, }) - const storage = createMemorySkillStorage([]) - const tool = skillToTool({ - skill: makeSkill({ name: 'x' }), + const storage = createMemorySnippetStorage([]) + const tool = snippetToTool({ + snippet: makeSnippet({ name: 'x' }), driver, bindings: {}, storage, @@ -158,15 +158,15 @@ describe('skillToTool', () => { const eventNames = (ctx.emitCustomEvent as any).mock.calls.map( ([name]: [string]) => name, ) - expect(eventNames).toContain('code_mode:skill_error') + expect(eventNames).toContain('code_mode:snippet_error') }) it('records stats (success=true) on success', async () => { const { driver } = createMockDriver() - const storage = createMemorySkillStorage([makeSkill({ name: 'x' })]) + const storage = createMemorySnippetStorage([makeSnippet({ name: 'x' })]) const spy = vi.spyOn(storage, 'updateStats') - const tool = skillToTool({ - skill: makeSkill({ name: 'x' }), + const tool = snippetToTool({ + snippet: makeSnippet({ name: 'x' }), driver, bindings: {}, storage, @@ -180,10 +180,10 @@ describe('skillToTool', () => { success: false, error: { message: 'no' }, }) - const storage = createMemorySkillStorage([makeSkill({ name: 'x' })]) + const storage = createMemorySnippetStorage([makeSnippet({ name: 'x' })]) const spy = vi.spyOn(storage, 'updateStats') - const tool = skillToTool({ - skill: makeSkill({ name: 'x' }), + const tool = snippetToTool({ + snippet: makeSnippet({ name: 'x' }), driver, bindings: {}, storage, @@ -196,9 +196,9 @@ describe('skillToTool', () => { it('serializes input as a JSON literal in the sandbox code, preventing injection', async () => { const { driver, executeSpy } = createMockDriver() - const storage = createMemorySkillStorage([]) - const tool = skillToTool({ - skill: makeSkill(), + const storage = createMemorySnippetStorage([]) + const tool = snippetToTool({ + snippet: makeSnippet(), driver, bindings: {}, storage, @@ -213,14 +213,14 @@ describe('skillToTool', () => { }) }) -describe('skillsToTools', () => { - it('returns one ServerTool per skill', () => { +describe('snippetsToTools', () => { + it('returns one ServerTool per snippet', () => { const { driver } = createMockDriver() - const storage = createMemorySkillStorage([]) - const tools = skillsToTools({ - skills: [ - makeSkill({ id: '1', name: 'a' }), - makeSkill({ id: '2', name: 'b' }), + const storage = createMemorySnippetStorage([]) + const tools = snippetsToTools({ + snippets: [ + makeSnippet({ id: '1', name: 'a' }), + makeSnippet({ id: '2', name: 'b' }), ], driver, tools: [ diff --git a/packages/ai-code-mode-skills/tests/trust-strategies.test.ts b/packages/ai-code-mode-snippets/tests/trust-strategies.test.ts similarity index 78% rename from packages/ai-code-mode-skills/tests/trust-strategies.test.ts rename to packages/ai-code-mode-snippets/tests/trust-strategies.test.ts index a70c671557..6d929ebceb 100644 --- a/packages/ai-code-mode-skills/tests/trust-strategies.test.ts +++ b/packages/ai-code-mode-snippets/tests/trust-strategies.test.ts @@ -5,66 +5,66 @@ import { createDefaultTrustStrategy, createRelaxedTrustStrategy, } from '../src/trust-strategies' -import type { SkillStats, TrustLevel } from '../src/types' +import type { SnippetStats, TrustLevel } from '../src/types' describe('createDefaultTrustStrategy', () => { - it('starts new skills as untrusted', () => { + it('starts new snippets as untrusted', () => { const strategy = createDefaultTrustStrategy() expect(strategy.getInitialTrustLevel()).toBe('untrusted') }) it('promotes untrusted → provisional at 10 executions with 90% success', () => { const strategy = createDefaultTrustStrategy() - const stats: SkillStats = { executions: 10, successRate: 0.9 } + const stats: SnippetStats = { executions: 10, successRate: 0.9 } expect(strategy.calculateTrustLevel('untrusted', stats)).toBe('provisional') }) it('does not promote with 9 executions (below threshold)', () => { const strategy = createDefaultTrustStrategy() - const stats: SkillStats = { executions: 9, successRate: 1.0 } + const stats: SnippetStats = { executions: 9, successRate: 1.0 } expect(strategy.calculateTrustLevel('untrusted', stats)).toBe('untrusted') }) it('does not promote at 89% success rate', () => { const strategy = createDefaultTrustStrategy() - const stats: SkillStats = { executions: 50, successRate: 0.89 } + const stats: SnippetStats = { executions: 50, successRate: 0.89 } expect(strategy.calculateTrustLevel('untrusted', stats)).toBe('untrusted') }) it('promotes provisional → trusted at 100 executions with 95% success', () => { const strategy = createDefaultTrustStrategy() - const stats: SkillStats = { executions: 100, successRate: 0.95 } + const stats: SnippetStats = { executions: 100, successRate: 0.95 } expect(strategy.calculateTrustLevel('provisional', stats)).toBe('trusted') }) it('does not promote provisional → trusted at 99 executions', () => { const strategy = createDefaultTrustStrategy() - const stats: SkillStats = { executions: 99, successRate: 1.0 } + const stats: SnippetStats = { executions: 99, successRate: 1.0 } expect(strategy.calculateTrustLevel('provisional', stats)).toBe( 'provisional', ) }) - it('never downgrades a trusted skill', () => { + it('never downgrades a trusted snippet', () => { const strategy = createDefaultTrustStrategy() - const stats: SkillStats = { executions: 1000, successRate: 0.1 } + const stats: SnippetStats = { executions: 1000, successRate: 0.1 } expect(strategy.calculateTrustLevel('trusted', stats)).toBe('trusted') }) it('never skips provisional (untrusted cannot jump to trusted)', () => { const strategy = createDefaultTrustStrategy() - const stats: SkillStats = { executions: 500, successRate: 1.0 } + const stats: SnippetStats = { executions: 500, successRate: 1.0 } expect(strategy.calculateTrustLevel('untrusted', stats)).toBe('provisional') }) }) describe('createAlwaysTrustedStrategy', () => { - it('makes new skills trusted immediately', () => { + it('makes new snippets trusted immediately', () => { const strategy = createAlwaysTrustedStrategy() expect(strategy.getInitialTrustLevel()).toBe('trusted') }) - it('keeps skills trusted regardless of stats', () => { + it('keeps snippets trusted regardless of stats', () => { const strategy = createAlwaysTrustedStrategy() const levels: Array = ['untrusted', 'provisional', 'trusted'] for (const level of levels) { @@ -76,20 +76,20 @@ describe('createAlwaysTrustedStrategy', () => { }) describe('createRelaxedTrustStrategy', () => { - it('starts new skills as untrusted', () => { + it('starts new snippets as untrusted', () => { const strategy = createRelaxedTrustStrategy() expect(strategy.getInitialTrustLevel()).toBe('untrusted') }) it('promotes untrusted → provisional at 3 executions with 80% success', () => { const strategy = createRelaxedTrustStrategy() - const stats: SkillStats = { executions: 3, successRate: 0.8 } + const stats: SnippetStats = { executions: 3, successRate: 0.8 } expect(strategy.calculateTrustLevel('untrusted', stats)).toBe('provisional') }) it('promotes provisional → trusted at 10 executions with 90% success', () => { const strategy = createRelaxedTrustStrategy() - const stats: SkillStats = { executions: 10, successRate: 0.9 } + const stats: SnippetStats = { executions: 10, successRate: 0.9 } expect(strategy.calculateTrustLevel('provisional', stats)).toBe('trusted') }) }) @@ -109,7 +109,7 @@ describe('createCustomTrustStrategy', () => { const strategy = createCustomTrustStrategy({ provisionalThreshold: { executions: 5, successRate: 0.5 }, }) - const stats: SkillStats = { executions: 5, successRate: 0.5 } + const stats: SnippetStats = { executions: 5, successRate: 0.5 } expect(strategy.calculateTrustLevel('untrusted', stats)).toBe('provisional') }) @@ -117,7 +117,7 @@ describe('createCustomTrustStrategy', () => { const strategy = createCustomTrustStrategy({ trustedThreshold: { executions: 20, successRate: 0.85 }, }) - const stats: SkillStats = { executions: 20, successRate: 0.85 } + const stats: SnippetStats = { executions: 20, successRate: 0.85 } expect(strategy.calculateTrustLevel('provisional', stats)).toBe('trusted') }) @@ -125,7 +125,7 @@ describe('createCustomTrustStrategy', () => { const strategy = createCustomTrustStrategy({ provisionalThreshold: { executions: 5, successRate: 0.9 }, }) - const stats: SkillStats = { executions: 5, successRate: 0.8 } + const stats: SnippetStats = { executions: 5, successRate: 0.8 } expect(strategy.calculateTrustLevel('untrusted', stats)).toBe('untrusted') }) }) diff --git a/packages/ai-code-mode-skills/tsconfig.json b/packages/ai-code-mode-snippets/tsconfig.json similarity index 100% rename from packages/ai-code-mode-skills/tsconfig.json rename to packages/ai-code-mode-snippets/tsconfig.json diff --git a/packages/ai-code-mode-skills/vite.config.ts b/packages/ai-code-mode-snippets/vite.config.ts similarity index 100% rename from packages/ai-code-mode-skills/vite.config.ts rename to packages/ai-code-mode-snippets/vite.config.ts diff --git a/packages/ai-code-mode/README.md b/packages/ai-code-mode/README.md index 8b85dd3dff..9d21073218 100644 --- a/packages/ai-code-mode/README.md +++ b/packages/ai-code-mode/README.md @@ -89,7 +89,7 @@ Creates both the `execute_typescript` tool and its matching system prompt. This - `tools` — Array of `ServerTool` or `ToolDefinition` instances. Exposed as `external_*` functions in the sandbox - `timeout` — Execution timeout in ms (default: 30000) - `memoryLimit` — Memory limit in MB (default: 128, supported by the Node, QuickJS, and QuickJS Bun drivers) -- `getSkillBindings` — Optional async function returning dynamic bindings +- `getSnippetBindings` — Optional async function returning dynamic bindings ### `createCodeModeTool(config)` / `createCodeModeSystemPrompt(config)` diff --git a/packages/ai-code-mode/skills/ai-code-mode/SKILL.md b/packages/ai-code-mode/skills/ai-code-mode/SKILL.md index 51ab021466..06a9646689 100644 --- a/packages/ai-code-mode/skills/ai-code-mode/SKILL.md +++ b/packages/ai-code-mode/skills/ai-code-mode/SKILL.md @@ -5,8 +5,8 @@ description: > createCodeModeTool() with isolate drivers (createNodeIsolateDriver, createQuickJSIsolateDriver, createQuickJSBunIsolateDriver, createCloudflareIsolateDriver), - codeModeWithSkills() for persistent skill libraries, trust strategies, - skill storage (FileSystem, LocalStorage, InMemory, Mongo), client-side + codeModeWithSnippets() for persistent snippet libraries, trust strategies, + snippet storage (FileSystem, LocalStorage, InMemory, Mongo), client-side execution progress via code_mode:* custom events in useChat. type: core library: tanstack-ai @@ -14,7 +14,7 @@ library_version: '0.3.8' sources: - 'TanStack/ai:docs/code-mode/code-mode.md' - 'TanStack/ai:docs/code-mode/code-mode-isolates.md' - - 'TanStack/ai:docs/code-mode/code-mode-with-skills.md' + - 'TanStack/ai:docs/code-mode/code-mode-with-snippets.md' - 'TanStack/ai:docs/code-mode/client-integration.md' - 'TanStack/ai:docs/code-mode/lazy-tools.md' --- @@ -149,23 +149,23 @@ const driver = createCloudflareIsolateDriver({ | QuickJS Bun | Bun servers | None | No | Fast (native QuickJS) | | Cloudflare | Edge deployments | None | N/A | Fast (V8 on edge) | -### 2. Adding Persistent Skills with codeModeWithSkills() +### 2. Adding Persistent Snippets with codeModeWithSnippets() -Skills let the LLM save reusable code snippets. On future requests, relevant skills are loaded and exposed as callable tools. +Snippets let the LLM save reusable code snippets. On future requests, relevant snippets are loaded and exposed as callable tools. ```typescript import { chat, maxIterations } from '@tanstack/ai' import { createNodeIsolateDriver } from '@tanstack/ai-isolate-node' -import { codeModeWithSkills } from '@tanstack/ai-code-mode-skills' -import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' +import { codeModeWithSnippets } from '@tanstack/ai-code-mode-snippets' +import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storage' import { createDefaultTrustStrategy, createAlwaysTrustedStrategy, createCustomTrustStrategy, -} from '@tanstack/ai-code-mode-skills' +} from '@tanstack/ai-code-mode-snippets' import { openaiText } from '@tanstack/ai-openai' -// Trust strategies control how skills earn trust through executions +// Trust strategies control how snippets earn trust through executions // Default: untrusted -> provisional (10+ runs, >=90%) -> trusted (100+ runs, >=95%) // Relaxed: untrusted -> provisional (3+ runs, >=80%) -> trusted (10+ runs, >=90%) // Always trusted: immediately trusted (dev/testing) @@ -173,26 +173,26 @@ import { openaiText } from '@tanstack/ai-openai' const trustStrategy = createDefaultTrustStrategy() // Storage options: file system (production) or memory (testing) -const storage = createFileSkillStorage({ - directory: './.skills', +const storage = createFileSnippetStorage({ + directory: './.snippets', trustStrategy, }) const driver = createNodeIsolateDriver() -// High-level API: automatic LLM-based skill selection -const { toolsRegistry, systemPrompt, selectedSkills } = - await codeModeWithSkills({ +// High-level API: automatic LLM-based snippet selection +const { toolsRegistry, systemPrompt, selectedSnippets } = + await codeModeWithSnippets({ config: { driver, tools: [myTool1, myTool2], timeout: 60_000, memoryLimit: 128, }, - adapter: openaiText('gpt-4o-mini'), // cheap model for skill selection - skills: { + adapter: openaiText('gpt-4o-mini'), // cheap model for snippet selection + snippets: { storage, - maxSkillsInContext: 5, + maxSnippetsInContext: 5, }, messages, }) @@ -206,7 +206,7 @@ const stream = chat({ }) ``` -The registry includes: `execute_typescript`, `search_skills`, `get_skill`, `register_skill`, and one tool per selected skill. +The registry includes: `execute_typescript`, `search_snippets`, `get_snippet`, `register_snippet`, and one tool per selected snippet. Custom trust strategy example: @@ -221,13 +221,13 @@ const strategy = createCustomTrustStrategy({ Storage implementations: ```typescript -// File storage (production) -- persists skills as files on disk -import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' -const fileStorage = createFileSkillStorage({ directory: './.skills' }) +// File storage (production) -- persists snippets as files on disk +import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storage' +const fileStorage = createFileSnippetStorage({ directory: './.snippets' }) // Memory storage (testing) -- in-memory, lost on restart -import { createMemorySkillStorage } from '@tanstack/ai-code-mode-skills/storage' -const memStorage = createMemorySkillStorage() +import { createMemorySnippetStorage } from '@tanstack/ai-code-mode-snippets/storage' +const memStorage = createMemorySnippetStorage() ``` ### 3. Client-Side Execution Progress Display @@ -334,14 +334,14 @@ The `onCustomEvent` callback signature is identical across all framework integra (eventType: string, data: unknown, context: { toolCallId?: string }) => void ``` -Skill-specific events (when using `codeModeWithSkills`): +Snippet-specific events (when using `codeModeWithSnippets`): -| Event | When | Key fields | -| ------------------------ | ------------------ | ----------------------------- | -| `code_mode:skill_call` | Skill tool invoked | `skill`, `input`, `timestamp` | -| `code_mode:skill_result` | Skill completed | `skill`, `result`, `duration` | -| `code_mode:skill_error` | Skill failed | `skill`, `error`, `duration` | -| `skill:registered` | New skill saved | `id`, `name`, `description` | +| Event | When | Key fields | +| -------------------------- | -------------------- | ------------------------------- | +| `code_mode:snippet_call` | Snippet tool invoked | `snippet`, `input`, `timestamp` | +| `code_mode:snippet_result` | Snippet completed | `snippet`, `result`, `duration` | +| `code_mode:snippet_error` | Snippet failed | `snippet`, `error`, `duration` | +| `snippet:registered` | New snippet saved | `id`, `name`, `description` | ### 4. Lazy Tools diff --git a/packages/ai-code-mode/src/create-code-mode-tool.ts b/packages/ai-code-mode/src/create-code-mode-tool.ts index 5d476f8414..1c1213d5fe 100644 --- a/packages/ai-code-mode/src/create-code-mode-tool.ts +++ b/packages/ai-code-mode/src/create-code-mode-tool.ts @@ -94,7 +94,7 @@ export function createCodeModeTool( tools, timeout = 30000, memoryLimit = 128, - getSkillBindings, + getSnippetBindings, onSecretParameter, transpile = stripTypeScript, } = config @@ -107,7 +107,7 @@ export function createCodeModeTool( // Transform tools to bindings with external_ prefix (static bindings) const staticBindings = toolsToBindings(tools, 'external_') - // Shared across static + dynamic (skill) binding scans so a given + // Shared across static + dynamic (snippet) binding scans so a given // (toolName, paramPath) pair surfaces at most once per code-mode instance. const secretDedupCache = new Set() @@ -220,22 +220,24 @@ export function createCodeModeTool( ) } - // Step 2: Get dynamic skill bindings if available - const skillBindings = getSkillBindings ? await getSkillBindings() : {} + // Step 2: Get dynamic snippet bindings if available + const snippetBindings = getSnippetBindings + ? await getSnippetBindings() + : {} // Scan dynamic bindings too — their schemas are equally in-scope for // the same exfiltration threat. Dedup cache prevents repeat warnings // when the same binding reappears across executions. - const skillBindingValues = Object.values(skillBindings) - if (skillBindingValues.length > 0) { - warnIfBindingsExposeSecrets(skillBindingValues, { + const snippetBindingValues = Object.values(snippetBindings) + if (snippetBindingValues.length > 0) { + warnIfBindingsExposeSecrets(snippetBindingValues, { handler: onSecretParameter, dedupCache: secretDedupCache, }) } // Step 3: Merge static and dynamic bindings, then wrap with event awareness - const allBindings = { ...staticBindings, ...skillBindings } + const allBindings = { ...staticBindings, ...snippetBindings } const eventAwareBindings = createEventAwareBindings( allBindings, emitCustomEvent, diff --git a/packages/ai-code-mode/src/types.ts b/packages/ai-code-mode/src/types.ts index 7dbbd02a38..f791b2a164 100644 --- a/packages/ai-code-mode/src/types.ts +++ b/packages/ai-code-mode/src/types.ts @@ -183,20 +183,20 @@ export interface CodeModeToolConfig { /** * Optional function to get additional bindings dynamically. - * Called at execution time (each execute_typescript call) to get current skill bindings. + * Called at execution time (each execute_typescript call) to get current snippet bindings. * These are merged with the static external_* bindings. * - * @returns Record of skill bindings with skill_ prefix + * @returns Record of snippet bindings with snippet_ prefix * * @example * ```typescript - * getSkillBindings: async () => { - * const skills = await storage.loadAll() - * return skillsToBindings(skills, 'skill_') + * getSnippetBindings: async () => { + * const snippets = await storage.loadAll() + * return snippetsToBindings(snippets, 'snippet_') * } * ``` */ - getSkillBindings?: () => Promise> + getSnippetBindings?: () => Promise> /** * How to surface tool parameters whose names look like secrets. @@ -233,8 +233,8 @@ export interface CodeModeToolConfig { * and `await` in its input (the default wraps the code in an async function * internally to allow this). * - * NOTE: This only affects `createCodeModeTool`. The skills helpers - * (`skillsToTools`, `codeModeWithSkills` in `@tanstack/ai-code-mode-skills`) + * NOTE: This only affects `createCodeModeTool`. The snippet helpers + * (`snippetsToTools`, `codeModeWithSnippets` in `@tanstack/ai-code-mode-snippets`) * call the exported `stripTypeScript` directly, so they ignore this hook — but * they still get the edge-safe sucrase default, so #487 is fixed for them too; * they just can't be pointed at a different transpiler. diff --git a/packages/ai-code-mode/tests/create-code-mode-tool.test.ts b/packages/ai-code-mode/tests/create-code-mode-tool.test.ts index bae2f5927d..6a1dca6b2c 100644 --- a/packages/ai-code-mode/tests/create-code-mode-tool.test.ts +++ b/packages/ai-code-mode/tests/create-code-mode-tool.test.ts @@ -256,11 +256,11 @@ describe('createCodeModeTool', () => { ) }) - it('getSkillBindings merges dynamic bindings into context', async () => { + it('getSnippetBindings merges dynamic bindings into context', async () => { const { driver } = createMockDriver() - const skillBinding = { - name: 'skill_greet', + const snippetBinding = { + name: 'snippet_greet', description: 'Greet someone', inputSchema: { type: 'object' }, execute: vi.fn().mockResolvedValue('hi'), @@ -269,7 +269,7 @@ describe('createCodeModeTool', () => { const tool = createCodeModeTool({ driver, tools: [createMockTool('fetchWeather')], - getSkillBindings: async () => ({ skill_greet: skillBinding }), + getSnippetBindings: async () => ({ snippet_greet: snippetBinding }), }) await tool.execute!({ typescriptCode: 'return 1' }) @@ -279,7 +279,7 @@ describe('createCodeModeTool', () => { if (!contextConfig) { throw new Error('Expected createContext to be called') } - expect(contextConfig.bindings).toHaveProperty('skill_greet') + expect(contextConfig.bindings).toHaveProperty('snippet_greet') expect(contextConfig.bindings).toHaveProperty('external_fetchWeather') }) diff --git a/packages/ai/skills/ai-core/tool-calling/SKILL.md b/packages/ai/skills/ai-core/tool-calling/SKILL.md index 67316ef08f..1c5616061b 100644 --- a/packages/ai/skills/ai-core/tool-calling/SKILL.md +++ b/packages/ai/skills/ai-core/tool-calling/SKILL.md @@ -623,7 +623,7 @@ export const Route = createFileRoute('/api/chat')({ ## Provider Skills -> **Not to be confused with `@tanstack/ai-code-mode-skills`**, which are locally-generated TypeScript functions executed client-side. Provider Skills are hosted, provider-managed bundles that the model loads on demand and runs inside the provider's server-side sandbox. +> **Not to be confused with `@tanstack/ai-code-mode-snippets`**, whose snippets are TypeScript functions your application generates and runs in its own Code Mode sandbox (a local JS isolate). Provider Skills are hosted, provider-managed bundles that the model loads on demand and runs inside the provider's server-side sandbox. Provider Skills are inert without an execution tool. The execution tool is what activates the sandbox; skills are additional capability bundles that run inside it: diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index e80d02175a..49e7928a99 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -1555,20 +1555,25 @@ export interface CodeModeExternalErrorEvent extends CustomEvent { name: 'code_mode:external_error' value: { function: string; error: string; duration: number } } -export interface CodeModeSkillCallEvent extends CustomEvent { - name: 'code_mode:skill_call' - value: { skill: string; input: unknown; timestamp: number } +export interface CodeModeSnippetCallEvent extends CustomEvent { + name: 'code_mode:snippet_call' + value: { snippet: string; input: unknown; timestamp: number } } -export interface CodeModeSkillResultEvent extends CustomEvent { - name: 'code_mode:skill_result' - value: { skill: string; result: unknown; duration: number; timestamp: number } +export interface CodeModeSnippetResultEvent extends CustomEvent { + name: 'code_mode:snippet_result' + value: { + snippet: string + result: unknown + duration: number + timestamp: number + } } -export interface CodeModeSkillErrorEvent extends CustomEvent { - name: 'code_mode:skill_error' - value: { skill: string; error: string; duration: number; timestamp: number } +export interface CodeModeSnippetErrorEvent extends CustomEvent { + name: 'code_mode:snippet_error' + value: { snippet: string; error: string; duration: number; timestamp: number } } -export interface SkillRegisteredEvent extends CustomEvent { - name: 'skill:registered' +export interface SnippetRegisteredEvent extends CustomEvent { + name: 'snippet:registered' value: { id: string; name: string; description: string; timestamp: number } } @@ -1587,10 +1592,10 @@ export type KnownCustomEvent = | CodeModeExternalCallEvent | CodeModeExternalResultEvent | CodeModeExternalErrorEvent - | CodeModeSkillCallEvent - | CodeModeSkillResultEvent - | CodeModeSkillErrorEvent - | SkillRegisteredEvent + | CodeModeSnippetCallEvent + | CodeModeSnippetResultEvent + | CodeModeSnippetErrorEvent + | SnippetRegisteredEvent | StructuredOutputStartEvent | StructuredOutputCompleteEvent | ApprovalRequestedEvent diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d72911f51..03a987c888 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -248,7 +248,7 @@ importers: version: 19.2.3(@types/react@19.2.7) '@vitejs/plugin-react': specifier: ^5.1.2 - version: 5.1.2(supports-color@7.2.0)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 5.1.2(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) tailwindcss: specifier: ^4.1.18 version: 4.1.18 @@ -443,9 +443,9 @@ importers: '@tanstack/ai-code-mode': specifier: workspace:* version: link:../../packages/ai-code-mode - '@tanstack/ai-code-mode-skills': + '@tanstack/ai-code-mode-snippets': specifier: workspace:* - version: link:../../packages/ai-code-mode-skills + version: link:../../packages/ai-code-mode-snippets '@tanstack/ai-gemini': specifier: workspace:* version: link:../../packages/ai-gemini @@ -636,7 +636,7 @@ importers: version: 8.18.1 '@vitejs/plugin-react': specifier: ^5.1.2 - version: 5.1.2(supports-color@7.2.0)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 5.1.2(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) jsdom: specifier: ^27.2.0 version: 27.3.0(postcss@8.5.19) @@ -1675,7 +1675,7 @@ importers: specifier: ^4.2.0 version: 4.3.6 - packages/ai-code-mode-skills: + packages/ai-code-mode-snippets: dependencies: '@tanstack/ai': specifier: workspace:^ @@ -1704,7 +1704,7 @@ importers: version: 13.1.0 dotenv: specifier: ^17.2.3 - version: 17.2.3 + version: 17.4.2 tsx: specifier: ^4.21.0 version: 4.21.0 @@ -16936,7 +16936,7 @@ snapshots: '@angular/compiler-cli@21.2.17(@angular/compiler@21.2.19)(typescript@5.9.3)': dependencies: '@angular/compiler': 21.2.19 - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@jridgewell/sourcemap-codec': 1.5.5 chokidar: 5.0.0 convert-source-map: 1.9.0 @@ -17433,7 +17433,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/core@7.29.0(supports-color@7.2.0)': + '@babel/core@7.29.0': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 @@ -17453,6 +17453,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/core@7.29.0(supports-color@7.2.0)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.0(supports-color@7.2.0)) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@7.2.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/core@7.29.7': dependencies: '@babel/code-frame': 7.29.7 @@ -17562,7 +17582,7 @@ snapshots: '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-annotate-as-pure': 7.29.7 '@babel/helper-member-expression-to-functions': 7.29.7 '@babel/helper-optimise-call-expression': 7.29.7 @@ -17660,7 +17680,7 @@ snapshots: '@babel/helper-module-transforms@7.28.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.29.7 '@babel/traverse': 7.29.7 @@ -17676,7 +17696,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.0)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.0(supports-color@7.2.0))': dependencies: '@babel/core': 7.29.0(supports-color@7.2.0) '@babel/helper-module-imports': 7.29.7 @@ -17685,6 +17705,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -17746,7 +17775,7 @@ snapshots: '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-member-expression-to-functions': 7.29.7 '@babel/helper-optimise-call-expression': 7.29.7 '@babel/traverse': 7.29.7 @@ -17868,7 +17897,7 @@ snapshots: '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.29.7)': @@ -17878,7 +17907,7 @@ snapshots: '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': @@ -17903,7 +17932,7 @@ snapshots: '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.29.7)': @@ -18018,7 +18047,7 @@ snapshots: '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: @@ -18122,14 +18151,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0(supports-color@7.2.0))': + dependencies: + '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0(supports-color@7.2.0))': dependencies: '@babel/core': 7.29.0(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-react-jsx@7.29.7(@babel/core@7.29.7)': @@ -18174,7 +18213,7 @@ snapshots: '@babel/plugin-transform-typescript@7.28.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-annotate-as-pure': 7.29.7 '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.29.7 @@ -18220,7 +18259,7 @@ snapshots: '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0) @@ -19365,7 +19404,7 @@ snapshots: '@expo/metro-config@56.0.13(expo@56.0.5)(typescript@5.9.3)': dependencies: '@babel/code-frame': 7.29.0 - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/generator': 7.29.1 '@expo/config': 56.0.9(typescript@5.9.3) '@expo/env': 2.3.0 @@ -19466,7 +19505,7 @@ snapshots: '@expo/require-utils@56.1.3(typescript@5.9.3)': dependencies: '@babel/code-frame': 7.29.7 - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.29.0) optionalDependencies: typescript: 5.9.3 @@ -22902,7 +22941,7 @@ snapshots: '@tanstack/directive-functions-plugin@1.131.2(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/code-frame': 7.27.1 - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/traverse': 7.29.7 '@babel/types': 7.29.7 '@tanstack/router-utils': 1.131.2 @@ -22915,7 +22954,7 @@ snapshots: '@tanstack/directive-functions-plugin@1.141.0(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/code-frame': 7.27.1 - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/traverse': 7.29.7 '@babel/types': 7.29.7 '@tanstack/router-utils': 1.141.0 @@ -23358,7 +23397,7 @@ snapshots: '@tanstack/router-plugin@1.131.50(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.0) '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0) '@babel/template': 7.29.7 @@ -23381,7 +23420,7 @@ snapshots: '@tanstack/router-plugin@1.141.1(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.0) '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0) '@babel/template': 7.29.7 @@ -23404,7 +23443,7 @@ snapshots: '@tanstack/router-plugin@1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.0) '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0) '@babel/template': 7.29.7 @@ -23426,7 +23465,7 @@ snapshots: '@tanstack/router-plugin@1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.0) '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0) '@babel/template': 7.29.7 @@ -23458,7 +23497,7 @@ snapshots: '@tanstack/router-utils@1.131.2': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/generator': 7.29.7 '@babel/parser': 7.29.7 '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) @@ -23469,7 +23508,7 @@ snapshots: '@tanstack/router-utils@1.141.0': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/generator': 7.29.7 '@babel/parser': 7.29.7 '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) @@ -23482,7 +23521,7 @@ snapshots: '@tanstack/router-utils@1.158.0': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/generator': 7.29.7 '@babel/parser': 7.29.7 '@babel/types': 7.29.7 @@ -23497,7 +23536,7 @@ snapshots: '@tanstack/server-functions-plugin@1.131.2(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/code-frame': 7.27.1 - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.0) '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0) '@babel/template': 7.29.7 @@ -23513,7 +23552,7 @@ snapshots: '@tanstack/server-functions-plugin@1.141.0(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/code-frame': 7.27.1 - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.0) '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0) '@babel/template': 7.29.7 @@ -23769,7 +23808,7 @@ snapshots: '@tanstack/start-plugin-core@1.131.50(1b6c6d12f54334d0121e3887f2ab8ba8)': dependencies: '@babel/code-frame': 7.26.2 - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/types': 7.29.7 '@tanstack/router-core': 1.131.50 '@tanstack/router-generator': 1.131.50 @@ -23827,7 +23866,7 @@ snapshots: '@tanstack/start-plugin-core@1.141.1(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.6(srvx@0.11.17))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/code-frame': 7.26.2 - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/types': 7.29.0 '@rolldown/pluginutils': 1.0.0-beta.40 '@tanstack/router-core': 1.141.1 @@ -23859,7 +23898,7 @@ snapshots: '@tanstack/start-plugin-core@1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.6(srvx@0.11.17))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/code-frame': 7.27.1 - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/types': 7.29.7 '@rolldown/pluginutils': 1.0.0-beta.40 '@tanstack/router-core': 1.159.4 @@ -23889,7 +23928,7 @@ snapshots: '@tanstack/start-plugin-core@1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.6(srvx@0.11.17))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/code-frame': 7.27.1 - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/types': 7.29.7 '@rolldown/pluginutils': 1.0.0-beta.40 '@tanstack/router-core': 1.159.4 @@ -24753,7 +24792,7 @@ snapshots: '@vitejs/plugin-react@4.7.0(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) '@rolldown/pluginutils': 1.0.0-beta.27 @@ -24766,6 +24805,18 @@ snapshots: '@vitejs/plugin-react@5.1.2(supports-color@7.2.0)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0(supports-color@7.2.0)) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0(supports-color@7.2.0)) + '@rolldown/pluginutils': 1.0.0-beta.53 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-react@5.1.2(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + dependencies: + '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) '@rolldown/pluginutils': 1.0.0-beta.53 @@ -24777,7 +24828,7 @@ snapshots: '@vitejs/plugin-react@5.1.2(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) '@rolldown/pluginutils': 1.0.0-beta.53 @@ -25283,7 +25334,7 @@ snapshots: babel-dead-code-elimination@1.0.10: dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/parser': 7.29.7 '@babel/traverse': 7.29.7 '@babel/types': 7.29.7 @@ -25292,7 +25343,7 @@ snapshots: babel-dead-code-elimination@1.0.12: dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/parser': 7.29.7 '@babel/traverse': 7.29.7 '@babel/types': 7.29.7 @@ -25301,7 +25352,7 @@ snapshots: babel-plugin-jsx-dom-expressions@0.40.3(@babel/core@7.29.0): dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-module-imports': 7.18.6 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.0) '@babel/types': 7.29.7 @@ -25463,7 +25514,7 @@ snapshots: babel-preset-solid@1.9.10(@babel/core@7.29.0)(solid-js@1.9.10): dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 babel-plugin-jsx-dom-expressions: 0.40.3(@babel/core@7.29.0) optionalDependencies: solid-js: 1.9.10 @@ -26602,7 +26653,7 @@ snapshots: esbuild-plugin-solid@0.5.0(esbuild@0.28.1)(solid-js@1.9.10): dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) babel-preset-solid: 1.9.10(@babel/core@7.29.0)(solid-js@1.9.10) esbuild: 0.28.1 @@ -28137,7 +28188,7 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/parser': 7.29.7 '@istanbuljs/schema': 0.1.6 istanbul-lib-coverage: 3.2.2 @@ -28946,7 +28997,7 @@ snapshots: metro-babel-transformer@0.84.4: dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 flow-enums-runtime: 0.0.6 hermes-parser: 0.35.0 metro-cache-key: 0.84.4 @@ -29043,7 +29094,7 @@ snapshots: metro-transform-plugins@0.84.4: dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/generator': 7.29.7 '@babel/template': 7.29.7 '@babel/traverse': 7.29.7 @@ -29054,7 +29105,7 @@ snapshots: metro-transform-worker@0.84.4: dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/generator': 7.29.7 '@babel/parser': 7.29.7 '@babel/types': 7.29.7 @@ -29075,7 +29126,7 @@ snapshots: metro@0.84.4: dependencies: '@babel/code-frame': 7.29.7 - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/generator': 7.29.7 '@babel/parser': 7.29.7 '@babel/template': 7.29.7 @@ -32940,7 +32991,7 @@ snapshots: vinxi@0.5.3(91b39b40f4efbadaf93b05367f1ce328): dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.0) '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0) '@types/micromatch': 4.0.10