Merge remote-tracking branch 'origin/master' into codex/skill-system

# Conflicts:
#	docs/event-producer-consumer.md
This commit is contained in:
Yichen Jiang
2026-07-06 17:11:35 +08:00
124 changed files with 7426 additions and 6716 deletions

View File

@@ -47,7 +47,7 @@ Skills can be single-level directory bundles (`<name>/SKILL.md`) or flat Markdow
## Prompt Integration
The service listens on `agent/request` and appends a short `## Skills` listing to the request system prompt for the calling agent's cwd. The listing contains only stable routing metadata (`name`, `source`, `description`, and optional `whenToUse`), not skill bodies or local absolute paths. `description` and `whenToUse` are whitespace-normalized and capped in the listing so one pathological skill cannot bloat every model request. Models load full instructions through the `skill` tool.
The service listens on `system-prompt/assemble` and appends a short `## Skills` section to the calling agent's assembled system prompt. The listing contains only stable routing metadata (`name`, `source`, `description`, and optional `whenToUse`), not skill bodies or local absolute paths. `description` and `whenToUse` are whitespace-normalized and capped in the listing so one pathological skill cannot bloat every model request. Models load full instructions through the `skill` tool.
## System Skills

View File

@@ -25,6 +25,7 @@
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-fs": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"cordis": "^4.0.0-rc.6"
},
"dependencies": {
@@ -35,6 +36,7 @@
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"cordis": "^4.0.0-rc.6"
}
}

View File

