Skip to content

Commit 4345a87

Browse files
committed
Allow loading skills dynamically
1 parent 44ef1c1 commit 4345a87

File tree

2 files changed

+89
-4
lines changed
  • common/src/tools/params/tool
  • packages/agent-runtime/src/tools/handlers/tool

2 files changed

+89
-4
lines changed

common/src/tools/params/tool/skill.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ export const AVAILABLE_SKILLS_PLACEHOLDER = '{{AVAILABLE_SKILLS}}'
3434
// Base description - the full description with available skills is generated dynamically
3535
const baseDescription = `Load a skill by name to get its full instructions. Skills provide reusable behaviors and domain-specific knowledge that you can use to complete tasks.
3636
37-
The following are the only skills that are currently available (do not try to use any other skills):
37+
The following are the pre-loaded skills available at session start:
3838
${AVAILABLE_SKILLS_PLACEHOLDER}
3939
40+
Note: You can also load any skill that was created during this session by specifying its name. The skill will be loaded dynamically from disk.
41+
4042
Example:
4143
${$getNativeToolCallExampleString({
4244
toolName,

packages/agent-runtime/src/tools/handlers/tool/skill.ts

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import { jsonToolResult } from '@codebuff/common/util/messages'
2+
import { SKILLS_DIR_NAME, SKILL_FILE_NAME } from '@codebuff/common/constants/skills'
3+
import { SkillFrontmatterSchema, type SkillDefinition } from '@codebuff/common/types/skill'
4+
import fs from 'fs'
5+
import path from 'path'
6+
import os from 'os'
7+
import matter from 'gray-matter'
28

39
import type { CodebuffToolHandlerFunction } from '../handler-function-type'
410
import type {
@@ -7,6 +13,73 @@ import type {
713
} from '@codebuff/common/tools/list'
814
import type { ProjectFileContext } from '@codebuff/common/util/file'
915

16+
/**
17+
* Dynamically load a single skill from disk.
18+
* Used when a skill is not found in the pre-loaded cache but may have been created during the session.
19+
*/
20+
async function loadSkillFromDisk(
21+
projectRoot: string,
22+
skillName: string,
23+
): Promise<SkillDefinition | null> {
24+
const home = os.homedir()
25+
const skillsDirs = [
26+
// Global directories first
27+
path.join(home, '.agents', SKILLS_DIR_NAME),
28+
path.join(home, '.claude', SKILLS_DIR_NAME),
29+
// Project directories (later takes precedence for overwriting)
30+
path.join(projectRoot, '.agents', SKILLS_DIR_NAME),
31+
path.join(projectRoot, '.claude', SKILLS_DIR_NAME),
32+
]
33+
34+
for (const skillsDir of skillsDirs) {
35+
const skillDir = path.join(skillsDir, skillName)
36+
const skillFilePath = path.join(skillDir, SKILL_FILE_NAME)
37+
38+
try {
39+
// Check if the skill directory and file exist
40+
const stat = fs.statSync(skillDir)
41+
if (!stat.isDirectory()) continue
42+
43+
fs.statSync(skillFilePath) // Will throw if file doesn't exist
44+
45+
// Read and parse the skill file
46+
const content = fs.readFileSync(skillFilePath, 'utf8')
47+
const parsed = matter(content)
48+
49+
if (!parsed.data || Object.keys(parsed.data).length === 0) {
50+
continue
51+
}
52+
53+
// Validate frontmatter
54+
const result = SkillFrontmatterSchema.safeParse(parsed.data)
55+
if (!result.success) {
56+
continue
57+
}
58+
59+
const frontmatter = result.data
60+
61+
// Verify name matches directory name
62+
if (frontmatter.name !== skillName) {
63+
continue
64+
}
65+
66+
return {
67+
name: frontmatter.name,
68+
description: frontmatter.description,
69+
content,
70+
license: frontmatter.license,
71+
filePath: skillFilePath,
72+
metadata: frontmatter.metadata,
73+
}
74+
} catch {
75+
// Skill doesn't exist in this directory, try the next one
76+
continue
77+
}
78+
}
79+
80+
return null
81+
}
82+
1083
type ToolName = 'skill'
1184

1285
export const handleSkill = (async (params: {
@@ -20,14 +93,24 @@ export const handleSkill = (async (params: {
2093
await previousToolCallFinished
2194

2295
const skills = fileContext.skills ?? {}
23-
const skill = skills[name]
96+
const cachedSkill = skills[name]
97+
98+
// If skill not in cache, try to load it dynamically from disk
99+
// This supports skills created during the session
100+
const diskSkill = cachedSkill
101+
? null
102+
: fileContext.projectRoot
103+
? await loadSkillFromDisk(fileContext.projectRoot, name)
104+
: null
105+
106+
const skill = cachedSkill ?? diskSkill
24107

25108
if (!skill) {
26109
const availableSkills = Object.keys(skills)
27110
const suggestion =
28111
availableSkills.length > 0
29-
? ` Available skills: ${availableSkills.join(', ')}`
30-
: ' No skills are currently available.'
112+
? ` Available skills: ${availableSkills.join(', ')}. You can also load skills created during this session by name.`
113+
: ' No skills are currently available. You can load skills created during this session by name.'
31114

32115
return {
33116
output: jsonToolResult({

0 commit comments

Comments
 (0)