feat: update Skill tool to support load/unload commands (#219)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -16,6 +16,12 @@ export const PROJECT_BLOCK_LABELS = [
|
||||
"loaded_skills",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Block labels that should be read-only (agent cannot modify via memory tools).
|
||||
* These blocks are managed by specific tools (e.g., Skill tool for skills/loaded_skills).
|
||||
*/
|
||||
export const READ_ONLY_BLOCK_LABELS = ["skills", "loaded_skills"] as const;
|
||||
|
||||
/**
|
||||
* Check if a block label is a project-level block
|
||||
*/
|
||||
@@ -77,8 +83,9 @@ async function loadMemoryBlocksFromMdx(): Promise<CreateBlock[]> {
|
||||
}
|
||||
const { frontmatter, body } = parseMdxFrontmatter(content);
|
||||
|
||||
const label = frontmatter.label || filename.replace(".mdx", "");
|
||||
const block: CreateBlock = {
|
||||
label: frontmatter.label || filename.replace(".mdx", ""),
|
||||
label,
|
||||
value: body,
|
||||
};
|
||||
|
||||
@@ -86,6 +93,11 @@ async function loadMemoryBlocksFromMdx(): Promise<CreateBlock[]> {
|
||||
block.description = frontmatter.description;
|
||||
}
|
||||
|
||||
// Set read-only for skills blocks (managed by Skill tool, not memory tools)
|
||||
if ((READ_ONLY_BLOCK_LABELS as readonly string[]).includes(label)) {
|
||||
block.read_only = true;
|
||||
}
|
||||
|
||||
memoryBlocks.push(block);
|
||||
} catch (error) {
|
||||
console.error(`Error loading ${filename}:`, error);
|
||||
|
||||
@@ -154,7 +154,8 @@ How to store Skills:
|
||||
How to use Skills:
|
||||
- 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.
|
||||
- If any skill is relevant, load it using the `Skill` tool with `command: "load"`.
|
||||
- Then, navigate and discover additional linked files in its directory as needed. Don't load additional files immediately, only load them when needed.
|
||||
- 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.
|
||||
- When the task is completed, unload irrelevant skills using the Skill tool with `command: "unload"`.
|
||||
- After creating a new skill, use `command: "refresh"` to re-scan the skills directory and update the available skills list.
|
||||
IMPORTANT: Always unload irrelevant skills using the Skill tool to free up context space.
|
||||
@@ -139,7 +139,8 @@ How to store Skills:
|
||||
How to use Skills:
|
||||
- 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.
|
||||
- If any skill is relevant, load it using the `Skill` tool with `command: "load"`.
|
||||
- Then, navigate and discover additional linked files in its directory as needed. Don't load additional files immediately, only load them when needed.
|
||||
- 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.
|
||||
- When the task is completed, unload irrelevant skills using the Skill tool with `command: "unload"`.
|
||||
- After creating a new skill, use `command: "refresh"` to re-scan the skills directory and update the available skills list.
|
||||
IMPORTANT: Always unload irrelevant skills using the Skill tool to free up context space.
|
||||
@@ -100,7 +100,8 @@ How to store Skills:
|
||||
How to use Skills:
|
||||
- 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.
|
||||
- If any skill is relevant, load it using the `Skill` tool with `command: "load"`.
|
||||
- Then, navigate and discover additional linked files in its directory as needed. Don't load additional files immediately, only load them when needed.
|
||||
- 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.
|
||||
- When the task is completed, unload irrelevant skills using the Skill tool with `command: "unload"`.
|
||||
- After creating a new skill, use `command: "refresh"` to re-scan the skills directory and update the available skills list.
|
||||
IMPORTANT: Always unload irrelevant skills using the Skill tool to free up context space.
|
||||
@@ -1,6 +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.
|
||||
description: A memory block to store the full instructions and capabilities from each loaded SKILL.md file in this block. Do not manually edit this block - use the Skill tool to load and unload skills.
|
||||
---
|
||||
|
||||
[CURRENTLY EMPTY]
|
||||
[CURRENTLY EMPTY]
|
||||
|
||||
@@ -16,7 +16,7 @@ Your goal is to guide the user through a **focused, collaborative workflow** to
|
||||
- `loaded_skills` – SKILL.md contents for currently loaded skills
|
||||
2. If a `skill-creator` skill is **not already loaded** in `loaded_skills`, you should **attempt to load it** using the `Skill` tool:
|
||||
- Call the `Skill` tool with:
|
||||
- `skill: "skill-creator"`
|
||||
- `command: "load", skills: ["skill-creator"]`
|
||||
- The environment may resolve this from either the project’s `.skills` directory or a bundled `skills/skills/skill-creator/SKILL.md` location.
|
||||
3. If loading `skill-creator` fails (for example, the tool errors or the file is missing), or if the environment does not provide it, continue using your own judgment based on these instructions.
|
||||
|
||||
@@ -118,6 +118,5 @@ Your goal is to:
|
||||
1. Understand the desired skill thoroughly.
|
||||
2. Propose a clear, reusable design.
|
||||
3. Implement or update the actual skill files in the repository.
|
||||
4. Leave the user with a ready‑to‑use skill that appears in the `skills` memory block and can be loaded with the `Skill` tool.
|
||||
|
||||
|
||||
4. After creating or updating the skill files, use the `Skill` tool with `command: "refresh"` to re-scan the skills directory and update the `skills` memory block.
|
||||
5. Leave the user with a ready‑to‑use skill that appears in the `skills` memory block and can be loaded with the `Skill` tool.
|
||||
@@ -1,6 +1,5 @@
|
||||
<system-reminder>
|
||||
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.
|
||||
2. For any skills that are irrelevant, unload them using the `Skill` tool with `command: "unload"`.
|
||||
</system-reminder>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
label: skills
|
||||
description: A memory block to store all available Skills with their metadata (name and description). Whenever a new Skill is discovered / created or an existing Skill is updated, I should store it here. I should always check the `.skills` directory for an updated skill list.
|
||||
description: A memory block listing all available skills with their metadata (name and description). This block is auto-generated based on the `.skills` directory. Do not manually edit this block.
|
||||
---
|
||||
|
||||
[CURRENTLY EMPTY: CREATE A LIST OF AVAILABLE SKILLS (DIRECTORIES WITH SKILL.MD FILE) WITH DESCRIPTIONS IN THIS MEMORY BLOCK. ALWAYS CHECK THE `.skills` DIRECTORY TO MAKE SURE YOU HAVE AN UPDATED LIST OF SKILLS]
|
||||
[CURRENTLY EMPTY]
|
||||
|
||||
@@ -35,7 +35,8 @@ How to store Skills:
|
||||
How to use Skills:
|
||||
- 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.
|
||||
- If any skill is relevant, load it using the `Skill` tool with `command: "load"`.
|
||||
- Then, navigate and discover additional linked files in its directory as needed. Don't load additional files immediately, only load them when needed.
|
||||
- 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.
|
||||
- When the task is completed, unload irrelevant skills using the Skill tool with `command: "unload"`.
|
||||
- After creating a new skill, use `command: "refresh"` to re-scan the skills directory and update the available skills list.
|
||||
IMPORTANT: Always unload irrelevant skills using the Skill tool to free up context space.
|
||||
@@ -1,28 +1,35 @@
|
||||
# Skill
|
||||
|
||||
Load a skill into the system prompt within the main conversation
|
||||
Load or unload skills into the agent's memory.
|
||||
|
||||
<skills_instructions>
|
||||
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
|
||||
- Use `command: "load"` with a list of skill IDs to load skills
|
||||
- Use `command: "unload"` with a list of skill IDs to unload skills
|
||||
- Use `command: "refresh"` to re-scan the skills directory and update the available skills list
|
||||
- When you load a skill, the SKILL.md content will be added to 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
|
||||
- `command: "load", skills: ["data-analysis"]` - load the data-analysis skill
|
||||
- `command: "load", skills: ["web-scraper", "pdf"]` - load multiple skills
|
||||
- `command: "unload", skills: ["data-analysis"]` - unload the data-analysis skill
|
||||
- `command: "refresh"` - re-scan and update available skills list
|
||||
|
||||
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
|
||||
- Loading an already-loaded skill will be skipped (no error)
|
||||
- Unloading a not-loaded skill will be skipped (no error)
|
||||
- Use `refresh` after creating a new skill to make it available for loading
|
||||
</skills_instructions>
|
||||
|
||||
Usage notes:
|
||||
- The `skill` parameter is required and should be the skill ID (e.g., "data-analysis")
|
||||
- The `command` parameter is required: either "load", "unload", or "refresh"
|
||||
- The `skills` parameter is required for load/unload: an array of skill IDs to load or unload
|
||||
- The `skills` parameter is not used for refresh
|
||||
- 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
|
||||
|
||||
@@ -6,11 +6,16 @@ import {
|
||||
getSkillsDirectory,
|
||||
setHasLoadedSkills,
|
||||
} from "../../agent/context";
|
||||
import { SKILLS_DIR } from "../../agent/skills";
|
||||
import {
|
||||
discoverSkills,
|
||||
formatSkillsForMemory,
|
||||
SKILLS_DIR,
|
||||
} from "../../agent/skills";
|
||||
import { validateRequiredParams } from "./validation.js";
|
||||
|
||||
interface SkillArgs {
|
||||
skill: string;
|
||||
command: "load" | "unload" | "refresh";
|
||||
skills?: string[];
|
||||
}
|
||||
|
||||
interface SkillResult {
|
||||
@@ -18,13 +23,63 @@ interface SkillResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse loaded_skills block content to extract skill IDs
|
||||
* Parse loaded_skills block content to extract skill IDs and their content boundaries
|
||||
*/
|
||||
function parseLoadedSkills(value: string): string[] {
|
||||
function parseLoadedSkills(
|
||||
value: string,
|
||||
): Map<string, { start: number; end: number }> {
|
||||
const skillMap = new Map<string, { start: number; end: number }>();
|
||||
const skillHeaderRegex = /# Skill: ([^\n]+)/g;
|
||||
|
||||
const headers: { id: string; start: number }[] = [];
|
||||
|
||||
// Find all skill headers
|
||||
let match = skillHeaderRegex.exec(value);
|
||||
while (match !== null) {
|
||||
const skillId = match[1]?.trim();
|
||||
if (skillId) {
|
||||
headers.push({ id: skillId, start: match.index });
|
||||
}
|
||||
match = skillHeaderRegex.exec(value);
|
||||
}
|
||||
|
||||
// Determine boundaries for each skill
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const current = headers[i];
|
||||
const next = headers[i + 1];
|
||||
|
||||
if (!current) continue;
|
||||
|
||||
let end: number;
|
||||
if (next) {
|
||||
// Find the separator before the next skill
|
||||
const searchStart = current.start;
|
||||
const searchEnd = next.start;
|
||||
const substring = value.substring(searchStart, searchEnd);
|
||||
const sepMatch = substring.lastIndexOf("\n\n---\n\n");
|
||||
if (sepMatch !== -1) {
|
||||
end = searchStart + sepMatch;
|
||||
} else {
|
||||
end = searchEnd;
|
||||
}
|
||||
} else {
|
||||
end = value.length;
|
||||
}
|
||||
|
||||
skillMap.set(current.id, { start: current.start, end });
|
||||
}
|
||||
|
||||
return skillMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of loaded skill IDs
|
||||
*/
|
||||
function getLoadedSkillIds(value: string): string[] {
|
||||
const skillRegex = /# Skill: ([^\n]+)/g;
|
||||
const skills: string[] = [];
|
||||
let match: RegExpExecArray | null = skillRegex.exec(value);
|
||||
|
||||
let match = skillRegex.exec(value);
|
||||
while (match !== null) {
|
||||
const skillId = match[1]?.trim();
|
||||
if (skillId) {
|
||||
@@ -44,16 +99,115 @@ function extractSkillsDir(skillsBlockValue: string): string | null {
|
||||
return match ? match[1]?.trim() || null : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read skill content from file
|
||||
*/
|
||||
async function readSkillContent(
|
||||
skillId: string,
|
||||
skillsDir: string,
|
||||
): Promise<string> {
|
||||
// Try primary skills directory
|
||||
const skillPath = join(skillsDir, skillId, "SKILL.md");
|
||||
|
||||
try {
|
||||
return await readFile(skillPath, "utf-8");
|
||||
} catch (primaryError) {
|
||||
// Fallback: check for bundled skills in a repo-level skills directory
|
||||
try {
|
||||
const bundledSkillsDir = join(process.cwd(), "skills", "skills");
|
||||
const bundledSkillPath = join(bundledSkillsDir, skillId, "SKILL.md");
|
||||
return await readFile(bundledSkillPath, "utf-8");
|
||||
} catch {
|
||||
// If bundled fallback also fails, rethrow the original error
|
||||
throw primaryError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get skills directory, trying multiple sources
|
||||
*/
|
||||
async function getResolvedSkillsDir(
|
||||
client: ReturnType<typeof getCurrentClient>,
|
||||
agentId: string,
|
||||
): Promise<string> {
|
||||
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);
|
||||
}
|
||||
|
||||
return skillsDir;
|
||||
}
|
||||
|
||||
export async function skill(args: SkillArgs): Promise<SkillResult> {
|
||||
validateRequiredParams(args, ["skill"], "Skill");
|
||||
const { skill: skillId } = args;
|
||||
validateRequiredParams(args, ["command"], "Skill");
|
||||
const { command, skills: skillIds } = args;
|
||||
|
||||
if (command !== "load" && command !== "unload" && command !== "refresh") {
|
||||
throw new Error(
|
||||
`Invalid command "${command}". Must be "load", "unload", or "refresh".`,
|
||||
);
|
||||
}
|
||||
|
||||
// For load/unload, skills array is required
|
||||
if (command !== "refresh") {
|
||||
if (!Array.isArray(skillIds) || skillIds.length === 0) {
|
||||
throw new Error(
|
||||
`Skill tool requires a non-empty 'skills' array for "${command}" command`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Get current agent context
|
||||
const client = getCurrentClient();
|
||||
const agentId = getCurrentAgentId();
|
||||
|
||||
// Retrieve the loaded_skills block directly
|
||||
// Handle refresh command
|
||||
if (command === "refresh") {
|
||||
const skillsDir = await getResolvedSkillsDir(client, agentId);
|
||||
|
||||
// Discover skills from directory
|
||||
const { skills, errors } = await discoverSkills(skillsDir);
|
||||
|
||||
// Log any errors
|
||||
if (errors.length > 0) {
|
||||
for (const error of errors) {
|
||||
console.warn(
|
||||
`Skill discovery error: ${error.path}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Format and update the skills block
|
||||
const formattedSkills = formatSkillsForMemory(skills, skillsDir);
|
||||
await client.agents.blocks.update("skills", {
|
||||
agent_id: agentId,
|
||||
value: formattedSkills,
|
||||
});
|
||||
|
||||
return {
|
||||
message: `Refreshed skills list: found ${skills.length} skill(s)${errors.length > 0 ? `, ${errors.length} error(s)` : ""}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Retrieve the loaded_skills block for load/unload
|
||||
let loadedSkillsBlock: Awaited<
|
||||
ReturnType<typeof client.agents.blocks.retrieve>
|
||||
>;
|
||||
@@ -67,86 +221,125 @@ export async function skill(args: SkillArgs): Promise<SkillResult> {
|
||||
);
|
||||
}
|
||||
|
||||
// Determine skills directory
|
||||
let skillsDir = getSkillsDirectory();
|
||||
const skillsDir = await getResolvedSkillsDir(client, agentId);
|
||||
|
||||
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 in the primary skills directory
|
||||
let skillPath = join(skillsDir, skillId, "SKILL.md");
|
||||
|
||||
// Read the skill file directly, with a fallback to bundled skills if not found
|
||||
let skillContent: string;
|
||||
try {
|
||||
skillContent = await readFile(skillPath, "utf-8");
|
||||
} catch (primaryError) {
|
||||
// Fallback: check for bundled skills in a repo-level skills directory
|
||||
try {
|
||||
const bundledSkillsDir = join(process.cwd(), "skills", "skills");
|
||||
const bundledSkillPath = join(bundledSkillsDir, skillId, "SKILL.md");
|
||||
skillContent = await readFile(bundledSkillPath, "utf-8");
|
||||
// Update path and directory to reflect bundled location for this invocation
|
||||
skillsDir = bundledSkillsDir;
|
||||
skillPath = bundledSkillPath;
|
||||
} catch {
|
||||
// If bundled fallback also fails, rethrow the original error
|
||||
throw primaryError;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse current loaded_skills block value
|
||||
let currentValue = loadedSkillsBlock.value?.trim() || "";
|
||||
const loadedSkills = parseLoadedSkills(currentValue);
|
||||
const loadedSkillIds = getLoadedSkillIds(currentValue);
|
||||
const results: string[] = [];
|
||||
|
||||
// Check if skill is already loaded
|
||||
if (loadedSkills.includes(skillId)) {
|
||||
return {
|
||||
message: `Skill "${skillId}" is already loaded`,
|
||||
};
|
||||
// skillIds is guaranteed to be non-empty for load/unload (validated above)
|
||||
const skillsToProcess = skillIds as string[];
|
||||
|
||||
if (command === "load") {
|
||||
// Load skills
|
||||
for (const skillId of skillsToProcess) {
|
||||
if (loadedSkillIds.includes(skillId)) {
|
||||
results.push(`"${skillId}" already loaded`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const skillContent = await readSkillContent(skillId, skillsDir);
|
||||
|
||||
// Replace placeholder if this is the first skill
|
||||
if (currentValue === "[CURRENTLY EMPTY]") {
|
||||
currentValue = "";
|
||||
}
|
||||
|
||||
// Append new skill
|
||||
const separator = currentValue ? "\n\n---\n\n" : "";
|
||||
currentValue = `${currentValue}${separator}# Skill: ${skillId}\n${skillContent}`;
|
||||
loadedSkillIds.push(skillId);
|
||||
results.push(`"${skillId}" loaded`);
|
||||
} catch (error) {
|
||||
results.push(
|
||||
`"${skillId}" failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the block
|
||||
await client.agents.blocks.update("loaded_skills", {
|
||||
agent_id: agentId,
|
||||
value: currentValue,
|
||||
});
|
||||
|
||||
// Update the cached flag
|
||||
if (loadedSkillIds.length > 0) {
|
||||
setHasLoadedSkills(true);
|
||||
}
|
||||
} else {
|
||||
// Unload skills
|
||||
const skillBoundaries = parseLoadedSkills(currentValue);
|
||||
|
||||
// Sort skills to unload by their position (descending) so we can remove from end first
|
||||
const sortedSkillsToUnload = skillsToProcess
|
||||
.filter((id) => skillBoundaries.has(id))
|
||||
.sort((a, b) => {
|
||||
const boundaryA = skillBoundaries.get(a);
|
||||
const boundaryB = skillBoundaries.get(b);
|
||||
return (boundaryB?.start || 0) - (boundaryA?.start || 0);
|
||||
});
|
||||
|
||||
for (const skillId of skillsToProcess) {
|
||||
if (!loadedSkillIds.includes(skillId)) {
|
||||
results.push(`"${skillId}" not loaded`);
|
||||
continue;
|
||||
}
|
||||
results.push(`"${skillId}" unloaded`);
|
||||
}
|
||||
|
||||
// Remove skills from content (in reverse order to maintain indices)
|
||||
for (const skillId of sortedSkillsToUnload) {
|
||||
const boundary = skillBoundaries.get(skillId);
|
||||
if (boundary) {
|
||||
// Check if there's a separator before this skill
|
||||
const beforeStart = boundary.start;
|
||||
let actualStart = beforeStart;
|
||||
|
||||
// Look for preceding separator
|
||||
const precedingSep = "\n\n---\n\n";
|
||||
if (beforeStart >= precedingSep.length) {
|
||||
const potentialSep = currentValue.substring(
|
||||
beforeStart - precedingSep.length,
|
||||
beforeStart,
|
||||
);
|
||||
if (potentialSep === precedingSep) {
|
||||
actualStart = beforeStart - precedingSep.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the skill content
|
||||
currentValue =
|
||||
currentValue.substring(0, actualStart) +
|
||||
currentValue.substring(boundary.end);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the value
|
||||
currentValue = currentValue.trim();
|
||||
if (currentValue === "") {
|
||||
currentValue = "[CURRENTLY EMPTY]";
|
||||
}
|
||||
|
||||
// Update the block
|
||||
await client.agents.blocks.update("loaded_skills", {
|
||||
agent_id: agentId,
|
||||
value: currentValue,
|
||||
});
|
||||
|
||||
// Update the cached flag
|
||||
const remainingSkills = getLoadedSkillIds(currentValue);
|
||||
setHasLoadedSkills(remainingSkills.length > 0);
|
||||
}
|
||||
|
||||
// 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`,
|
||||
message: results.join(", "),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Failed to load skill: ${String(error)}`);
|
||||
throw new Error(`Failed to ${command} skill(s): ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"skill": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The skill name/id (e.g., \"data-analysis\", \"web-scraper\")"
|
||||
"enum": ["load", "unload", "refresh"],
|
||||
"description": "The operation to perform: \"load\" to load skills, \"unload\" to unload skills, \"refresh\" to re-scan the skills directory and update the available skills list"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of skill IDs to load or unload (required for load/unload, not used for refresh)"
|
||||
}
|
||||
},
|
||||
"required": ["skill"],
|
||||
"required": ["command"],
|
||||
"additionalProperties": false,
|
||||
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user