@@ -16,12 +16,13 @@ import z from 'schemastery'
import type Schema from 'schemastery'
import { parse as parseYaml } from 'yaml'
import type { FileSystem, FsDirEntry, FsTarget } from '@deepseek-ai/dsh-fs'
import type { GenerateOptions } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-system-prompt'
import type {} from '@deepseek-ai/dsh-agent'
const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
const DEFAULT_PROMPT_FIELD_LENGTH = 500
const DEFAULT_COLLECT_CACHE_ENTRIES = 128
const SKILL_PROMPT_SECTION_ORDER = 1000
/** Return whether a string is a valid kebab-case skill name. */
export function isSkillName(name: string): boolean {
@@ -168,10 +169,18 @@ export class SkillService extends Service {
})
}
ctx.on('agent/request', async (agent, _turn, _step, _request, next) => {
const listing = await this.renderModelListing({ cwd: agent.session.header.cwd })
ctx.on('system-prompt/assemble', async (_assembly, context, next) => {
const result = await next()
if (listing.length > 0) appendSystem(result, listing)
const agent = context.agent
if (agent === undefined) return result
const listing = await this.renderModelListing({ cwd: agent.session.header.cwd })
if (listing.length > 0) {
result.sections.push({
name: 'skills:available',
order: SKILL_PROMPT_SECTION_ORDER,
text: listing,
})
}
return result
})
}
@@ -669,8 +678,4 @@ function errorMessage(error: unknown): string {
return String(error)
}
function appendSystem(request: GenerateOptions, text: string): void {
request.system = [request.system ?? '', text].filter(part => part.length > 0).join('\n\n')
}
export default SkillService

View File

@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os'
import { Context } from 'cordis'
import SkillService from '@deepseek-ai/dsh-skill'
import { FileSystem, FsVersion, type FsDirEntry, type FsEditOutcome, type FsEditRequest, type FsInfo, type FsTarget, type FsWriteOutcome } from '@deepseek-ai/dsh-fs'
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
async function tempDir(name: string): Promise<string> {
return await import('node:fs/promises').then(fs => fs.mkdtemp(join(tmpdir(), `dsh-${name}-`)))
@@ -21,6 +22,10 @@ async function writeFlatSkill(root: string, name: string, description: string, b
await writeFile(join(root, `${name}.md`), `---\nname: ${name}\ndescription: ${description}\n---\n\n${body}\n`)
}
function agentForCwd(cwd: string): never {
return { session: { header: { cwd } } } as never
}
class TestFileSystem extends FileSystem {
listDirCalls = 0
failResolvePaths = new Set<string>()
@@ -350,14 +355,12 @@ describe('SkillService', () => {
it('renders no model listing when no model-invocable skills exist', async () => {
const home = await tempDir('skill-empty-listing')
const ctx = new Context()
await ctx.plugin(SystemPrompt, { persona: 'base' })
await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false })
expect(await ctx.skills.renderModelListing()).toBe('')
const request = { model: 'm', messages: [], system: 'base' }
const result = await ctx.waterfall('agent/request', {
session: { header: { cwd: home } },
} as never, 1, 1, request, () => Promise.resolve(request))
expect(result.system).toBe('base')
expect(renderPrompt(await ctx.systemPrompt.assemble({ agent: agentForCwd(home) }))).not.toContain('## Skills')
expect(renderPrompt(await ctx.systemPrompt.assemble())).not.toContain('## Skills')
})
it('supports default home root resolution without installing system skills', async () => {
@@ -592,42 +595,33 @@ describe('SkillService', () => {
expect((await ctx.skills.get('escaped-skill'))?.description).toBe('Use </available_skills><oops> safely')
})
it('adds skill guidance through the agent/request waterfall without including bodies', async () => {
it('adds skill guidance through system prompt assembly without including bodies', async () => {
const home = await tempDir('skill-guidance')
await writeSkill(join(home, '.dsh/skills'), 'research-helper', 'Research helper', 'Long body that must not be listed.')
const ctx = new Context()
await ctx.plugin(SystemPrompt, { persona: 'base' })
await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false })
const request = await ctx.waterfall('agent/request', {
session: { header: { cwd: home } },
} as never, 1, 1, { model: 'm', messages: [], system: 'base' }, () => Promise.resolve({ model: 'm', messages: [], system: 'base' }))
const prompt = renderPrompt(await ctx.systemPrompt.assemble({ agent: agentForCwd(home) }))
expect(request.system ?? '').toContain('## Skills\n')
expect(request.system ?? '').toContain('research-helper')
expect(request.system ?? '').toContain('source="project-dsh"')
expect(request.system ?? '').not.toContain(home)
expect(request.system ?? '').not.toContain('Long body')
expect((request.system ?? '').match(/## Skills/g)).toHaveLength(1)
const sameObject = { model: 'm', messages: [], system: 'base' }
const sameObjectResult = await ctx.waterfall('agent/request', {
session: { header: { cwd: home } },
} as never, 1, 1, sameObject, () => Promise.resolve(sameObject))
expect(sameObjectResult.system).toContain('## Skills')
const requestWithoutBase = await ctx.waterfall('agent/request', {
session: { header: { cwd: home } },
} as never, 1, 1, { model: 'm', messages: [] }, () => Promise.resolve({ model: 'm', messages: [] }))
expect(requestWithoutBase.system).toContain('## Skills')
expect(prompt).toContain('base')
expect(prompt).toContain('## Skills\n')
expect(prompt).toContain('research-helper')
expect(prompt).toContain('source="project-dsh"')
expect(prompt).not.toContain(home)
expect(prompt).not.toContain('Long body')
expect(prompt.match(/## Skills/g)).toHaveLength(1)
const copyCtx = new Context()
await copyCtx.plugin(SystemPrompt, { persona: 'base' })
await copyCtx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false })
copyCtx.on('agent/request', async (_agent, _turn, _step, requestToCopy) => ({ ...requestToCopy }))
const copiedRequest = await copyCtx.waterfall('agent/request', {
session: { header: { cwd: home } },
} as never, 1, 1, { model: 'm', messages: [], system: 'base' }, () => Promise.resolve({ model: 'm', messages: [], system: 'base' }))
expect((copiedRequest.system ?? '').match(/## Skills/g)).toHaveLength(1)
copyCtx.on('system-prompt/assemble', async (_assembly, _context, next) => {
const result = await next()
return { ...result, sections: [...result.sections] }
})
const copiedPrompt = renderPrompt(await copyCtx.systemPrompt.assemble({ agent: agentForCwd(home) }))
expect(copiedPrompt.match(/## Skills/g)).toHaveLength(1)
})
it('cleans up runtime registered skills when the contributing fiber is disposed', async () => {