Merge remote-tracking branch 'origin/master' into codex/skill-system
# Conflicts: # docs/event-producer-consumer.md
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user