Merge branch 'codex/simp-ui-identity-residue' into codex/simp-name-front-door-defaults

# Conflicts:
#	docs/config-catalog.md
#	packages/examples/acp-demo/src/index.ts
#	packages/examples/stdio-demo/src/index.ts
This commit is contained in:
Tianyi Cui
2026-07-18 12:42:33 +08:00
657 changed files with 36684 additions and 7561 deletions

View File

@@ -11,7 +11,7 @@ A terminal chat always wants the same cluster, so the package owns it rather tha
| Plugin | Why it is here |
|---|---|
| `@cordisjs/plugin-logger-console` | the console logger — stdout is just the terminal here, so logging to it is correct (the ACP app must NOT have this) |
| `@deepseek-ai/dsh-agent-spine-demo` | the spine, pre-creating one agent under the `main` config label from this app's `model`, with `process.cwd()` as the fresh session cwd and carrying its `persona` |
| `@deepseek-ai/dsh-agent-spine-demo` | the spine, pre-creating a `main` agent from this app's provider/model pair with `process.cwd()` as the fresh session cwd and carrying its `persona` |
| `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log under `persistenceRoot` |
| `@deepseek-ai/dsh-user-interaction` | the human question/answer seam used by confirmation tools |
| `@deepseek-ai/dsh-tool-ask-user` | the model-facing `ask_user_question` tool |
@@ -25,9 +25,11 @@ The leaf `cordis.yml` supplies only the **swappable backends** — an LLM adapte
| Key | Default | Routed to |
|---|---|---|
| `model` | (required) | the pre-created agent's model |
| `persona` | — | the deployment persona template (may reference `{{model}}`/`{{cwd}}`), routed to `dsh-system-prompt` |
| `provider` | (required) | the pre-created `main` agent's registered provider route |
| `model` | (required) | the pre-created `main` agent's model |
| `persona` | — | the deployment persona template (may reference `{{provider}}`/`{{model}}`/`{{cwd}}`), routed to `dsh-system-prompt` |
| `toolOrder` | — | explicit model-facing tool order (a name list with one `'<unlisted-tools>'` rest entry; absent — lexicographic; an unregistered name fails each turn at prompt assembly), routed to `dsh-system-prompt` |
| `dshHome` | `$DSH_HOME` or `~/.dsh` | Harness home exposed to model bash and used by local skill discovery |
| `tools` | `{ mode: 'native' }` | tool-registry presentation config (`native` / `code` / `both`), routed through `dsh-agent-spine-demo` |
| `skills` | owner defaults | registry-cache, local-provider, and model-facing skill-tool config, routed through `dsh-agent-spine-demo` |
| `toolBash` | owner defaults | model-facing bash config routed through `dsh-agent-spine-demo`, including bash's producer-local `enableRunInBackground` |
@@ -54,7 +56,6 @@ Fresh stdio sessions use the process launch directory as `session.header.cwd` an
name: '@deepseek-ai/dsh-llm-deepseek'
config:
apiKey: !!js process.env.DEEPSEEK_API_KEY
models: [deepseek-v4-flash]
- id: bash
name: '@deepseek-ai/dsh-bash-local'
config:
@@ -62,6 +63,7 @@ Fresh stdio sessions use the process launch directory as `session.header.cwd` an
- id: stdio-agent
name: '@deepseek-ai/dsh-stdio-demo'
config:
provider: deepseek
model: deepseek-v4-flash
persona: 'You are a coding assistant powered by the {{model}} model.'
```

View File

@@ -38,6 +38,7 @@
"@deepseek-ai/dsh-agent-loop": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-agent-spine-demo": "^0.0.1",
"@deepseek-ai/dsh-workspace-context": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1",
"@deepseek-ai/dsh-stdio": "^0.0.1",
@@ -57,6 +58,7 @@
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-agent-spine-demo": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-workspace-context": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-stdio": "workspace:^",

View File

