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

This commit is contained in:
Yichen Jiang
2026-07-09 23:11:10 +08:00
438 changed files with 24091 additions and 1723 deletions

View File

@@ -39,12 +39,11 @@ This is the [interface/implementation/consumer seam](../../../docs/rfc/implement
```ts
import type { Config } from '@deepseek-ai/dsh-agent-core'
// { agents?, persona?, skills? } — the schema is z.intersect([AgentLoop.Config,
// SystemPrompt.Config, { skills }]), so validation and defaulting can never
// drift from the owners'.
// { agents?, persona?, toolOrder?, skills? } — the schema intersects the owner schemas,
// so validation and defaulting can never drift from the owners.
```
The bundle FORWARDS each field to the child that owns it: `agents` to `agent-loop` (default `[]`), so each app supplies its own pre-created agents — a stdio app pre-creates a `main`; the ACP app pre-creates none (it creates agents on demand at `session/new`) — `persona` to `dsh-system-prompt` (default `''`), and `skills.registry` / `skills.local` to the skill registry and local provider. Forwarding is exactly why the owners can live in the shared spine even though the apps disagree on what to configure.
The bundle FORWARDS each field to the child that owns it: `agents` to `agent-loop` (default `[]`), so each app supplies its own pre-created agents — a stdio app pre-creates a `main`; the ACP app pre-creates none (it creates agents on demand at `session/new`) — `persona` to `dsh-system-prompt` (default `''`), the deployment's persona section; `toolOrder` to `dsh-system-prompt` (absent — lexicographic), the explicit model-facing tool order; and `skills.registry` / `skills.local` to the skill registry and local provider. Forwarding is exactly why the owners can live in the shared spine even though the apps disagree on what to configure.
## Why a code bundle, not a shared YAML include

View File

@@ -73,17 +73,21 @@ export interface SkillConfig {
/**
* Bundle config: each field forwarded verbatim to the child that owns it —
* `agents` to the agent loop (an app that pre-creates no agents, like the ACP
* bridge, simply omits it), `persona` to the system-prompt plugin (the
* deployment's persona section), and `skills` to the skill registry/local
* provider. All three are optional INPUT here because each owner's schema supplies the default
* (`[]` / `''` / the DSH skill roots); the schema is the INTERSECTION of the
* owners' own schemas, so validation and defaulting can never drift from them.
* bridge, simply omits it), `persona` and `toolOrder` to the system-prompt
* plugin (the deployment's persona section and the explicit model-facing tool
* order), and `skills` to the skill registry/local provider. Every field is
* optional INPUT here because each owner's schema supplies the default (`[]` /
* `''` / absent — lexicographic / the DSH skill roots); the schema is the
* INTERSECTION of the owners' own schemas, so validation and defaulting can
* never drift from them.
*/
export interface Config {
/** The agent-loop `agents` list (see dsh-agent-loop's `Config`). */
agents?: AgentLoopConfig['agents']
/** The deployment persona (see dsh-system-prompt's `Config`). */
persona?: SystemPromptConfig['persona']
/** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */
toolOrder?: SystemPromptConfig['toolOrder']
/** Skill registry and local provider config. */
skills?: SkillConfig
}
@@ -104,11 +108,11 @@ export const Config = z.intersect([
/**
* Load the spine. Each `ctx.plugin(...)` mounts one child of the bundle fiber;
* `agent-loop` receives the forwarded `agents` list and `system-prompt` the
* forwarded `persona`. Load order is irrelevant (cordis pends each fiber on
* its `inject` until the services it needs exist), but the listing mirrors the
* dependency layering for readability: the LLM vocabulary and core registries
* first, then the dev tripwire and the bash tool consumer, then the loop that
* drives them.
* forwarded `persona` and `toolOrder`. Load order is irrelevant (cordis pends
* each fiber on its `inject` until the services it needs exist), but the
* listing mirrors the dependency layering for readability: the LLM vocabulary
* and core registries first, then the dev tripwire and the bash tool consumer,
* then the loop that drives them.
*/
export function apply(ctx: Context, config: Config): void {
ctx.plugin(Timer)
@@ -117,8 +121,13 @@ export function apply(ctx: Context, config: Config): void {
// The forwarded fields are validated + defaulted by this bundle's intersected
// schema before apply runs, so the ?? fallbacks only narrow the
// optional-input TYPES — they mirror the owners' schema defaults, never
// introduce different ones.
ctx.plugin(SystemPrompt, { persona: config.persona ?? '' })
// introduce different ones. toolOrder has no owner-supplied default value —
// ABSENT means "lexicographic order" — so it is forwarded conditionally
// rather than via ??.
ctx.plugin(SystemPrompt, {
persona: config.persona ?? '',
...config.toolOrder !== undefined ? { toolOrder: config.toolOrder } : {},
})
ctx.plugin(ToolRegistry)
ctx.plugin(SkillService, config.skills?.registry ?? {})
ctx.plugin(SkillLocal, config.skills?.local ?? {})

View File

@@ -4,6 +4,7 @@ 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 } from '@deepseek-ai/dsh-agent'
@@ -152,6 +153,23 @@ describe('dsh-agent-core bundle', () => {
})
})
it('forwards toolOrder to the system-prompt assembly', async () => {
const ctx = await mount({ toolOrder: ['zulu', TOOL_ORDER_REST] })
// 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('re-exports the loop config schema as its own', () => {
expect(agentCore.Config).toBeDefined()
expect(agentCore.name).toBe('agent-core')

View File

@@ -0,0 +1,458 @@
/**
* Negative-path tests for the config catalog generator (`scripts/gen-config-catalog.ts`).
*
* The generated catalog is frozen by a regenerate-and-diff freshness gate, so
* the freshness half is exercised by `pnpm run verify-config-catalog` in CI.
* What a freshness diff CANNOT prove is that the generator REJECTS malformed
* source the way it promises to — an unclassifiable package, an undocumented
* config field, a schema key the config type does not declare, or a referenced
* type name that resolves nowhere. These tests drive `collectConfigCatalog()`
* against synthetic fixture packages to prove each guard fires (and that
* well-formed packages classify and extract correctly), mirroring the
* negative tests for gen-cordis-catalog. The spec lives in this package
* because agent-core is the config-composition plugin (its schema is the
* intersection of its children's), the shape the generator's cross-package
* folding exists for.
*/
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { collectConfigCatalog, render } from '../../../../scripts/gen-config-catalog.ts'
/** Write one fixture package (package.json + src files) under a scan root. */
function writePkg(root: string, dir: string, name: string, files: Record<string, string>): void {
const pkgDir = join(root, 'packages', dir)
mkdirSync(join(pkgDir, 'src'), { recursive: true })
writeFileSync(join(pkgDir, 'package.json'), JSON.stringify({ name }))
for (const [rel, text] of Object.entries(files)) writeFileSync(join(pkgDir, rel), text)
}
const roots: string[] = []
const makeRoot = (): string => {
const root = mkdtempSync(join(tmpdir(), 'config-catalog-'))
roots.push(root)
return root
}
/** One-package fixture: the common case. */
const make = (files: Record<string, string>, name = '@fix/one'): string => {
const root = makeRoot()
writePkg(root, 'group/one', name, files)
return root
}
afterEach(() => {
while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
})
const DOCUMENTED_CONFIG = `/** Fixture config. */
export interface Config {
/** A knob. */
knob?: string
}
`
describe('gen-config-catalog classification', () => {
it('classifies an apply plugin with a config parameter and extracts the paste', () => {
const entries = collectConfigCatalog(make({
'src/index.ts': `import type { Context } from 'cordis'
export const inject = ['tools']
${DOCUMENTED_CONFIG}
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
}))
expect(entries).toHaveLength(1)
expect(entries[0]).toMatchObject({ pkg: '@fix/one', kind: 'config', configTypeName: 'Config', inject: ['tools'] })
expect(entries[0]?.pastes?.[0]?.text).toContain('/** A knob. */')
})
it('classifies a default service class, reading its constructor and static inject', () => {
const entries = collectConfigCatalog(make({
'src/index.ts': `import type { Context } from 'cordis'
import z from 'schemastery'
${DOCUMENTED_CONFIG}
/** Fixture service. */
export default class Fix {
static inject = ['llm']
static Config = z.object({ knob: z.string() }) as unknown as z<Config>
constructor(ctx: Context, config: Config) {}
}
`,
}))
expect(entries[0]).toMatchObject({ kind: 'config', className: 'Fix', inject: ['llm'], schemaKeys: ['knob'] })
})
it('classifies an abstract default class as a seam', () => {
const entries = collectConfigCatalog(make({
'src/index.ts': 'export default abstract class FixSeam { abstract run(): void }\n',
}))
expect(entries[0]).toMatchObject({ kind: 'seam', className: 'FixSeam' })
})
it('classifies a plugin whose apply takes no config as no-config', () => {
const entries = collectConfigCatalog(make({
'src/index.ts': 'import type { Context } from \'cordis\'\n/** Load. */\nexport function apply(ctx: Context): void {}\n',
}))
expect(entries[0]?.kind).toBe('no-config')
})
it('classifies a module with neither default export nor apply as a library', () => {
const entries = collectConfigCatalog(make({
'src/index.ts': 'export const helper = 1\n',
}))
expect(entries[0]?.kind).toBe('library')
})
it('hard-errors on a package with no entry file', () => {
const root = makeRoot()
mkdirSync(join(root, 'packages', 'group', 'one'), { recursive: true })
writeFileSync(join(root, 'packages', 'group', 'one', 'package.json'), JSON.stringify({ name: '@fix/one' }))
expect(() => collectConfigCatalog(root)).toThrow(/entry .* is missing or unreadable/)
})
it('hard-errors on a package.json without a name', () => {
const root = makeRoot()
mkdirSync(join(root, 'packages', 'group', 'one', 'src'), { recursive: true })
writeFileSync(join(root, 'packages', 'group', 'one', 'package.json'), '{}')
expect(() => collectConfigCatalog(root)).toThrow(/has no "name"/)
})
})
describe('gen-config-catalog config extraction guards', () => {
it('hard-errors on a config field with no JSDoc prose', () => {
expect(() => collectConfigCatalog(make({
'src/index.ts': `import type { Context } from 'cordis'
export interface Config {
knob?: string
}
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
}))).toThrow(/config field 'Config\.knob' .* has no JSDoc prose/)
})
it('hard-errors on an undocumented field nested in a type literal', () => {
expect(() => collectConfigCatalog(make({
'src/index.ts': `import type { Context } from 'cordis'
/** Fixture config. */
export interface Config {
/** Entries. */
entries: {
id: string
}[]
}
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
}))).toThrow(/config field 'Config\.entries\.id' .* has no JSDoc prose/)
})
it('pastes a package-local type transitively and records external refs', () => {
const entries = collectConfigCatalog(make({
'src/index.ts': `import type { Context } from 'cordis'
import type { Mode } from './types.ts'
import type { Remote } from '@fix/dep'
/** Fixture config. */
export interface Config {
/** The mode. */
mode?: Mode
/** The remote. */
remote?: Remote
}
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
'src/types.ts': '/** Fixture mode. */\nexport type Mode = \'a\' | \'b\'\n',
}))
expect(entries[0]?.pastes?.map(p => p.source)).toEqual([
'packages/group/one/src/index.ts:5',
'packages/group/one/src/types.ts:2',
])
expect(entries[0]?.refs).toEqual([{ alias: 'Remote', imported: 'Remote', specifier: '@fix/dep' }])
})
it('hard-errors on a referenced type name that resolves nowhere', () => {
expect(() => collectConfigCatalog(make({
'src/index.ts': `import type { Context } from 'cordis'
/** Fixture config. */
export interface Config {
/** The ghost. */
ghost?: Ghost
}
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
}))).toThrow(/references 'Ghost' .* neither declared in the package, imported, nor a known global/)
})
it('hard-errors on a config type imported from another package', () => {
expect(() => collectConfigCatalog(make({
'src/index.ts': `import type { Context } from 'cordis'
import type { Config } from '@fix/dep'
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
}))).toThrow(/config type 'Config' is imported from '@fix\/dep'/)
})
it('hard-errors when one name resolves to two different declarations across the closure', () => {
expect(() => collectConfigCatalog(make({
'src/index.ts': `import type { Context } from 'cordis'
import type { A } from './a.ts'
import type { B } from './b.ts'
/** Fixture config. */
export interface Config {
/** A. */
a?: A
/** B. */
b?: B
}
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
'src/a.ts': '/** First Option. */\nexport interface Option {\n /** X. */\n x?: string\n}\n/** A. */\nexport interface A {\n /** O. */\n o?: Option\n}\n',
'src/b.ts': '/** Second Option. */\nexport interface Option {\n /** Y. */\n y?: string\n}\n/** B. */\nexport interface B {\n /** O. */\n o?: Option\n}\n',
}))).toThrow(/type name 'Option' resolves to two different declarations/)
})
})
describe('gen-config-catalog schema cross-check', () => {
it('accepts a chained schema whose keys all appear on the config type', () => {
const entries = collectConfigCatalog(make({
'src/index.ts': `import type { Context } from 'cordis'
import z from 'schemastery'
${DOCUMENTED_CONFIG}
export const Config: z<Config> = z.object({ knob: z.string() }).default({})
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
}))
expect(entries[0]?.schemaKeys).toEqual(['knob'])
})
it('hard-errors on a schema key the config type does not declare', () => {
expect(() => collectConfigCatalog(make({
'src/index.ts': `import type { Context } from 'cordis'
import z from 'schemastery'
${DOCUMENTED_CONFIG}
export const Config: z<Config> = z.object({ knob: z.string(), hidden: z.number() })
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
}))).toThrow(/schema validates key 'hidden' but config type 'Config' declares no such member/)
})
it('hard-errors on a NESTED schema key the config type does not declare', () => {
expect(() => collectConfigCatalog(make({
'src/index.ts': `import type { Context } from 'cordis'
import z from 'schemastery'
/** Fixture config. */
export interface Config {
/** Entries. */
entries: {
/** Id. */
id: string
}[]
}
export const Config: z<Config> = z.object({ entries: z.array(z.object({ id: z.string(), ghost: z.string() })) })
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
}))).toThrow(/schema validates key 'entries\[\]\.ghost'/)
})
it('resolves nested keys through a workspace-imported intersection part (re-export chains included)', () => {
const root = makeRoot()
writePkg(root, 'group/dep', '@fix/dep', {
'src/index.ts': 'export * from \'./types.ts\'\n',
'src/types.ts': '/** Shared options. */\nexport interface Opts {\n /** Model. */\n model?: string\n}\n',
})
writePkg(root, 'group/one', '@fix/one', {
'src/index.ts': `import type { Context } from 'cordis'
import z from 'schemastery'
import type { Opts } from '@fix/dep'
/** Fixture config. */
export interface Config {
/** Entries. */
entries: (Opts & {
/** Id. */
id: string
})[]
}
export const Config: z<Config> = z.object({ entries: z.array(z.object({ id: z.string(), model: z.string() })) })
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
})
expect(() => collectConfigCatalog(root)).not.toThrow()
})
it('resolves nested keys through a Partial<> wrapper', () => {
expect(() => collectConfigCatalog(make({
'src/index.ts': `import type { Context } from 'cordis'
import z from 'schemastery'
/** Caps. */
export interface Caps {
/** X. */
x?: boolean
}
/** Fixture config. */
export interface Config {
/** Capabilities. */
capabilities?: Partial<Caps>
}
export const Config: z<Config> = z.object({ capabilities: z.object({ x: z.boolean() }) })
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
}))).not.toThrow()
})
it('leaves a nested key under an external (unresolvable) type unreported', () => {
expect(() => collectConfigCatalog(make({
'src/index.ts': `import type { Context } from 'cordis'
import z from 'schemastery'
import type { External } from 'some-external-pkg'
/** Fixture config. */
export interface Config {
/** Options. */
options?: External
}
export const Config: z<Config> = z.object({ options: z.object({ whatever: z.string() }) })
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
}))).not.toThrow()
})
it('folds an intersected workspace schema into the subset check', () => {
const root = makeRoot()
writePkg(root, 'group/leaf', '@fix/leaf', {
'src/index.ts': `import type { Context } from 'cordis'
import z from 'schemastery'
/** Leaf config. */
export interface Config {
/** Leaf knob. */
leaf?: string
}
/** Leaf service. */
export default class Leaf {
static Config = z.object({ leaf: z.string() }) as unknown as z<Config>
constructor(ctx: Context, config: Config) {}
}
`,
})
writePkg(root, 'group/bundle', '@fix/bundle', {
'src/index.ts': `import type { Context } from 'cordis'
import z from 'schemastery'
import Leaf from '@fix/leaf'
/** Bundle config. */
export interface Config {
/** Forwarded leaf knob. */
leaf?: string
}
export const Config = z.intersect([Leaf.Config]) as unknown as z<Config>
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
})
const entries = collectConfigCatalog(root)
expect(entries.find(e => e.pkg === '@fix/bundle')?.schemaComposes).toEqual(['@fix/leaf'])
})
it('resolves composed nested keys through an indexed-access forwarder', () => {
const root = makeRoot()
writePkg(root, 'group/leaf', '@fix/leaf', {
'src/index.ts': `import type { Context } from 'cordis'
import z from 'schemastery'
/** Leaf config. */
export interface Config {
/** Agents. */
agents: {
/** Id. */
id: string
}[]
}
/** Leaf service. */
export default class Leaf {
static Config = z.object({ agents: z.array(z.object({ id: z.string() })) }) as unknown as z<Config>
constructor(ctx: Context, config: Config) {}
}
`,
})
writePkg(root, 'group/bundle', '@fix/bundle', {
'src/index.ts': `import type { Context } from 'cordis'
import z from 'schemastery'
import Leaf, { type Config as LeafConfig } from '@fix/leaf'
/** Bundle config forwarding the leaf's agents list. */
export interface Config {
/** Forwarded agents list. */
agents?: LeafConfig['agents']
}
export const Config = z.intersect([Leaf.Config]) as unknown as z<Config>
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
})
expect(() => collectConfigCatalog(root)).not.toThrow()
})
it('hard-errors when an intersected schema key is missing from the bundle config type', () => {
const root = makeRoot()
writePkg(root, 'group/leaf', '@fix/leaf', {
'src/index.ts': `import type { Context } from 'cordis'
import z from 'schemastery'
/** Leaf config. */
export interface Config {
/** Leaf knob. */
leaf?: string
}
/** Leaf service. */
export default class Leaf {
static Config = z.object({ leaf: z.string() }) as unknown as z<Config>
constructor(ctx: Context, config: Config) {}
}
`,
})
writePkg(root, 'group/bundle', '@fix/bundle', {
'src/index.ts': `import type { Context } from 'cordis'
import z from 'schemastery'
import Leaf from '@fix/leaf'
/** Bundle config that forgot to declare the forwarded field. */
export interface Config {
/** Unrelated. */
other?: string
}
export const Config = z.intersect([Leaf.Config]) as unknown as z<Config>
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
})
expect(() => collectConfigCatalog(root)).toThrow(/schema validates key 'leaf' but config type 'Config' declares no such member/)
})
})
describe('gen-config-catalog render', () => {
it('renders sections, fences, and the terse classification lists', () => {
const root = makeRoot()
writePkg(root, 'group/one', '@fix/one', {
'src/index.ts': `import type { Context } from 'cordis'
${DOCUMENTED_CONFIG}
/** Load. */
export function apply(ctx: Context, config: Config): void {}
`,
})
writePkg(root, 'group/lib', '@fix/lib', { 'src/index.ts': 'export const helper = 1\n' })
writePkg(root, 'group/seam', '@fix/seam', {
'src/index.ts': 'export default abstract class Seam { abstract run(): void }\n',
})
const page = render(collectConfigCatalog(root))
expect(page).toContain('## `@fix/one`')
expect(page).toContain('```ts config-catalog')
expect(page).toContain('/** A knob. */')
expect(page).toContain('- `@fix/lib` ([`packages/group/lib/src/index.ts`](../packages/group/lib/src/index.ts))')
expect(page).toContain('- `@fix/seam` — abstract `Seam`')
})
})