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

# Conflicts:
#	AGENTS.md
#	docs/config-catalog.md
#	docs/module-graph.md
#	examples/echo-agent/composition.md
#	packages/README.md
#	packages/core/README.md
#	packages/examples/acp-demo/package.json
#	packages/examples/acp-demo/src/index.ts
#	packages/examples/acp-demo/tests/acp-agent.spec.ts
#	packages/examples/acp-demo/tests/built-bin.e2e.ts
#	packages/examples/acp-demo/tsconfig.json
#	packages/examples/agent-spine-demo/README.md
#	packages/examples/agent-spine-demo/package.json
#	packages/examples/agent-spine-demo/tests/agent-core.spec.ts
#	packages/examples/stdio-demo/package.json
#	packages/examples/stdio-demo/src/index.ts
#	packages/examples/stdio-demo/tests/built-bin.e2e.ts
#	packages/examples/stdio-demo/tests/stdio-agent.spec.ts
#	packages/examples/stdio-demo/tsconfig.json
#	pnpm-lock.yaml
#	python/sdk-runtime/README.i18n.yaml
#	python/sdk-runtime/src/deepseek_harness_runtime/runtime/cordis.yml
#	python/sdk/tests/test_bundled_runtime.py
This commit is contained in:
Yichen Jiang
2026-07-15 17:04:16 +08:00
185 changed files with 4140 additions and 825 deletions
@@ -0,0 +1,324 @@
import { describe, expect, it } from 'vitest'
import { mkdir, mkdtemp, rm, writeFile } 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 * as agentCore from '../src/index.ts'
import { AgentId, agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import type { Message } from '@deepseek-ai/dsh-llm'
async function composePrefix(ctx: Context, cwd: string): Promise<Message[]> {
const agent = { session: { header: { cwd } } } as unknown as Agent
const empty: Message[] = []
return await agentEvents(ctx, agent).waterfall(
'agent/session-prefix', empty, new AbortController().signal,
() => Promise.resolve(empty),
)
}
/**
* Unit coverage for the @deepseek-ai/dsh-agent-spine-demo bundle: mounting it brings
* up the whole default spine in one `ctx.plugin`, and the forwarded
* `agents` config reaches the loop (default `[]`, or a pre-created agent).
*
* The bundle is exercised through `ctx.plugin(agentCore, …)` — the NAMESPACE
* import, the same shape the Loader builds from `unwrapExports`. The real
* Loader-path guard (export shape, `unwrapExports`) is the app packages' keyless
* bin smokes; here we assert the composition + config forwarding.
*/
async function mount(config: agentCore.Config): Promise<Context> {
const oldDshHome = process.env.DSH_HOME
const oldAgentsHome = process.env.DSH_AGENTS_HOME
process.env.DSH_HOME = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-home-'))
process.env.DSH_AGENTS_HOME = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-agents-'))
const ctx = new Context()
try {
await ctx.plugin(agentCore, config)
// The bundle mounts its children inside apply() (not awaited there); let their
// fibers settle so the spine services and any pre-created agent are ready.
await new Promise(resolve => setTimeout(resolve, 50))
return ctx
} 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
}
}
}
async function withIsolatedSkillHomes<T>(run: () => Promise<T>): Promise<T> {
const oldDshHome = process.env.DSH_HOME
const oldAgentsHome = process.env.DSH_AGENTS_HOME
process.env.DSH_HOME = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-home-'))
process.env.DSH_AGENTS_HOME = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-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
}
}
}
function waitForMainIdle(ctx: Context): Promise<void> {
return new Promise((resolve) => {
const dispose = ctx.on('agent/status', (agent, status) => {
if (agent.id === 'main' && status === 'idle') {
dispose()
resolve()
}
})
})
}
function messageText(message: Message | undefined): string {
return message?.content.map(block => block.type === 'text' ? block.text : '').join('\n') ?? ''
}
describe('dsh-agent-spine-demo bundle', () => {
it('brings up the full default spine', async () => {
const ctx = await mount({ workspaceContext: false })
// One service from each layer of the spine proves the children loaded.
expect(ctx.get('timer')).toBeDefined()
expect(ctx.get('llm')).toBeDefined()
expect(ctx.get('sessions')).toBeDefined()
expect(ctx.get('systemPrompt')).toBeDefined()
expect(ctx.get('tools')).toBeDefined()
expect(ctx.get('skills')).toBeDefined()
expect(ctx.get('agents')).toBeDefined()
expect(ctx.get('agentLoop')).toBeDefined()
await ctx.fiber.dispose()
})
it('includes the skill registry, local provider, and skill tool without builtin skills', async () => {
const ctx = await mount({ workspaceContext: false })
expect(ctx.skills).toBeDefined()
expect(ctx.tools.schemas().map(tool => tool.name)).toContain('skill')
expect(await ctx.skills.list()).toEqual([])
await ctx.fiber.dispose()
})
it('defaults the agents list to empty (no pre-created agents)', async () => {
const ctx = await mount({ workspaceContext: false })
expect(ctx.get('agents')?.get(AgentId('main'))).toBeUndefined()
await ctx.fiber.dispose()
})
it('forwards a pre-created agent to the loop and the persona to system-prompt', async () => {
const ctx = await mount({
agents: [{ id: AgentId('main'), model: 'mock' }],
persona: 'You are main.',
workspaceContext: false,
})
expect(ctx.get('agents')?.get(AgentId('main'))).toBeDefined()
const assembly = await ctx.get('systemPrompt')!.assemble()
expect(assembly.sections.find(s => s.name === 'deployment:persona')?.text).toBe('You are main.')
await ctx.fiber.dispose()
})
it('tolerates a schema-bypassing direct apply (the ?? fallbacks fire)', async () => {
// ctx.plugin validates + defaults the bundle config first; a direct apply
// skips the schema, so the forwarding `?? []` / `?? ''` are what fire.
const ctx = new Context()
agentCore.apply(ctx, { workspaceContext: false })
await new Promise(resolve => setTimeout(resolve, 50))
expect(ctx.get('agentLoop')).toBeDefined()
expect(ctx.get('agents')?.list()).toHaveLength(0)
const assembly = await ctx.get('systemPrompt')!.assemble()
expect(assembly.sections.find(s => s.name === 'deployment:persona')?.text).toBe('')
await ctx.fiber.dispose()
})
it('loads workspace instructions into requests through the bundled spine', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-workspace-context-'))
try {
await mkdir(join(root, '.git'), { recursive: true })
await writeFile(join(root, 'AGENTS.md'), 'bundled project rule')
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await mount({ workspaceContext: { maxBytes: 65536 } })
await ctx.plugin(LocalFileSystem, { cwd: '/' })
ctx.llm.registerAdapter(['mock'], adapter)
const handle = await ctx.agents.create({
agentId: AgentId('main'),
sessionId: SessionId('main-session'),
meta: { cwd: root },
agentOptions: { model: 'mock' },
})
const agent = handle.agent
agent.send([{ type: 'text', text: 'hi' }])
await waitForMainIdle(ctx)
const sentText = adapter.requests[0]?.messages.map(messageText).join('\n')
expect(sentText).toContain('hi')
expect(sentText).toContain('bundled project rule')
expect(adapter.requests[0]?.system).toContain('You are an AI agent powered by the DeepSeek Harness SDK.')
expect(adapter.requests[0]?.system).not.toContain('bundled project rule')
await handle.dispose()
await ctx.fiber.dispose()
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('forwards workspace-context config to the bundled loader', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-workspace-context-disabled-'))
try {
await mkdir(join(root, '.git'), { recursive: true })
await writeFile(join(root, 'AGENTS.md'), 'must not be injected')
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await mount({ workspaceContext: { maxBytes: 0 } })
ctx.llm.registerAdapter(['mock'], adapter)
const handle = await ctx.agents.create({
agentId: AgentId('main'),
sessionId: SessionId('main-disabled-session'),
meta: { cwd: root },
agentOptions: { model: 'mock' },
})
handle.agent.send([{ type: 'text', text: 'hi' }])
await waitForMainIdle(ctx)
expect(adapter.requests[0]?.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'hi' }] }])
await handle.dispose()
await ctx.fiber.dispose()
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('forwards skill config to the registry, local provider, and model-facing consumer', async () => {
const home = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-skill-home-'))
const agentsHome = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-skill-agents-'))
const custom = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-skill-custom-'))
await mkdir(custom, { recursive: true })
await writeFile(join(custom, 'custom-skill.md'), '---\nname: custom-skill\ndescription: Custom skill\n---\n\nCustom body.\n')
const ctx = await mount({
agents: [],
workspaceContext: false,
skills: {
registry: { collectCacheMaxEntries: 4 },
local: {
dshHome: join(home, '.dsh'),
agentsHome: join(agentsHome, '.agents'),
customSkillDirs: [custom],
},
tool: { catalogDescriptionMaxLength: 6 },
},
})
expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['custom-skill'])
expect(JSON.stringify(await composePrefix(ctx, '/tmp'))).toContain('- `custom-skill`: Cus...')
await ctx.fiber.dispose()
})
it('places workspace instructions before the skill catalog in the session prefix', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-prefix-order-'))
try {
await mkdir(join(root, '.git'), { recursive: true })
await writeFile(join(root, 'AGENTS.md'), 'workspace rule before skills')
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await mount({ workspaceContext: { maxBytes: 65536 } })
await ctx.plugin(LocalFileSystem, { cwd: '/' })
ctx.llm.registerAdapter(['mock'], adapter)
ctx.skills.register({
name: 'prefix-order-skill',
description: 'Skill catalog after workspace rules',
source: 'runtime',
content: 'body',
})
const handle = await ctx.agents.create({
agentId: AgentId('main'),
sessionId: SessionId('prefix-order-session'),
meta: { cwd: root },
agentOptions: { model: 'mock' },
})
handle.agent.send([{ type: 'text', text: 'hi' }])
await waitForMainIdle(ctx)
expect(messageText(adapter.requests[0]?.messages[0])).toContain('workspace rule before skills')
expect(messageText(adapter.requests[0]?.messages[1])).toContain('prefix-order-skill')
await handle.dispose()
await ctx.fiber.dispose()
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('uses the default skill config when apply is called directly without skills', async () => {
await withIsolatedSkillHomes(async () => {
const ctx = new Context()
agentCore.apply(ctx, { agents: [], workspaceContext: false })
await new Promise(resolve => setTimeout(resolve, 50))
expect(ctx.skills).toBeDefined()
expect(await ctx.skills.list()).toEqual([])
await ctx.fiber.dispose()
})
})
it('forwards toolOrder to the system-prompt assembly', async () => {
const ctx = await mount({ toolOrder: ['zulu', TOOL_ORDER_REST], workspaceContext: false })
// The bundle's own bash tools pend on the absent `ctx.bash` executor in
// this providerless mount, so register two plain tools to order.
for (const name of ['alpha', 'zulu']) {
ctx.get('tools')!.register({
name,
description: name,
parameters: {},
execute: async () => [],
})
}
const assembly = await ctx.get('systemPrompt')!.assemble()
expect(assembly.tools.map(tool => tool.name)).toEqual(['zulu', 'alpha', 'skill'])
await ctx.fiber.dispose()
})
it('supports direct apply with workspace instructions disabled and no forwarded agents', async () => {
const ctx = new Context()
agentCore.apply(ctx, { workspaceContext: false })
await new Promise(resolve => setTimeout(resolve, 50))
expect(ctx.get('agents')?.list()).toEqual([])
expect(ctx.get('systemPrompt')).toBeDefined()
await ctx.fiber.dispose()
})
it('re-exports the loop config schema as its own', () => {
expect(agentCore.Config).toBeDefined()
expect(agentCore.name).toBe('agent-spine-demo')
})
it('has the namespace-plugin export shape (no stray default) so the Loader keeps name/Config/apply', () => {
// A default export would make `unwrapExports` collapse this inject-less namespace and silently
// drop `name`/`Config`. Apps import the bundle directly, so this is its Loader-shape guard.
expect('default' in agentCore).toBe(false)
expect(typeof agentCore.apply).toBe('function')
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(agentCore) as Record<string, unknown>
expect(unwrapped).toBe(agentCore)
expect(unwrapped.name).toBe('agent-spine-demo')
expect(unwrapped.Config).toBeDefined()
expect(typeof unwrapped.apply).toBe('function')
})
})