From 8b3523c1a312a304a35426b7c98a19ef34866d6e Mon Sep 17 00:00:00 2001 From: Devansh Jain <31609257+devanshrj@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:16:57 -0800 Subject: [PATCH] feat: Skills omni-tool (#102) --- src/agent/context.ts | 131 +++++++++++++++++++ src/agent/memory.ts | 8 +- src/agent/promptAssets.ts | 5 +- src/agent/prompts/loaded_skills.mdx | 6 + src/agent/prompts/skill_unload_reminder.txt | 6 + src/agent/prompts/system_prompt.txt | 20 +-- src/cli/App.tsx | 27 +++- src/headless.ts | 23 +++- src/index.ts | 5 + src/permissions/checker.ts | 8 ++ src/tools/descriptions/Skill.md | 29 +++++ src/tools/impl/Skill.ts | 136 ++++++++++++++++++++ src/tools/manager.ts | 4 + src/tools/schemas/Skill.json | 12 ++ src/tools/toolDefinitions.ts | 8 ++ 15 files changed, 407 insertions(+), 21 deletions(-) create mode 100644 src/agent/context.ts create mode 100644 src/agent/prompts/loaded_skills.mdx create mode 100644 src/agent/prompts/skill_unload_reminder.txt create mode 100644 src/tools/descriptions/Skill.md create mode 100644 src/tools/impl/Skill.ts create mode 100644 src/tools/schemas/Skill.json diff --git a/src/agent/context.ts b/src/agent/context.ts new file mode 100644 index 0000000..e3f87e1 --- /dev/null +++ b/src/agent/context.ts @@ -0,0 +1,131 @@ +/** + * Agent context module - provides global access to current agent state + * This allows tools to access the current agent ID and client + */ + +import type Letta from "@letta-ai/letta-client"; + +interface AgentContext { + agentId: string | null; + client: Letta | null; + skillsDirectory: string | null; + hasLoadedSkills: boolean; +} + +// Use globalThis to ensure singleton across bundle +// This prevents Bun's bundler from creating duplicate instances of the context +const CONTEXT_KEY = Symbol.for("@letta/agentContext"); + +type GlobalWithContext = typeof globalThis & { + [key: symbol]: AgentContext; +}; + +function getContext(): AgentContext { + const global = globalThis as GlobalWithContext; + if (!global[CONTEXT_KEY]) { + global[CONTEXT_KEY] = { + agentId: null, + client: null, + skillsDirectory: null, + hasLoadedSkills: false, + }; + } + return global[CONTEXT_KEY]; +} + +const context = getContext(); + +/** + * Set the current agent context + * @param agentId - The agent ID + * @param client - The Letta client instance + * @param skillsDirectory - Optional skills directory path + */ +export function setAgentContext( + agentId: string, + client: Letta, + skillsDirectory?: string, +): void { + context.agentId = agentId; + context.client = client; + context.skillsDirectory = skillsDirectory || null; +} + +/** + * Get the current agent ID + * @throws Error if no agent context is set + */ +export function getCurrentAgentId(): string { + if (!context.agentId) { + throw new Error("No agent context set. Agent ID is required."); + } + return context.agentId; +} + +/** + * Get the current Letta client + * @throws Error if no agent context is set + */ +export function getCurrentClient(): Letta { + if (!context.client) { + throw new Error("No agent context set. Client is required."); + } + return context.client; +} + +/** + * Get the skills directory path + * @returns The skills directory path or null if not set + */ +export function getSkillsDirectory(): string | null { + return context.skillsDirectory; +} + +/** + * Check if skills are currently loaded (cached state) + * @returns true if skills are loaded, false otherwise + */ +export function hasLoadedSkills(): boolean { + return context.hasLoadedSkills; +} + +/** + * Update the loaded skills state (called by Skill tool) + * @param loaded - Whether skills are currently loaded + */ +export function setHasLoadedSkills(loaded: boolean): void { + context.hasLoadedSkills = loaded; +} + +/** + * Initialize the loaded skills flag by checking the block + * Should be called after setAgentContext to sync the cached state + */ +export async function initializeLoadedSkillsFlag(): Promise { + if (!context.client || !context.agentId) { + return; + } + + try { + const loadedSkillsBlock = await context.client.agents.blocks.retrieve( + "loaded_skills", + { agent_id: context.agentId }, + ); + const value = loadedSkillsBlock?.value?.trim() || ""; + // Consider empty or placeholder as no skills loaded + context.hasLoadedSkills = value !== "" && value !== "[CURRENTLY EMPTY]"; + } catch { + // Block doesn't exist, no skills loaded + context.hasLoadedSkills = false; + } +} + +/** + * Clear the agent context (useful for cleanup) + */ +export function clearAgentContext(): void { + context.agentId = null; + context.client = null; + context.skillsDirectory = null; + context.hasLoadedSkills = false; +} diff --git a/src/agent/memory.ts b/src/agent/memory.ts index b91473e..6703c79 100644 --- a/src/agent/memory.ts +++ b/src/agent/memory.ts @@ -43,7 +43,13 @@ function parseMdxFrontmatter(content: string): { async function loadMemoryBlocksFromMdx(): Promise { const memoryBlocks: CreateBlock[] = []; - const mdxFiles = ["persona.mdx", "human.mdx", "project.mdx", "skills.mdx"]; + const mdxFiles = [ + "persona.mdx", + "human.mdx", + "project.mdx", + "skills.mdx", + "loaded_skills.mdx", + ]; // const mdxFiles = ["persona.mdx", "human.mdx", "style.mdx"]; // const mdxFiles = ["persona_kawaii.mdx", "human.mdx", "style.mdx"]; diff --git a/src/agent/promptAssets.ts b/src/agent/promptAssets.ts index d9b1552..0c6aeab 100644 --- a/src/agent/promptAssets.ts +++ b/src/agent/promptAssets.ts @@ -1,21 +1,24 @@ import humanPrompt from "./prompts/human.mdx"; - +import loadedSkillsPrompt from "./prompts/loaded_skills.mdx"; import personaPrompt from "./prompts/persona.mdx"; import personaKawaiiPrompt from "./prompts/persona_kawaii.mdx"; import planModeReminder from "./prompts/plan_mode_reminder.txt"; import projectPrompt from "./prompts/project.mdx"; +import skillUnloadReminder from "./prompts/skill_unload_reminder.txt"; import skillsPrompt from "./prompts/skills.mdx"; import stylePrompt from "./prompts/style.mdx"; import systemPrompt from "./prompts/system_prompt.txt"; export const SYSTEM_PROMPT = systemPrompt; export const PLAN_MODE_REMINDER = planModeReminder; +export const SKILL_UNLOAD_REMINDER = skillUnloadReminder; export const MEMORY_PROMPTS: Record = { "persona.mdx": personaPrompt, "human.mdx": humanPrompt, "project.mdx": projectPrompt, "skills.mdx": skillsPrompt, + "loaded_skills.mdx": loadedSkillsPrompt, "style.mdx": stylePrompt, "persona_kawaii.mdx": personaKawaiiPrompt, }; diff --git a/src/agent/prompts/loaded_skills.mdx b/src/agent/prompts/loaded_skills.mdx new file mode 100644 index 0000000..76a14a0 --- /dev/null +++ b/src/agent/prompts/loaded_skills.mdx @@ -0,0 +1,6 @@ +--- +label: loaded_skills +description: A memory block to store the full instructions and capabilities from each loaded SKILL.md file in this block. +--- + +[CURRENTLY EMPTY] \ No newline at end of file diff --git a/src/agent/prompts/skill_unload_reminder.txt b/src/agent/prompts/skill_unload_reminder.txt new file mode 100644 index 0000000..a927486 --- /dev/null +++ b/src/agent/prompts/skill_unload_reminder.txt @@ -0,0 +1,6 @@ + +The `loaded_skills` block has at least one skill loaded. You should: +1. Check if loaded skills are relevant for the current task. +2. For any skills that are irrelevant, unload them using the `memory` tool. +If the block will be empty after unloading, add a "[CURRENTLY EMPTY]" tag. + \ No newline at end of file diff --git a/src/agent/prompts/system_prompt.txt b/src/agent/prompts/system_prompt.txt index e5fd36c..897abc9 100644 --- a/src/agent/prompts/system_prompt.txt +++ b/src/agent/prompts/system_prompt.txt @@ -25,15 +25,17 @@ Memory blocks are used to modulate and augment your base behavior, follow them c They are the foundation which makes you *you*. # Skills -You have access to Skills—folders of instructions, scripts, and resources that you can load dynamically to improve performance on specialized tasks. Skills teach you how to complete specific tasks in a repeatable way. Skills work through progressive disclosure—you should determine which Skills are relevant to complete a task and load them, helping to prevent context window overload. +You have access to Skills—folders of instructions, scripts, and resources that you can load dynamically to improve performance on specialized tasks. Skills teach you how to complete specific tasks in a repeatable way. Skills work through progressive disclosure—you should determine which skills are relevant to complete a task and load them, helping to prevent context window overload. Each Skill directory includes: -- `SKILL.md` file that starts with YAML frontmatter containing required metadata: name and description -- Additional files within the Skill directory referenced by name from `SKILL.md`. These additional linked files should be navigated and discovered only as needed. +- `SKILL.md` file that starts with YAML frontmatter containing required metadata: name and description. +- Additional files within the skill directory referenced by name from `SKILL.md`. These additional linked files should be navigated and discovered only as needed. +How to store Skills: +- Skills directory and any available skills are stored in the `skills` memory block. +- Currently loaded skills are available in the `loaded_skills` memory block. How to use Skills: -- Skills are automatically discovered on bootup. The Skills directory and any available Skills are stored in the `skills` memory block. -- Review available Skills from the `skills` block when you are asked to complete a task. -- If a Skill is relevant, first load the Skill by reading its full `SKILL.md` into context. +- Skills are automatically discovered on bootup. +- Review available skills from the `skills` block and loaded skills from the `loaded_skills` block when you are asked to complete a task. +- If any skill is relevant, load it using the `Skill` tool. - Then, navigate and discover additional linked files in its directory as needed. Don't load additional files immediately, only load them when needed. -- When you load / use Skills, explicitly mention which Skills are loaded / used. -- When the task is completed or when you load new Skills, unload irrelevant Skills by removing them from the context. -Remember to always keep only the full `SKILL.md` into context for all Skills relevant to the current task. Use additional files as needed. \ No newline at end of file +- When the task is completed, unload irrelevant skills from the `loaded_skills` block. +IMPORTANT: Always remove irrelevant skills using memory management tools from the `loaded_skills` block. \ No newline at end of file diff --git a/src/cli/App.tsx b/src/cli/App.tsx index bbec37a..5b52ba0 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -96,6 +96,16 @@ function getPlanModeReminder(): string { return PLAN_MODE_REMINDER; } +// Get skill unload reminder if skills are loaded (using cached flag) +function getSkillUnloadReminder(): string { + const { hasLoadedSkills } = require("../agent/context"); + if (hasLoadedSkills()) { + const { SKILL_UNLOAD_REMINDER } = require("../agent/promptAssets"); + return SKILL_UNLOAD_REMINDER; + } + return ""; +} + // Items that have finished rendering and no longer change type StaticItem = | { @@ -1353,14 +1363,17 @@ export default function App({ // Prepend plan mode reminder if in plan mode const planModeReminder = getPlanModeReminder(); + + // Prepend skill unload reminder if skills are loaded (using cached flag) + const skillUnloadReminder = getSkillUnloadReminder(); + + // Combine reminders with content (plan mode first, then skill unload) + const allReminders = planModeReminder + skillUnloadReminder; const messageContent = - planModeReminder && typeof contentParts === "string" - ? planModeReminder + contentParts - : Array.isArray(contentParts) && planModeReminder - ? [ - { type: "text" as const, text: planModeReminder }, - ...contentParts, - ] + allReminders && typeof contentParts === "string" + ? allReminders + contentParts + : Array.isArray(contentParts) && allReminders + ? [{ type: "text" as const, text: allReminders }, ...contentParts] : contentParts; // Append the user message to transcript IMMEDIATELY (optimistic update) diff --git a/src/headless.ts b/src/headless.ts index 7924913..3342b98 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -8,6 +8,7 @@ import type { ApprovalCreate } from "@letta-ai/letta-client/resources/agents/mes import type { StopReasonType } from "@letta-ai/letta-client/resources/runs/runs"; import type { ApprovalResult } from "./agent/approval-execution"; import { getClient } from "./agent/client"; +import { initializeLoadedSkillsFlag, setAgentContext } from "./agent/context"; import { createAgent } from "./agent/create"; import { sendMessageStream } from "./agent/message"; import { getModelUpdateArgs } from "./agent/model"; @@ -155,6 +156,10 @@ export async function handleHeadlessCommand( settingsManager.updateLocalProjectSettings({ lastAgent: agent.id }); settingsManager.updateSettings({ lastAgent: agent.id }); + // Set agent context for tools that need it (e.g., Skill tool) + setAgentContext(agent.id, client, skillsDirectory); + await initializeLoadedSkillsFlag(); + // Validate output format const outputFormat = (values["output-format"] as string | undefined) || "text"; @@ -305,14 +310,26 @@ export async function handleHeadlessCommand( // Clear any pending approvals before starting a new turn await resolveAllPendingApprovals(); - // Get plan mode reminder if in plan mode + // Build message content with reminders (plan mode first, then skill unload) const { permissionMode } = await import("./permissions/mode"); - let messageContent = prompt; + const { hasLoadedSkills } = await import("./agent/context"); + let messageContent = ""; + + // Add plan mode reminder if in plan mode (highest priority) if (permissionMode.getMode() === "plan") { const { PLAN_MODE_REMINDER } = await import("./agent/promptAssets"); - messageContent = PLAN_MODE_REMINDER + prompt; + messageContent += PLAN_MODE_REMINDER; } + // Add skill unload reminder if skills are loaded (using cached flag) + if (hasLoadedSkills()) { + const { SKILL_UNLOAD_REMINDER } = await import("./agent/promptAssets"); + messageContent += SKILL_UNLOAD_REMINDER; + } + + // Add user prompt + messageContent += prompt; + // Start with the user message let currentInput: Array = [ { diff --git a/src/index.ts b/src/index.ts index 8efe00f..dc5ac8f 100755 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { parseArgs } from "node:util"; import type { AgentState } from "@letta-ai/letta-client/resources/agents/agents"; import { getResumeData, type ResumeData } from "./agent/check-approval"; import { getClient } from "./agent/client"; +import { initializeLoadedSkillsFlag, setAgentContext } from "./agent/context"; import { permissionMode } from "./permissions/mode"; import { settingsManager } from "./settings-manager"; import { loadTools, upsertToolsToServer } from "./tools/manager"; @@ -556,6 +557,10 @@ async function main() { settingsManager.updateLocalProjectSettings({ lastAgent: agent.id }); settingsManager.updateSettings({ lastAgent: agent.id }); + // Set agent context for tools that need it (e.g., Skill tool) + setAgentContext(agent.id, client, skillsDirectory); + await initializeLoadedSkillsFlag(); + // Check if we're resuming an existing agent const localProjectSettings = settingsManager.getLocalProjectSettings(); const isResumingProject = diff --git a/src/permissions/checker.ts b/src/permissions/checker.ts index 7f2a834..f37693a 100644 --- a/src/permissions/checker.ts +++ b/src/permissions/checker.ts @@ -102,6 +102,14 @@ export function checkPermission( } } + // Always allow Skill tool (read-only operation that loads skills from potentially external directories) + if (toolName === "Skill") { + return { + decision: "allow", + reason: "Skill tool is always allowed (read-only)", + }; + } + // After checking CLI overrides, check if Read/Glob/Grep within working directory if (WORKING_DIRECTORY_TOOLS.includes(toolName)) { const filePath = extractFilePath(toolArgs); diff --git a/src/tools/descriptions/Skill.md b/src/tools/descriptions/Skill.md new file mode 100644 index 0000000..3672686 --- /dev/null +++ b/src/tools/descriptions/Skill.md @@ -0,0 +1,29 @@ +# Skill + +Load a skill into the system prompt within the main conversation + + +When users ask you to perform tasks, check if any of the available skills can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge. + +How to use skills: +- Invoke skills using this tool with the skill name only (no arguments) +- When you invoke a skill, the SKILL.md content will be loaded into the `loaded_skills` memory block +- The skill's prompt will provide detailed instructions on how to complete the task +- Examples: + - `skill: "data-analysis"` - invoke the data-analysis skill + - `skill: "web-scraper"` - invoke the web-scraper skill + +Important: +- Only load skills that are available in the `skills` memory block +- Skills remain loaded until you unload them +- Unload skills when done to free up context space +- Do not invoke a skill that is already loaded +- You can check what skills are currently loaded in the `loaded_skills` memory block + + +Usage notes: +- The `skill` parameter is required and should be the skill ID (e.g., "data-analysis") +- Skills are loaded from the skills directory specified in the `skills` memory block +- Skills remain loaded in the `loaded_skills` memory block until explicitly unloaded +- Only use skill IDs that appear in the `skills` memory block +- Each skill provides specialized instructions and capabilities for specific tasks diff --git a/src/tools/impl/Skill.ts b/src/tools/impl/Skill.ts new file mode 100644 index 0000000..38af4c6 --- /dev/null +++ b/src/tools/impl/Skill.ts @@ -0,0 +1,136 @@ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { + getCurrentAgentId, + getCurrentClient, + getSkillsDirectory, + setHasLoadedSkills, +} from "../../agent/context"; +import { SKILLS_DIR } from "../../agent/skills"; +import { validateRequiredParams } from "./validation.js"; + +interface SkillArgs { + skill: string; +} + +interface SkillResult { + message: string; +} + +/** + * Parse loaded_skills block content to extract skill IDs + */ +function parseLoadedSkills(value: string): string[] { + const skillRegex = /# Skill: ([^\n]+)/g; + const skills: string[] = []; + let match: RegExpExecArray | null = skillRegex.exec(value); + + while (match !== null) { + const skillId = match[1]?.trim(); + if (skillId) { + skills.push(skillId); + } + match = skillRegex.exec(value); + } + + return skills; +} + +/** + * Extracts skills directory from skills block value + */ +function extractSkillsDir(skillsBlockValue: string): string | null { + const match = skillsBlockValue.match(/Skills Directory: (.+)/); + return match ? match[1]?.trim() || null : null; +} + +export async function skill(args: SkillArgs): Promise { + validateRequiredParams(args, ["skill"], "Skill"); + const { skill: skillId } = args; + + try { + // Get current agent context + const client = getCurrentClient(); + const agentId = getCurrentAgentId(); + + // Retrieve the loaded_skills block directly + let loadedSkillsBlock: Awaited< + ReturnType + >; + try { + loadedSkillsBlock = await client.agents.blocks.retrieve("loaded_skills", { + agent_id: agentId, + }); + } catch (error) { + throw new Error( + `Error: loaded_skills block not found. This block is required for the Skill tool to work.\nAgent ID: ${agentId}\nError: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + // Determine skills directory + let skillsDir = getSkillsDirectory(); + + if (!skillsDir) { + // Try to extract from skills block + try { + const skillsBlock = await client.agents.blocks.retrieve("skills", { + agent_id: agentId, + }); + if (skillsBlock?.value) { + skillsDir = extractSkillsDir(skillsBlock.value); + } + } catch { + // Skills block doesn't exist, will fall back to default + } + } + + if (!skillsDir) { + // Fall back to default .skills directory in cwd + skillsDir = join(process.cwd(), SKILLS_DIR); + } + + // Construct path to SKILL.md + const skillPath = join(skillsDir, skillId, "SKILL.md"); + + // Read the skill file directly + const skillContent = await readFile(skillPath, "utf-8"); + + // Parse current loaded_skills block value + let currentValue = loadedSkillsBlock.value?.trim() || ""; + const loadedSkills = parseLoadedSkills(currentValue); + + // Check if skill is already loaded + if (loadedSkills.includes(skillId)) { + return { + message: `Skill "${skillId}" is already loaded`, + }; + } + + // Replace placeholder if this is the first skill + if (currentValue === "[CURRENTLY EMPTY]") { + currentValue = ""; + } + + // Append new skill to loaded_skills block + const separator = currentValue ? "\n\n---\n\n" : ""; + const newValue = `${currentValue}${separator}# Skill: ${skillId}\n${skillContent}`; + + // Update the block + await client.agents.blocks.update("loaded_skills", { + agent_id: agentId, + value: newValue, + }); + + // Update the cached flag to indicate skills are loaded + setHasLoadedSkills(true); + + return { + message: `Skill "${skillId}" loaded successfully`, + }; + } catch (error) { + if (error instanceof Error) { + throw error; + } + throw new Error(`Failed to load skill: ${String(error)}`); + } +} diff --git a/src/tools/manager.ts b/src/tools/manager.ts index 5c80d53..d60e672 100644 --- a/src/tools/manager.ts +++ b/src/tools/manager.ts @@ -57,6 +57,7 @@ export const ANTHROPIC_DEFAULT_TOOLS: ToolName[] = [ "LS", "MultiEdit", "Read", + "Skill", "TodoWrite", "Write", ]; @@ -69,6 +70,7 @@ export const OPENAI_DEFAULT_TOOLS: ToolName[] = [ "grep_files", "apply_patch", "update_plan", + "Skill", ]; export const GEMINI_DEFAULT_TOOLS: ToolName[] = [ @@ -81,6 +83,7 @@ export const GEMINI_DEFAULT_TOOLS: ToolName[] = [ "write_file_gemini", "write_todos", "read_many_files", + "Skill", ]; // Tool permissions configuration @@ -95,6 +98,7 @@ const TOOL_PERMISSIONS: Record = { LS: { requiresApproval: false }, MultiEdit: { requiresApproval: true }, Read: { requiresApproval: false }, + Skill: { requiresApproval: false }, TodoWrite: { requiresApproval: false }, Write: { requiresApproval: true }, shell_command: { requiresApproval: true }, diff --git a/src/tools/schemas/Skill.json b/src/tools/schemas/Skill.json new file mode 100644 index 0000000..263d1d2 --- /dev/null +++ b/src/tools/schemas/Skill.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "properties": { + "skill": { + "type": "string", + "description": "The skill name/id (e.g., \"data-analysis\", \"web-scraper\")" + } + }, + "required": ["skill"], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" +} diff --git a/src/tools/toolDefinitions.ts b/src/tools/toolDefinitions.ts index 7fa433f..0aadde9 100644 --- a/src/tools/toolDefinitions.ts +++ b/src/tools/toolDefinitions.ts @@ -22,6 +22,7 @@ import RunShellCommandGeminiDescription from "./descriptions/RunShellCommandGemi import SearchFileContentGeminiDescription from "./descriptions/SearchFileContentGemini.md"; import ShellDescription from "./descriptions/Shell.md"; import ShellCommandDescription from "./descriptions/ShellCommand.md"; +import SkillDescription from "./descriptions/Skill.md"; import TodoWriteDescription from "./descriptions/TodoWrite.md"; import UpdatePlanDescription from "./descriptions/UpdatePlan.md"; import WriteDescription from "./descriptions/Write.md"; @@ -51,6 +52,7 @@ import { run_shell_command } from "./impl/RunShellCommandGemini"; import { search_file_content } from "./impl/SearchFileContentGemini"; import { shell } from "./impl/Shell"; import { shell_command } from "./impl/ShellCommand"; +import { skill } from "./impl/Skill"; import { todo_write } from "./impl/TodoWrite"; import { update_plan } from "./impl/UpdatePlan"; import { write } from "./impl/Write"; @@ -80,6 +82,7 @@ import RunShellCommandGeminiSchema from "./schemas/RunShellCommandGemini.json"; import SearchFileContentGeminiSchema from "./schemas/SearchFileContentGemini.json"; import ShellSchema from "./schemas/Shell.json"; import ShellCommandSchema from "./schemas/ShellCommand.json"; +import SkillSchema from "./schemas/Skill.json"; import TodoWriteSchema from "./schemas/TodoWrite.json"; import UpdatePlanSchema from "./schemas/UpdatePlan.json"; import WriteSchema from "./schemas/Write.json"; @@ -145,6 +148,11 @@ const toolDefinitions = { description: ReadDescription.trim(), impl: read as unknown as ToolImplementation, }, + Skill: { + schema: SkillSchema, + description: SkillDescription.trim(), + impl: skill as unknown as ToolImplementation, + }, TodoWrite: { schema: TodoWriteSchema, description: TodoWriteDescription.trim(),