feat: 添加 skill learning 技能学习闭环系统

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
unraid
2026-04-22 22:38:09 +08:00
parent 04c7ed4250
commit 1837df5f88
64 changed files with 11009 additions and 36 deletions

View File

@@ -0,0 +1,107 @@
import { z } from 'zod/v4'
import type { ToolResultBlockParam } from 'src/Tool.js'
import { buildTool } from 'src/Tool.js'
import { lazySchema } from 'src/utils/lazySchema.js'
import {
DISCOVER_SKILLS_TOOL_NAME,
DESCRIPTION,
DISCOVER_SKILLS_PROMPT,
} from './prompt.js'
const inputSchema = lazySchema(() =>
z.strictObject({
description: z
.string()
.describe(
'Description of what you want to do. Be specific — e.g. "deploy a Next.js app to Cloudflare Workers" rather than just "deploy".',
),
limit: z
.number()
.optional()
.describe('Maximum number of results to return (default: 5)'),
}),
)
type InputSchema = ReturnType<typeof inputSchema>
type DiscoverInput = z.infer<InputSchema>
type DiscoverOutput = {
results: Array<{ name: string; description: string; score: number }>
count: number
}
export const DiscoverSkillsTool = buildTool({
name: DISCOVER_SKILLS_TOOL_NAME,
searchHint: 'find search discover skills commands tools capabilities',
maxResultSizeChars: 10_000,
strict: true,
get inputSchema(): InputSchema {
return inputSchema()
},
async description() {
return DESCRIPTION
},
async prompt() {
return DISCOVER_SKILLS_PROMPT
},
isConcurrencySafe() {
return true
},
isReadOnly() {
return true
},
userFacingName() {
return 'Discover Skills'
},
renderToolUseMessage(input: Partial<DiscoverInput>) {
return `Searching skills: ${input.description?.slice(0, 80) ?? '...'}`
},
mapToolResultToToolResultBlockParam(
content: DiscoverOutput,
toolUseID: string,
): ToolResultBlockParam {
if (content.count === 0) {
return {
tool_use_id: toolUseID,
type: 'tool_result',
content: 'No matching skills found for that description.',
}
}
const lines = content.results.map(
(r, i) =>
`${i + 1}. **${r.name}** (score: ${r.score.toFixed(2)})\n ${r.description}`,
)
return {
tool_use_id: toolUseID,
type: 'tool_result',
content: `Found ${content.count} relevant skill(s):\n\n${lines.join('\n\n')}`,
}
},
async call(input: DiscoverInput, context) {
const { getSkillIndex, searchSkills } = await import(
'src/services/skillSearch/localSearch.js'
)
const { getCwd } = await import('src/utils/cwd.js')
const cwd = getCwd()
const index = await getSkillIndex(cwd)
const results = searchSkills(input.description, index, input.limit ?? 5)
return {
data: {
results: results.map(r => ({
name: r.name,
description: r.description,
score: r.score,
})),
count: results.length,
},
}
},
})

View File

@@ -0,0 +1,54 @@
import { describe, test, expect } from 'bun:test'
import { DISCOVER_SKILLS_TOOL_NAME } from '../prompt.js'
describe('DiscoverSkillsTool', () => {
test('DISCOVER_SKILLS_TOOL_NAME is not empty', () => {
expect(DISCOVER_SKILLS_TOOL_NAME).toBe('DiscoverSkills')
expect(DISCOVER_SKILLS_TOOL_NAME.length).toBeGreaterThan(0)
})
test('tool exports are functions', async () => {
const { DiscoverSkillsTool } = await import('../DiscoverSkillsTool.js')
expect(DiscoverSkillsTool).toBeDefined()
expect(DiscoverSkillsTool.name).toBe('DiscoverSkills')
expect(typeof DiscoverSkillsTool.call).toBe('function')
})
test('tool has correct metadata', async () => {
const { DiscoverSkillsTool } = await import('../DiscoverSkillsTool.js')
expect(await DiscoverSkillsTool.description()).toContain('skill')
expect(DiscoverSkillsTool.userFacingName()).toBe('Discover Skills')
expect(DiscoverSkillsTool.isReadOnly()).toBe(true)
expect(DiscoverSkillsTool.isConcurrencySafe()).toBe(true)
})
test('renderToolUseMessage formats input', async () => {
const { DiscoverSkillsTool } = await import('../DiscoverSkillsTool.js')
const msg = DiscoverSkillsTool.renderToolUseMessage({
description: 'deploy to cloudflare',
})
expect(msg).toContain('deploy to cloudflare')
})
test('mapToolResultToToolResultBlockParam formats empty results', async () => {
const { DiscoverSkillsTool } = await import('../DiscoverSkillsTool.js')
const result = DiscoverSkillsTool.mapToolResultToToolResultBlockParam(
{ results: [], count: 0 },
'test-id',
)
expect(result.content).toContain('No matching skills')
})
test('mapToolResultToToolResultBlockParam formats results', async () => {
const { DiscoverSkillsTool } = await import('../DiscoverSkillsTool.js')
const result = DiscoverSkillsTool.mapToolResultToToolResultBlockParam(
{
results: [{ name: 'test-skill', description: 'A test skill', score: 0.85 }],
count: 1,
},
'test-id',
)
expect(result.content).toContain('test-skill')
expect(result.content).toContain('0.85')
})
})

View File

@@ -1,3 +1,13 @@
// Auto-generated stub — replace with real implementation
export {};
export const DISCOVER_SKILLS_TOOL_NAME: string = '';
export const DISCOVER_SKILLS_TOOL_NAME = 'DiscoverSkills'
export const DESCRIPTION =
'Search for relevant skills by describing what you want to do'
export const DISCOVER_SKILLS_PROMPT = `Search for skills relevant to a task description. Returns matching skills ranked by relevance.
Use this when:
- The auto-surfaced skills don't cover your current task
- You're pivoting to a different kind of work mid-conversation
- You want to find specialized skills for an unusual workflow
The search uses TF-IDF keyword matching against all registered skills (bundled, user-defined, and MCP-provided). Results include skill name, description, and relevance score.`