@@ -17,6 +17,7 @@ import z from 'schemastery'
import { SessionId } from '@deepseek-ai/dsh-session'
import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
import * as agentCore from '@deepseek-ai/dsh-agent-spine-demo'
import * as workspaceContext from '@deepseek-ai/dsh-workspace-context'
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import * as toolAskUser from '@deepseek-ai/dsh-tool-ask-user'
@@ -28,7 +29,7 @@ const DEFAULT_WELCOME = 'ready.'
/**
* App config: the swappable per-demo values, each routed to where the app wires
* it. `model`/`resumeSessionId` configure the pre-created agent (through
* it. `provider`/`model`/`resumeSessionId` configure the pre-created `main` agent (through
* {@link @deepseek-ai/dsh-agent-spine-demo}'s forwarded `agents` list); `persona` is
* the deployment persona (forwarded to the system-prompt plugin); `toolOrder`
* is the explicit model-facing tool order (forwarded to the system-prompt plugin);
@@ -37,7 +38,9 @@ const DEFAULT_WELCOME = 'ready.'
* `welcome` is the UI banner.
*/
export interface Config {
/** Model name for the pre-created agent (must have a registered adapter). */
/** Provider route for the `main` agent. */
provider: string
/** Model name for the `main` agent (must have a registered adapter). */
model: string
/** Deployment persona (the system-prompt plugin's `persona` config). */
persona?: string
@@ -45,6 +48,8 @@ export interface Config {
toolOrder?: string[]
/** Tool-registry config — its presentation `mode` (forwarded through agent-spine-demo; see dsh-tools). */
tools?: ToolsConfig
/** DeepSeek Harness home directory exposed to bash and used for local skill discovery. */
dshHome?: string
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
persistenceRoot?: string
/** stdin-chat banner printed once on start. Defaults to `'ready.'`. */
@@ -61,9 +66,12 @@ export interface Config {
* (`resumeSessionId: !!js process.env.RESUME_SESSION_ID`).
*/
resumeSessionId?: string
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
workspaceContext: agentCore.Config['workspaceContext']
}
export const Config: z<Config> = z.object({
provider: z.string().required(),
model: z.string().required(),
persona: z.string(),
// The array default is forced to undefined: ABSENT means "lexicographic
@@ -71,12 +79,14 @@ export const Config: z<Config> = z.object({
// schemastery's native [] default would read as an invalid configured list.
toolOrder: z.array(z.string()).default(undefined as unknown as string[]),
tools: ToolRegistry.Config,
dshHome: z.string(),
persistenceRoot: z.string().default(DEFAULT_PERSISTENCE_ROOT),
welcome: z.string().default(DEFAULT_WELCOME),
skills: agentCore.SkillConfigSchema,
toolBash: agentCore.ToolBashConfigSchema,
toolTasks: agentCore.ToolTasksConfigSchema,
resumeSessionId: z.string(),
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
})
/**
@@ -98,18 +108,14 @@ export function apply(ctx: Context, config: Config): void {
sessionId,
})
ctx.plugin(agentCore, {
...config.persona !== undefined ? { persona: config.persona } : {},
...config.toolOrder !== undefined ? { toolOrder: config.toolOrder } : {},
...config.tools !== undefined ? { tools: config.tools } : {},
...agentCore.pickSpineConfig(config),
agents: [{
id: 'main',
id: SessionId('main'),
provider: config.provider,
model: config.model,
cwd: process.cwd(),
...resumeSessionId === undefined ? { sessionId } : { resumeSessionId: sessionId },
}],
...config.skills !== undefined ? { skills: config.skills } : {},
...config.toolBash !== undefined ? { toolBash: config.toolBash } : {},
...config.toolTasks !== undefined ? { toolTasks: config.toolTasks } : {},
})
ctx.plugin(toolAskUser)
}

View File

@@ -21,9 +21,9 @@ const stdioBin = join(repoRoot, 'packages/examples/stdio-demo/lib/bin.js')
const dshPackages = [
'examples/agent-spine-demo', 'core/agent', 'core/session', 'core/system-prompt',
'core/tools', 'core/agent-loop', 'llm/llm', 'bash/bash', 'bash/bash-local',
'bash/tool-bash', 'support/invariants', 'ui/app-boot',
'bash/tool-bash', 'context/workspace-context', 'support/invariants', 'ui/app-boot',
'session-persistence/session-persistence',
'session-persistence/session-persistence-jsonl', 'examples/stdio-demo',
'session-persistence/session-persistence-jsonl', 'examples/stdio-demo', 'util/paths',
'ui/stdio', 'ui/tool-ask-user', 'ui/user-interaction',
]
const vendorPackages = [
@@ -36,20 +36,37 @@ async function pkgName(absDir: string): Promise<string> {
return json.name
}
async function installWorkspacePackageCopy(absDir: string, target: string): Promise<void> {
await mkdir(dirname(target), { recursive: true })
await cp(absDir, target, {
recursive: true,
filter: source => !source.split('/').includes('node_modules'),
})
}
/**
* Build a temporary external consumer with built workspace/vendor links and a mock-backed config.
* The optional missing-but-disabled plugin verifies load guards accept intentionally fiber-less
* entries rather than treating them as import failures.
*/
async function makeConsumer(welcome: string, disabledBrokenEntry = false): Promise<string> {
async function makeConsumer(
welcome: string,
disabledBrokenEntry = false,
extraDshPackages: string[] = [],
extraEntries: string[] = [],
): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), 'stdio-built-bin-'))
const nm = join(dir, 'node_modules')
for (const rel of dshPackages) {
for (const rel of [...dshPackages, ...extraDshPackages]) {
const abs = join(repoRoot, 'packages', rel)
const name = await pkgName(abs)
const target = join(nm, name)
await mkdir(dirname(target), { recursive: true })
await symlink(abs, target)
if (extraDshPackages.includes(rel)) {
await installWorkspacePackageCopy(abs, target)
} else {
await mkdir(dirname(target), { recursive: true })
await symlink(abs, target)
}
}
for (const v of vendorPackages) {
const abs = join(repoRoot, 'vendor', v)
@@ -73,9 +90,12 @@ async function makeConsumer(welcome: string, disabledBrokenEntry = false): Promi
'- id: stdio-agent',
' name: \'@deepseek-ai/dsh-stdio-demo\'',
' config:',
' provider: mock',
' model: mock-echo',
' persona: \'demo\'',
' workspaceContext: false',
` welcome: '${welcome}'`,
...extraEntries,
...disabledBrokenEntry
? ['- id: off', ' name: \'./src/does-not-exist.ts\'', ' disabled: true']
: [],
@@ -147,6 +167,27 @@ describe.skipIf(!existsSync(stdioBin))('dsh-stdio-demo BUILT bin (node lib/bin.j
expect(code).toBe(0)
}, 30_000)
it('boots when optional spill plugins are loaded from a built consumer install', async () => {
consumer = await makeConsumer(
'SPILL-OK ready.',
false,
['spill/spill', 'spill/spill-local', 'spill/spill-policy', 'util/retention'],
[
'- id: spill-local',
' name: \'@deepseek-ai/dsh-spill-local\'',
'- id: spill-policy',
' name: \'@deepseek-ai/dsh-spill-policy\'',
' config:',
' maxInlineBytes: 50000',
],
)
const { stdout, code, stderr } = await runBuiltBin(consumer, './cordis.yml', '')
expect(stderr).not.toContain('failed to load')
expect(stderr).not.toContain('Cannot find package')
expect(stdout).toContain('SPILL-OK ready.')
expect(code).toBe(0)
}, 30_000)
it('fails LOUD (non-zero exit + stderr) on a config whose directory does not exist', async () => {
// boot() pre-resolves the bootstrap include to an absolute URL, so a nonexistent config
// directory cannot break its import; the include plugin's own read must fail loud instead.

View File

@@ -12,7 +12,7 @@ import * as stdioAgent from '../src/index.ts'
/**
* Unit coverage for app composition and config forwarding: console logger, pre-created main agent,
* agent-core spine, JSONL backend, and readline UI. HMR is a Loader-only leaf concern covered by the
* agent-spine-demo spine, JSONL backend, and readline UI. HMR is a Loader-only leaf concern covered by the
* keyless echo smoke; this tier pins the export shape because an inject-less app could otherwise
* survive namespace collapse while silently losing its schema.
*/
@@ -67,8 +67,8 @@ async function withIsolatedSkillHomes<T>(run: () => Promise<T>): Promise<T> {
describe('dsh-stdio-demo app', () => {
it('composes the spine + front-door cluster and pre-creates the main agent', async () => {
const ctx = await mount({ model: 'mock', persona: 'hi', persistenceRoot: '/tmp/dsh-stdio-demo-spec', skills: await isolatedSkillsConfig() })
// The spine services (brought up by the agent-core bundle) are all present.
const ctx = await mount({ provider: 'mock', model: 'mock', persona: 'hi', persistenceRoot: '/tmp/dsh-stdio-demo-spec', skills: await isolatedSkillsConfig(), workspaceContext: false })
// The spine services (brought up by the agent-spine-demo bundle) are all present.
expect(ctx.get('agents')).toBeDefined()
expect(ctx.get('agentLoop')).toBeDefined()
expect(ctx.get('sessionPersistence')).toBeDefined()
@@ -87,10 +87,12 @@ describe('dsh-stdio-demo app', () => {
it('normalizes an empty resume id to a fresh exact app identity', async () => {
const ctx = await mount({
provider: 'mock',
model: 'mock',
resumeSessionId: '',
persistenceRoot: '/tmp/dsh-stdio-agent-spec-empty-resume',
skills: await isolatedSkillsConfig(),
workspaceContext: false,
})
await expect.poll(() => ctx.get('agents')?.list()).toHaveLength(1)
const agent = ctx.get('agents')?.list()[0]
@@ -106,16 +108,30 @@ describe('dsh-stdio-demo app', () => {
// schema-bypassing direct-mount caller.
const ctx = new Context()
// No persona: covers the omitted-persona forwarding branch too.
stdioAgent.apply(ctx, { model: 'mock', skills: await isolatedSkillsConfig() })
await expect.poll(() => ctx.get('sessionPersistence')).toBeDefined()
stdioAgent.apply(ctx, { provider: 'mock', model: 'mock', skills: await isolatedSkillsConfig(), workspaceContext: false })
await expect.poll(() => ctx.get('agents')?.list()).toHaveLength(1)
expect(ctx.get('sessionPersistence')).toBeDefined()
expect(ctx.get('agents')?.list()[0]?.id).toMatch(/^main-session-/)
await ctx.fiber.dispose()
})
it('forwards explicit project-instruction controls to the bundled spine', async () => {
const ctx = await mount({
provider: 'mock',
model: 'mock',
persona: 'hi',
persistenceRoot: '/tmp/dsh-stdio-demo-spec-workspace-context',
workspaceContext: false,
})
await expect.poll(() => ctx.get('agents')?.list()).toHaveLength(1)
expect(ctx.get('agents')?.list()[0]?.id).toMatch(/^main-session-/)
await ctx.fiber.dispose()
})
it('uses default skill config when apply is called directly without skills', async () => {
await withIsolatedSkillHomes(async () => {
const ctx = new Context()
stdioAgent.apply(ctx, { model: 'mock' })
stdioAgent.apply(ctx, { provider: 'mock', model: 'mock', workspaceContext: false })
await new Promise(resolve => setTimeout(resolve, 80))
expect(ctx.skills).toBeDefined()
expect(await ctx.skills.list()).toEqual([])
@@ -128,18 +144,21 @@ describe('dsh-stdio-demo app', () => {
// session the resume is contained + logged, so no agent registers —
// the branch that maps resumeSessionId through is what this covers.
const ctx = await mount({
provider: 'mock',
model: 'mock',
persona: 'hi',
persistenceRoot: '/tmp/dsh-stdio-demo-spec-resume',
resumeSessionId: 'no-such-session',
skills: await isolatedSkillsConfig(),
workspaceContext: false,
})
expect(ctx.get('agents')?.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) })
it('forwards skill config and dshHome into agent-spine-demo', async () => {
const skills = await isolatedSkillsConfig(6)
const ctx = await mount({ provider: 'mock', model: 'mock', persona: 'hi', dshHome: skills.local!.dshHome!, skills, workspaceContext: false })
ctx.skills.register({ name: 'stdio-skill', description: 'Stdio skill', source: 'runtime', content: 'body' })
expect(JSON.stringify(await composePrefix(ctx))).toContain('- `stdio-skill`: Std...')
await ctx.fiber.dispose()
@@ -147,7 +166,9 @@ describe('dsh-stdio-demo app', () => {
it('forwards bundled tool config into agent-core', async () => {
const ctx = await mount({
provider: 'mock',
model: 'mock',
workspaceContext: false,
toolBash: { enableRunInBackground: false },
toolTasks: { waitTimeoutMs: 7, maxWaitTimeoutMs: 11 },
skills: await isolatedSkillsConfig(),
@@ -163,11 +184,13 @@ describe('dsh-stdio-demo app', () => {
expect(stdioAgent.Config).toBeDefined()
})
it('forwards toolOrder through agent-core to the system-prompt assembly', async () => {
it('forwards toolOrder through agent-spine-demo to the system-prompt assembly', async () => {
const ctx = await mount({
provider: 'mock',
model: 'mock',
toolOrder: ['zulu', TOOL_ORDER_REST],
persistenceRoot: '/tmp/dsh-stdio-demo-spec-tool-order',
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.

View File

@@ -32,6 +32,9 @@
{
"path": "../agent-spine-demo"
},
{
"path": "../../context/workspace-context"
},
{
"path": "../../ui/user-interaction"
},