Merge origin/master into project instruction files

This commit is contained in:
Yichen Jiang
2026-07-06 15:44:42 +08:00
118 changed files with 7460 additions and 6773 deletions

View File

@@ -5,7 +5,7 @@ import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import * as projectInstructions from '@deepseek-ai/dsh-project-instructions'
import { CallId, type GenerateOptions } from '@deepseek-ai/dsh-llm'
import { CallId } from '@deepseek-ai/dsh-llm'
import { Session, SessionId, SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
import type { Agent, HookContext } from '@deepseek-ai/dsh-agent'
import { AgentId } from '@deepseek-ai/dsh-agent'
@@ -122,17 +122,14 @@ function stubAgent(cwd?: string): Agent {
status: 'idle',
send() {},
steer() {},
inject() {},
inject(content, options) {
session.append('context/message', { content, source: options?.source ?? { kind: 'user' } }, { surfaceOp: 'append' })
},
cancel() {},
whenIdle: () => Promise.resolve(),
}
}
function firstText(message: GenerateOptions['messages'][number] | undefined): string | undefined {
const block = message?.content[0]
return block?.type === 'text' ? block.text : undefined
}
function blocksText(blocks: { type: string; text?: string }[] | undefined): string {
return blocks?.map(block => block.type === 'text' ? block.text ?? '' : '').join('\n') ?? ''
}
@@ -146,6 +143,18 @@ function appendAdditionalContext(agent: Agent, result: { additionalContext?: Hoo
}, { surfaceOp: 'append' }).seq
}
async function runBaselinePreStep(ctx: Context, agent: Agent): Promise<void> {
await ctx.serial('agent/pre-step', agent, 1, 1, '', AbortSignal.timeout(1000))
}
function derivedText(agent: Agent): string {
return blocksText(agent.session.deriveMessages()[0]?.content)
}
function expectNoDerivedMessages(agent: Agent): void {
expect(agent.session.deriveMessages()).toEqual([])
}
describe('project instruction discovery', () => {
it('loads user-global first, then root-to-cwd project instructions using the default candidate order', async () => {
const root = await tempRepo()
@@ -275,14 +284,11 @@ describe('project instruction discovery', () => {
await symlink(join(outside, 'secret.txt'), join(root, 'AGENTS.md'))
const ctx = new Context()
await mountProjectInstructions(ctx, { dshHome: home })
const agent = stubAgent(root)
const request: GenerateOptions = {
model: 'mock',
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
}
const result = await ctx.waterfall('agent/request', stubAgent(root), 1, 1, request, async () => request)
await runBaselinePreStep(ctx, agent)
expect(result.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }])
expectNoDerivedMessages(agent)
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -631,18 +637,15 @@ describe('project instruction request injection', () => {
expect('inject' in projectInstructions).toBe(false)
})
it('leaves requests unchanged when no filesystem provider is present', async () => {
it('does not inject baseline context when no filesystem provider is present', async () => {
const ctx = new Context()
try {
await ctx.plugin(projectInstructions, {})
const request: GenerateOptions = {
model: 'mock',
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
}
const agent = stubAgent('/virtual/repo')
const result = await ctx.waterfall('agent/request', stubAgent('/virtual/repo'), 1, 1, request, async () => request)
await runBaselinePreStep(ctx, agent)
expect(result.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }])
expectNoDerivedMessages(agent)
} finally {
await ctx.fiber.dispose()
}
@@ -673,7 +676,7 @@ describe('project instruction request injection', () => {
}
})
it('prepends a synthetic user workspace-context message without mutating the system prompt', async () => {
it('injects baseline workspace context through the session log before step derivation', async () => {
const root = await tempRepo()
const home = await tempRepo()
try {
@@ -681,20 +684,35 @@ describe('project instruction request injection', () => {
await write(join(root, 'AGENTS.md'), 'repo rule')
const ctx = new Context()
await mountProjectInstructions(ctx, { dshHome: home })
const agent = stubAgent(root)
const request: GenerateOptions = {
model: 'mock',
system: 'real system',
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
}
const result = await ctx.waterfall('agent/request', stubAgent(root), 1, 1, request, async () => request)
await runBaselinePreStep(ctx, agent)
expect(result.system).toBe('real system')
expect(result.messages).toHaveLength(2)
expect(result.messages[0]?.role).toBe('user')
expect(firstText(result.messages[0])).toContain('<workspace-context source="project-instruction-files">')
expect(firstText(result.messages[0])).toContain('repo rule')
expect(result.messages[1]).toEqual({ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] })
expect(agent.session.deriveMessages()).toHaveLength(1)
expect(derivedText(agent)).toContain('<context source="plugin">')
expect(derivedText(agent)).toContain('<workspace-context source="project-instruction-files">')
expect(derivedText(agent)).toContain('repo rule')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
}
})
it('does not duplicate still-visible baseline workspace context on later pre-step checks', async () => {
const root = await tempRepo()
const home = await tempRepo()
try {
await mkdir(join(root, '.git'), { recursive: true })
await write(join(root, 'AGENTS.md'), 'repo rule')
const ctx = new Context()
await mountProjectInstructions(ctx, { dshHome: home })
const agent = stubAgent(root)
await runBaselinePreStep(ctx, agent)
await runBaselinePreStep(ctx, agent)
expect(agent.session.events.filter(event => event.type === 'context/message')).toHaveLength(1)
expect(derivedText(agent)).toContain('repo rule')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -713,15 +731,12 @@ describe('project instruction request injection', () => {
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'AGENTS.md'), { type: 'file', content: 'ctx.fs rule' })
await ctx.plugin(projectInstructions, { dshHome: home })
const agent = stubAgent(root)
const request: GenerateOptions = {
model: 'mock',
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
}
const result = await ctx.waterfall('agent/request', stubAgent(root), 1, 1, request, async () => request)
await runBaselinePreStep(ctx, agent)
expect(firstText(result.messages[0])).toContain('ctx.fs rule')
expect(firstText(result.messages[0])).not.toContain('node fs rule')
expect(derivedText(agent)).toContain('ctx.fs rule')
expect(derivedText(agent)).not.toContain('node fs rule')
expect(fs.readTargets).toEqual([join(root, 'AGENTS.md')])
} finally {
await rm(root, { recursive: true, force: true })
@@ -739,14 +754,11 @@ describe('project instruction request injection', () => {
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'AGENTS.md'), { type: 'file', content: 'provider-only rule' })
await ctx.plugin(projectInstructions, { dshHome: home })
const agent = stubAgent(root)
const request: GenerateOptions = {
model: 'mock',
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
}
const result = await ctx.waterfall('agent/request', stubAgent(root), 1, 1, request, async () => request)
await runBaselinePreStep(ctx, agent)
expect(firstText(result.messages[0])).toContain('provider-only rule')
expect(derivedText(agent)).toContain('provider-only rule')
expect(fs.readTargets).toEqual([join(root, 'AGENTS.md')])
} finally {
await ctx.fiber.dispose()
@@ -769,17 +781,14 @@ describe('project instruction request injection', () => {
fs.entries.set(join(home, 'AGENTS.md'), { type: 'file', content: 'ctx global rule' })
fs.entries.set(join(root, 'CLAUDE.md'), { type: 'file', content: 'ctx claude rule' })
await ctx.plugin(projectInstructions, { dshHome: home })
const agent = stubAgent(root)
const request: GenerateOptions = {
model: 'mock',
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
}
const result = await ctx.waterfall('agent/request', stubAgent(root), 1, 1, request, async () => request)
await runBaselinePreStep(ctx, agent)
expect(firstText(result.messages[0])).toContain('ctx global rule')
expect(firstText(result.messages[0])).toContain('ctx claude rule')
expect(firstText(result.messages[0])).not.toContain('node global rule')
expect(firstText(result.messages[0])).not.toContain('node claude rule')
expect(derivedText(agent)).toContain('ctx global rule')
expect(derivedText(agent)).toContain('ctx claude rule')
expect(derivedText(agent)).not.toContain('node global rule')
expect(derivedText(agent)).not.toContain('node claude rule')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -798,14 +807,11 @@ describe('project instruction request injection', () => {
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'AGENTS.md'), { type: 'directory' })
await ctx.plugin(projectInstructions, { dshHome: home })
const agent = stubAgent(root)
const request: GenerateOptions = {
model: 'mock',
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
}
const result = await ctx.waterfall('agent/request', stubAgent(root), 1, 1, request, async () => request)
await runBaselinePreStep(ctx, agent)
expect(result.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }])
expectNoDerivedMessages(agent)
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -825,14 +831,11 @@ describe('project instruction request injection', () => {
fs.entries.set(join(root, 'AGENTS.md'), { type: 'directory' })
fs.lstatTypes.set(join(root, 'AGENTS.md'), 'file')
await ctx.plugin(projectInstructions, { dshHome: home })
const agent = stubAgent(root)
const request: GenerateOptions = {
model: 'mock',
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
}
const result = await ctx.waterfall('agent/request', stubAgent(root), 1, 1, request, async () => request)
await runBaselinePreStep(ctx, agent)
expect(result.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }])
expectNoDerivedMessages(agent)
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -851,14 +854,11 @@ describe('project instruction request injection', () => {
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'AGENTS.md'), { type: 'file' })
await ctx.plugin(projectInstructions, { dshHome: home })
const agent = stubAgent(root)
const request: GenerateOptions = {
model: 'mock',
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
}
const result = await ctx.waterfall('agent/request', stubAgent(root), 1, 1, request, async () => request)
await runBaselinePreStep(ctx, agent)
expect(firstText(result.messages[0])).toContain('## AGENTS.md')
expect(derivedText(agent)).toContain('## AGENTS.md')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -877,14 +877,11 @@ describe('project instruction request injection', () => {
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.throwOnStat.add(join(root, 'AGENTS.md'))
await ctx.plugin(projectInstructions, { dshHome: home })
const agent = stubAgent(root)
const request: GenerateOptions = {
model: 'mock',
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
}
const result = await ctx.waterfall('agent/request', stubAgent(root), 1, 1, request, async () => request)
await runBaselinePreStep(ctx, agent)
expect(result.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }])
expectNoDerivedMessages(agent)
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -903,14 +900,11 @@ describe('project instruction request injection', () => {
fs.throwOnStat.add(join(root, '.git'))
fs.entries.set(join(root, 'AGENTS.md'), { type: 'file', content: 'repo rule' })
await ctx.plugin(projectInstructions, { dshHome: home })
const agent = stubAgent(root)
const request: GenerateOptions = {
model: 'mock',
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
}
const result = await ctx.waterfall('agent/request', stubAgent(root), 1, 1, request, async () => request)
await runBaselinePreStep(ctx, agent)
expect(firstText(result.messages[0])).toContain('repo rule')
expect(derivedText(agent)).toContain('repo rule')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -928,16 +922,16 @@ describe('project instruction request injection', () => {
await write(join(repoB, 'AGENTS.md'), 'repo B only')
const ctx = new Context()
await mountProjectInstructions(ctx, { dshHome: home })
const requestA: GenerateOptions = { model: 'mock', messages: [{ role: 'user', content: [{ type: 'text', text: 'A' }] }] }
const requestB: GenerateOptions = { model: 'mock', messages: [{ role: 'user', content: [{ type: 'text', text: 'B' }] }] }
const agentA = stubAgent(repoA)
const agentB = stubAgent(repoB)
const resultA = await ctx.waterfall('agent/request', stubAgent(repoA), 1, 1, requestA, async () => requestA)
const resultB = await ctx.waterfall('agent/request', stubAgent(repoB), 1, 1, requestB, async () => requestB)
await runBaselinePreStep(ctx, agentA)
await runBaselinePreStep(ctx, agentB)
expect(firstText(resultA.messages[0])).toContain('repo A only')
expect(firstText(resultA.messages[0])).not.toContain('repo B only')
expect(firstText(resultB.messages[0])).toContain('repo B only')
expect(firstText(resultB.messages[0])).not.toContain('repo A only')
expect(derivedText(agentA)).toContain('repo A only')
expect(derivedText(agentA)).not.toContain('repo B only')
expect(derivedText(agentB)).toContain('repo B only')
expect(derivedText(agentB)).not.toContain('repo A only')
} finally {
await rm(repoA, { recursive: true, force: true })
await rm(repoB, { recursive: true, force: true })
@@ -956,19 +950,19 @@ describe('project instruction request injection', () => {
const ctx = new Context()
await ctx.plugin(LocalFileSystem, { cwd: '/' })
await ctx.plugin(projectInstructions, {})
const request: GenerateOptions = { model: 'mock', messages: [{ role: 'user', content: [{ type: 'text', text: 'prompt' }] }] }
const agent = stubAgent(cwd)
const result = await ctx.waterfall('agent/request', stubAgent(cwd), 1, 1, request, async () => request)
await runBaselinePreStep(ctx, agent)
expect(firstText(result.messages[0])).toContain('## AGENTS.md\n\nroot schema default rule')
expect(firstText(result.messages[0])).toContain('## child/AGENTS.md\n\nchild schema default rule')
expect(derivedText(agent)).toContain('## AGENTS.md\n\nroot schema default rule')
expect(derivedText(agent)).toContain('## child/AGENTS.md\n\nchild schema default rule')
await ctx.fiber.dispose()
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('cleans up its agent/request listener when the plugin fiber is disposed', async () => {
it('cleans up its agent/pre-step listener when the plugin fiber is disposed', async () => {
const root = await tempRepo()
const home = await tempRepo()
try {
@@ -977,14 +971,11 @@ describe('project instruction request injection', () => {
const ctx = new Context()
const fiber = await mountProjectInstructions(ctx, { dshHome: home })
await fiber.dispose()
const agent = stubAgent(root)
const request: GenerateOptions = {
model: 'mock',
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
}
const result = await ctx.waterfall('agent/request', stubAgent(root), 1, 1, request, async () => request)
await runBaselinePreStep(ctx, agent)
expect(result.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }])
expectNoDerivedMessages(agent)
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -999,14 +990,11 @@ describe('project instruction request injection', () => {
await write(join(root, 'AGENTS.md'), 'repo rule')
const ctx = new Context()
await mountProjectInstructions(ctx, { dshHome: home, baselineMaxBytes: 0 })
const agent = stubAgent(root)
const request: GenerateOptions = {
model: 'mock',
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
}
const result = await ctx.waterfall('agent/request', stubAgent(root), 1, 1, request, async () => request)
await runBaselinePreStep(ctx, agent)
expect(result.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }])
expectNoDerivedMessages(agent)
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -1021,14 +1009,11 @@ describe('project instruction request injection', () => {
await write(join(root, 'AGENTS.md'), 'repo rule')
const ctx = new Context()
await mountProjectInstructions(ctx, { dshHome: home, baselineMaxBytes: -1 })
const agent = stubAgent(root)
const request: GenerateOptions = {
model: 'mock',
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
}
const result = await ctx.waterfall('agent/request', stubAgent(root), 1, 1, request, async () => request)
await runBaselinePreStep(ctx, agent)
expect(result.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }])
expectNoDerivedMessages(agent)
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -1042,14 +1027,11 @@ describe('project instruction request injection', () => {
await mkdir(join(root, '.git'), { recursive: true })
const ctx = new Context()
await mountProjectInstructions(ctx, { dshHome: home })
const agent = stubAgent(root)
const request: GenerateOptions = {
model: 'mock',
messages: [{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }],
}
const result = await ctx.waterfall('agent/request', stubAgent(root), 1, 1, request, async () => request)
await runBaselinePreStep(ctx, agent)
expect(result.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'actual prompt' }] }])
expectNoDerivedMessages(agent)
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })