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

# Conflicts:
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	docs/rfc/README.md
#	packages/core/agent-core/package.json
#	packages/core/agent-core/src/index.ts
#	packages/core/agent-loop/README.md
#	packages/core/agent-loop/src/index.ts
#	packages/ui/acp-agent/src/index.ts
#	packages/ui/acp-agent/tests/acp-agent.spec.ts
#	packages/ui/stdio-agent/README.md
#	packages/ui/stdio-agent/src/index.ts
#	packages/ui/stdio-agent/tests/stdio-agent.spec.ts
This commit is contained in:
Yichen Jiang
2026-07-06 10:09:26 +08:00
205 changed files with 2738 additions and 1268 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-core` | the spine, pre-creating a `main` agent from this app's `model`/`systemPrompt` with `process.cwd()` as the fresh session cwd |
| `@deepseek-ai/dsh-agent-core` | the spine, pre-creating a `main` agent from this app's `model` with `process.cwd()` as the fresh session cwd and carrying its `persona` |
| `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log under `persistenceRoot` |
| `stdio-chat` (in-package module) | the readline UI, bound to the `main` agent |
@@ -24,7 +24,7 @@ The leaf `cordis.yml` supplies only the **swappable backends** — an LLM adapte
| Key | Default | Routed to |
|---|---|---|
| `model` | (required) | the pre-created `main` agent's model |
| `systemPrompt` | (required) | the `main` agent's system prompt |
| `persona` | — | the deployment persona template (may reference `{{model}}`), routed to `dsh-system-prompt` |
| `persistenceRoot` | `./.sessions` | the JSONL backend's root directory |
| `welcome` | `ready.` | the stdin-chat banner |
| `resumeSessionId` | — | resume a persisted session id instead of starting fresh (sourced from an env var in the leaf) |
@@ -56,7 +56,7 @@ Fresh stdio sessions use the process launch directory as `session.header.cwd`, s
name: '@deepseek-ai/dsh-stdio-agent'
config:
model: deepseek-v4-flash
systemPrompt: 'You are a CLI coding assistant. Your only tools are bash…'
persona: 'You are a coding assistant powered by the {{model}} model.'
```
Swap `llm-deepseek` for a `mock-llm` leaf plugin and you have the echo demo — "swap the backend, keep the app".

View File

@@ -51,17 +51,18 @@ export const name = 'stdio-agent'
/**
* App config: the swappable per-demo values, each routed to where the app wires
* it. `model`/`systemPrompt`/`resumeSessionId` configure the pre-created `main`
* agent (through {@link @deepseek-ai/dsh-agent-core}'s forwarded `agents` list).
* Fresh sessions use `process.cwd()` as their workspace cwd; resumed sessions
* it. `model`/`resumeSessionId` configure the pre-created `main` agent (through
* {@link @deepseek-ai/dsh-agent-core}'s forwarded `agents` list); `persona` is
* the deployment persona (forwarded to the system-prompt plugin);
* fresh sessions use `process.cwd()` as their workspace cwd; resumed sessions
* keep their persisted cwd. `persistenceRoot` is the JSONL backend's directory;
* `welcome` is the UI banner.
*/
export interface Config {
/** Model name for the `main` agent (must have a registered adapter). */
model: string
/** System prompt for the `main` agent. */
systemPrompt: string
/** Deployment persona (the system-prompt plugin's `persona` config). */
persona?: string
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
persistenceRoot?: string
/** stdin-chat banner printed once on start. Defaults to `'ready.'`. */
@@ -78,7 +79,7 @@ export interface Config {
export const Config: z<Config> = z.object({
model: z.string().required(),
systemPrompt: z.string().required(),
persona: z.string(),
persistenceRoot: z.string().default('./.sessions'),
welcome: z.string().default('ready.'),
skills: agentCore.SkillConfigSchema,
@@ -88,17 +89,17 @@ export const Config: z<Config> = z.object({
/**
* Compose the spine with the stdio front door. The console logger comes first
* (infra), then the agent-core bundle pre-creating the `main` agent from this
* app's `model`/`systemPrompt`/`resumeSessionId`, then the JSONL backend, then
* the readline UI bound to `main`. The `hmr` dev-reload plugin is a leaf
* concern (see the module doc), so it is not mounted here.
* app's `model`/`resumeSessionId` with the deployment `persona`, then the JSONL
* backend, then the readline UI bound to `main`. The `hmr` dev-reload plugin is
* a leaf concern (see the module doc), so it is not mounted here.
*/
export function apply(ctx: Context, config: Config): void {
ctx.plugin(ConsoleExporter)
ctx.plugin(agentCore, {
...config.persona !== undefined ? { persona: config.persona } : {},
agents: [{
id: AgentId('main'),
model: config.model,
systemPrompt: config.systemPrompt,
cwd: process.cwd(),
...config.resumeSessionId !== undefined ? { resumeSessionId: SessionId(config.resumeSessionId) } : {},
}],

View File

@@ -91,7 +91,7 @@ async function makeConsumer(welcome: string, disabledBrokenEntry = false): Promi
' name: \'@deepseek-ai/dsh-stdio-agent\'',
' config:',
' model: mock-echo',
' systemPrompt: \'demo\'',
' persona: \'demo\'',
` welcome: '${welcome}'`,
...disabledBrokenEntry
? ['- id: off', ' name: \'./src/does-not-exist.ts\'', ' disabled: true']

View File

@@ -11,8 +11,9 @@ import * as stdioAgent from '../src/index.ts'
* Unit coverage for the @deepseek-ai/dsh-stdio-agent app plugin: mounting it
* composes the console logger, the agent-core spine (pre-creating the `main`
* agent from the app config), the JSONL backend, and the readline UI in one
* `ctx.plugin`. The forwarded `model`/`systemPrompt` reach the pre-created
* agent; `persistenceRoot`/`welcome`/`resumeSessionId` route to their backends.
* `ctx.plugin`. The forwarded `model` reaches the pre-created agent and
* `persona` the system-prompt plugin; `persistenceRoot`/`welcome`/
* `resumeSessionId` route to their backends.
*
* `hmr` is NOT part of this plugin (it is a leaf entry — a Loader-only dev
* plugin the in-process tier cannot import); the keyless echo smoke in
@@ -60,7 +61,7 @@ async function withIsolatedSkillHomes<T>(run: () => Promise<T>): Promise<T> {
describe('dsh-stdio-agent app', () => {
it('composes the spine + front-door cluster and pre-creates the main agent', async () => {
const ctx = await mount({ model: 'mock', systemPrompt: 'hi', persistenceRoot: '/tmp/dsh-stdio-agent-spec', skills: await isolatedSkillsConfig() })
const ctx = await mount({ model: 'mock', persona: 'hi', persistenceRoot: '/tmp/dsh-stdio-agent-spec', skills: await isolatedSkillsConfig() })
// The spine services (brought up by the agent-core bundle) are all present.
expect(ctx.get('agents')).toBeDefined()
expect(ctx.get('agentLoop')).toBeDefined()
@@ -78,7 +79,8 @@ describe('dsh-stdio-agent app', () => {
// apply()'s last two lines are the ones that fire — covering a
// schema-bypassing direct-mount caller.
const ctx = new Context()
stdioAgent.apply(ctx, { model: 'mock', systemPrompt: 'hi', skills: await isolatedSkillsConfig() })
// No persona: covers the omitted-persona forwarding branch too.
stdioAgent.apply(ctx, { model: 'mock', skills: await isolatedSkillsConfig() })
await new Promise(resolve => setTimeout(resolve, 80))
expect(ctx.get('sessionPersistence')).toBeDefined()
expect(ctx.get('agents')?.get(AgentId('main'))).toBeDefined()
@@ -88,7 +90,7 @@ describe('dsh-stdio-agent app', () => {
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', systemPrompt: 'hi' })
stdioAgent.apply(ctx, { model: 'mock' })
await new Promise(resolve => setTimeout(resolve, 80))
expect(ctx.skills).toBeDefined()
expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(expect.arrayContaining([
@@ -105,7 +107,7 @@ describe('dsh-stdio-agent app', () => {
// the branch that maps resumeSessionId through is what this covers.
const ctx = await mount({
model: 'mock',
systemPrompt: 'hi',
persona: 'hi',
persistenceRoot: '/tmp/dsh-stdio-agent-spec-resume',
resumeSessionId: 'no-such-session',
skills: await isolatedSkillsConfig(),
@@ -115,7 +117,7 @@ describe('dsh-stdio-agent app', () => {
})
it('forwards skill config into agent-core', async () => {
const ctx = await mount({ model: 'mock', systemPrompt: 'hi', skills: await isolatedSkillsConfig() })
const ctx = await mount({ model: 'mock', persona: 'hi', skills: await isolatedSkillsConfig() })
expect(await ctx.skills.list()).toEqual([])
await ctx.fiber.dispose()
})