Merge remote-tracking branch 'origin/master' into codex/project-instruction-files

This commit is contained in:
Yichen Jiang
2026-07-11 23:02:24 +08:00
190 changed files with 12229 additions and 460 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# @deepseek-ai/dsh-acp-agent
The **ACP server app**: a Cordis app plugin that composes the providerless agent spine ([`@deepseek-ai/dsh-agent-core`](../../core/agent-core/README.md)) with the front-door cluster an [Agent Client Protocol](../acp/README.md) server needs, and a `bin` that boots a leaf `cordis.yml` speaking ACP JSON-RPC on stdio.
The **ACP server app**: a Cordis app plugin that composes the default agent spine ([`@deepseek-ai/dsh-agent-core`](../../core/agent-core/README.md)) with the front-door cluster an [Agent Client Protocol](../acp/README.md) server needs, and a `bin` that boots a leaf `cordis.yml` speaking ACP JSON-RPC on stdio.
It is the structured counterpart to [`@deepseek-ai/dsh-stdio-agent`](../stdio-agent/README.md): both consume the same spine, but this one bakes in the OPPOSITE front-door cluster.
+5 -1
View File
@@ -1,5 +1,5 @@
/**
* The ACP server app: the providerless agent spine ({@link
* The ACP server app: the default agent spine ({@link
* @deepseek-ai/dsh-agent-core}) plus the coupled front-door cluster an ACP
* server needs — JSONL session persistence and the {@link @deepseek-ai/dsh-acp}
* bridge, and DELIBERATELY NOTHING that writes to stdout.
@@ -63,6 +63,8 @@ export interface Config {
persistenceRoot?: string
/** Controls automatic AGENTS.md/CLAUDE.md loading; set `false` for hermetic prompts. */
workspaceContext?: agentCore.Config['workspaceContext']
/** Skill registry, local-provider, and model-facing consumer config forwarded to agent-core. */
skills?: agentCore.SkillConfig
}
export const Config: z<Config> = z.object({
@@ -75,6 +77,7 @@ export const Config: z<Config> = z.object({
tools: ToolRegistry.Config,
persistenceRoot: z.string().default('./.sessions'),
workspaceContext: z.union([z.const(false), workspaceContext.Config]),
skills: agentCore.SkillConfigSchema,
}) as unknown as z<Config>
/**
@@ -90,6 +93,7 @@ export function apply(ctx: Context, config: Config): void {
...config.toolOrder !== undefined ? { toolOrder: config.toolOrder } : {},
...config.tools !== undefined ? { tools: config.tools } : {},
...config.workspaceContext !== undefined ? { workspaceContext: config.workspaceContext } : {},
...config.skills !== undefined ? { skills: config.skills } : {},
})
ctx.plugin(UserInteractionService)
ctx.plugin(SessionPersistenceJsonl, { root: config.persistenceRoot ?? './.sessions' })
+63 -3
View File
@@ -1,7 +1,11 @@
import { describe, expect, it } from 'vitest'
import { mkdtemp } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
import type { Message } from '@deepseek-ai/dsh-llm'
import * as acpAgent from '../src/index.ts'
/**
@@ -23,9 +27,47 @@ async function mount(config: acpAgent.Config): Promise<Context> {
return ctx
}
async function isolatedSkillsConfig(catalogDescriptionMaxLength?: number): Promise<NonNullable<acpAgent.Config['skills']>> {
const home = await mkdtemp(join(tmpdir(), 'dsh-acp-agent-skills-'))
return {
local: { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') },
...catalogDescriptionMaxLength !== undefined ? { tool: { catalogDescriptionMaxLength } } : {},
}
}
async function composePrefix(ctx: Context): Promise<Message[]> {
const empty: Message[] = []
return await ctx.waterfall(
'agent/session-prefix', { session: { header: { cwd: '/tmp' } } } as never,
empty, new AbortController().signal, () => Promise.resolve(empty),
)
}
async function withIsolatedSkillHomes<T>(run: () => Promise<T>): Promise<T> {
const oldDshHome = process.env.DSH_HOME
const oldAgentsHome = process.env.DSH_AGENTS_HOME
const home = await mkdtemp(join(tmpdir(), 'dsh-acp-agent-default-skills-'))
process.env.DSH_HOME = join(home, '.dsh')
process.env.DSH_AGENTS_HOME = join(home, '.agents')
try {
return await run()
} finally {
if (oldDshHome === undefined) {
delete process.env.DSH_HOME
} else {
process.env.DSH_HOME = oldDshHome
}
if (oldAgentsHome === undefined) {
delete process.env.DSH_AGENTS_HOME
} else {
process.env.DSH_AGENTS_HOME = oldAgentsHome
}
}
}
describe('dsh-acp-agent composition', () => {
it('brings up the spine + persistence + the ACP bridge', async () => {
const ctx = await mount({ model: 'mock', persona: 'hi', persistenceRoot: '/tmp/dsh-acp-agent-test' })
const ctx = await mount({ model: 'mock', persona: 'hi', persistenceRoot: '/tmp/dsh-acp-agent-test', skills: await isolatedSkillsConfig() })
expect(ctx.get('agents')).toBeDefined()
expect(ctx.get('sessions')).toBeDefined()
expect(ctx.get('sessionPersistence')).toBeDefined()
@@ -44,7 +86,7 @@ describe('dsh-acp-agent composition', () => {
// persistenceRoot, so the runtime fallback is the one that fires.
const ctx = new Context()
// No persona: covers the omitted-persona forwarding branch too.
acpAgent.apply(ctx, { model: 'mock' })
acpAgent.apply(ctx, { model: 'mock', skills: await isolatedSkillsConfig() })
await new Promise(resolve => setTimeout(resolve, 50))
expect(ctx.get('sessionPersistence')).toBeDefined()
await ctx.fiber.dispose()
@@ -62,6 +104,24 @@ describe('dsh-acp-agent composition', () => {
await ctx.fiber.dispose()
})
it('uses default skill config when apply is called directly without skills', async () => {
await withIsolatedSkillHomes(async () => {
const ctx = new Context()
acpAgent.apply(ctx, { model: 'mock' })
await new Promise(resolve => setTimeout(resolve, 50))
expect(ctx.skills).toBeDefined()
expect(await ctx.skills.list()).toEqual([])
await ctx.fiber.dispose()
})
})
it('forwards skill config into agent-core', async () => {
const ctx = await mount({ model: 'mock', persona: 'hi', skills: await isolatedSkillsConfig(6) })
ctx.skills.register({ name: 'acp-skill', description: 'ACP skill', source: 'runtime', content: 'body' })
expect(JSON.stringify(await composePrefix(ctx))).toContain('- `acp-skill`: ACP...')
await ctx.fiber.dispose()
})
it('exposes its plugin shape', () => {
expect(acpAgent.name).toBe('acp-agent')
expect(acpAgent.Config).toBeDefined()
@@ -84,7 +144,7 @@ describe('dsh-acp-agent composition', () => {
})
}
const assembly = await ctx.get('systemPrompt')!.assemble()
expect(assembly.tools.map(tool => tool.name)).toEqual(['zulu', 'alpha'])
expect(assembly.tools.map(tool => tool.name)).toEqual(['zulu', 'alpha', 'skill'])
await ctx.fiber.dispose()
})
+12 -2
View File
@@ -120,7 +120,12 @@ describe.skipIf(!existsSync(acpBin))('dsh-acp-agent BUILT bin (node lib/bin.js,
child = spawn(process.execPath, ['--expose-internals', acpBin, './cordis.yml'], {
cwd: consumer,
// Dummy key: initialize never reaches the model, so it is never used.
env: { ...process.env, DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY ?? 'sk-dummy-for-boot' },
env: {
...process.env,
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY ?? 'sk-dummy-for-boot',
DSH_HOME: join(consumer, '.dsh'),
DSH_AGENTS_HOME: join(consumer, '.agents'),
},
stdio: ['pipe', 'pipe', 'pipe'],
})
const stderr: string[] = []
@@ -180,7 +185,12 @@ function runBinExpectingExit(configArg: string, cwd: string = tmpdir()): Promise
return new Promise((resolve, reject) => {
const proc = spawn(process.execPath, ['--expose-internals', acpBin, configArg], {
cwd,
env: { ...process.env, DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY ?? 'sk-dummy-for-boot' },
env: {
...process.env,
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY ?? 'sk-dummy-for-boot',
DSH_HOME: join(cwd, '.dsh'),
DSH_AGENTS_HOME: join(cwd, '.agents'),
},
stdio: ['pipe', 'pipe', 'pipe'],
})
child = proc
@@ -90,6 +90,8 @@ async function boot(): Promise<Spawned & { cwd: string }> {
TSX_TSCONFIG_PATH: repoTsconfig,
// Key-present check only; no prompt is sent, so the model is never called.
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY ?? 'keyless-acp-agent-smoke',
DSH_HOME: join(cwd, '.dsh'),
DSH_AGENTS_HOME: join(cwd, '.agents'),
},
stdio: ['pipe', 'pipe', 'pipe'],
},