refactor: split skill providers

This commit is contained in:
Yichen Jiang
2026-07-08 15:50:38 +08:00
parent 5fd647012e
commit f453ba77a2
77 changed files with 1673 additions and 1334 deletions

View File

@@ -10,6 +10,6 @@ Requires `ctx.tools` and `ctx.skills` (`inject: ['tools', 'skills']`).
|---|---|---|
| `name` | string (required) | Exact kebab-case skill name from the available skills listing. |
Execution uses the calling agent's `session.header.cwd` to resolve project-local skills. A successful call returns a text block containing `<skill_content name="...">`, the skill body, the skill base directory, and relative-path guidance. Unknown names, invalid names, and skills marked `disableModelInvocation: true` return `isError` tool results through the normal tool registry error path.
Execution uses the calling agent's `session.header.cwd` so workspace-sensitive providers can resolve the right winning skill. A successful call returns a text block containing `<skill_content name="...">`, the skill body, and provider resource guidance. Local filesystem skills include a base directory for resolving relative files; remote or embedded providers can return URL or opaque provider-managed guidance instead. Unknown names, invalid names, and skills marked `disableModelInvocation: true` return `isError` tool results through the normal tool registry error path.
The tool does not call `agent.inject()` in v1. Its result is already recorded as the tool result and becomes available to the next model step without duplicating the content as synthetic context.

View File

@@ -32,6 +32,7 @@
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-skill": "workspace:^",
"@deepseek-ai/dsh-skill-local": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"cordis": "^4.0.0-rc.6"
}

View File

@@ -6,6 +6,7 @@
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { assertNever } from '@deepseek-ai/dsh-llm'
import { isSkillName, type SkillDefinition } from '@deepseek-ai/dsh-skill'
export const name = 'tool-skill'
@@ -39,14 +40,34 @@ export function apply(ctx: Context): void {
}
function renderSkillContent(skill: SkillDefinition): string {
const resourceHint = renderResourceHint(skill)
return [
`<skill_content name="${skill.name}">`,
`# Skill: ${skill.name}`,
'',
skill.content,
'',
`Base directory for this skill: ${skill.directory}`,
'Resolve relative files mentioned by this skill against the base directory before using them.',
...resourceHint,
'</skill_content>',
].join('\n')
}
function renderResourceHint(skill: SkillDefinition): string[] {
const base = skill.resourceBase
if (base === undefined) {
return [`Resources for this skill are managed by provider "${skill.provider}".`]
}
switch (base.kind) {
case 'directory':
return [
`Base directory for this skill: ${base.path}`,
'Resolve relative files mentioned by this skill against the base directory before using them.',
]
case 'url':
return [`Base URL for this skill: ${base.url}`]
case 'opaque':
return [`Resources for this skill: ${base.description}`]
default:
return assertNever(base, 'SkillResourceBase.kind')
}
}

View File

@@ -7,6 +7,7 @@ import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import SkillService from '@deepseek-ai/dsh-skill'
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
import * as toolSkill from '@deepseek-ai/dsh-tool-skill'
async function tempDir(name: string): Promise<string> {
@@ -23,7 +24,8 @@ async function setup(home: string): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false })
await ctx.plugin(SkillService)
await ctx.plugin(SkillLocal, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') })
await ctx.plugin(toolSkill)
return ctx
}
@@ -34,7 +36,8 @@ describe('dsh-tool-skill', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
const home = await tempDir('tool-schema')
await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false })
await ctx.plugin(SkillService)
await ctx.plugin(SkillLocal, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') })
const fiber = await ctx.plugin(toolSkill)
expect(ctx.tools.schemas().map(tool => tool.name)).toEqual(['skill'])
@@ -70,6 +73,65 @@ describe('dsh-tool-skill', () => {
expect(block.text).toContain('Project instructions.')
})
it('renders provider-managed resource hints for non-local skills', async () => {
const home = await tempDir('tool-resource-hints')
const ctx = await setup(home)
ctx.skills.register({
name: 'opaque-skill',
description: 'Opaque skill',
source: 'runtime',
provider: 'runtime',
resourceBase: { kind: 'opaque', description: 'runtime memory' },
content: 'Opaque instructions.',
})
ctx.skills.register({
name: 'url-skill',
description: 'URL skill',
source: 'runtime',
provider: 'runtime',
resourceBase: { kind: 'url', url: 'https://skills.example.test/url-skill' },
content: 'URL instructions.',
})
ctx.skills.register({
name: 'provider-skill',
description: 'Provider skill',
source: 'runtime',
provider: 'runtime',
content: 'Provider instructions.',
})
const opaque = await ctx.tools.execute({ callId: CallId('c2'), name: 'skill', arguments: { name: 'opaque-skill' } })
const url = await ctx.tools.execute({ callId: CallId('c3'), name: 'skill', arguments: { name: 'url-skill' } })
const provider = await ctx.tools.execute({ callId: CallId('c4'), name: 'skill', arguments: { name: 'provider-skill' } })
if (opaque.content[0]?.type !== 'text' || url.content[0]?.type !== 'text' || provider.content[0]?.type !== 'text') {
throw new Error('expected text tool results')
}
expect(opaque.content[0].text).toContain('Resources for this skill: runtime memory')
expect(url.content[0].text).toContain('Base URL for this skill: https://skills.example.test/url-skill')
expect(provider.content[0].text).toContain('Resources for this skill are managed by provider "runtime"')
})
it('fails loud on an unknown resource base kind', async () => {
const home = await tempDir('tool-resource-assert-never')
const ctx = await setup(home)
ctx.skills.register({
name: 'rogue-resource-skill',
description: 'Rogue resource skill',
source: 'runtime',
provider: 'runtime',
resourceBase: { kind: 'future' } as never,
content: 'Rogue instructions.',
})
const result = await ctx.tools.execute({ callId: CallId('c5'), name: 'skill', arguments: { name: 'rogue-resource-skill' } })
expect(result.isError).toBe(true)
const block = result.content[0]
if (block?.type !== 'text') throw new Error('expected text tool result')
expect(block.text).toContain('unreachable variant')
})
it('returns isError for unknown, invalid, and model-disabled skills', async () => {
const home = await tempDir('tool-errors')
await writeSkill(join(home, '.dsh/skills'), 'hidden-skill', 'Hidden skill', 'Hidden instructions.')