Merge remote-tracking branch 'origin/stack/agent-profiles-1-seam' into stack/agent-profiles-3-wire

# Conflicts:
#	docs/module-graph.md
#	packages/host/apiproxy/src/api-proxy.ts
#	packages/host/apiproxy/tsconfig.json
This commit is contained in:
Yichen Jiang
2026-08-09 20:33:55 +08:00
1623 changed files with 15440 additions and 5453 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/cli/README.md
README.md: 12108fcbff4e649d0bcb3e01e688fa334ab91b14
README.zh.md: 37967973ed40143db0bc747875dbb6dad11d73b4
README.md: dd29f7fc03a783079ea3194de99589c1f545be5b
README.zh.md: 60e7aa1ec1ea2fad7e3f3d97a0f6bf42355adffc

View File

@@ -9,7 +9,7 @@ The `dsh` command is the product launcher for profiles: ordered stacks of plugin
| Command | Purpose |
|---|---|
| `dsh --profile <name>` | Boot the named profile under `$DSH_HOME/profiles/<name>`. |
| `dsh run [--profile <name>] [--patch <path>...] "task"` | Run one fresh persisted session, print the final answer, and exit; the profile defaults to `headless`. |
| `dsh run [--profile <name>] [--patch <path>...] "task"` | Run one fresh persisted session directly over core, print the final answer, and exit; the profile defaults to `headless` and mounts no Web server. |
| `dsh web` | Alias of `--profile web` with the Web flag family (`--host`, `--port`, `--dev`, ...). |
| `dsh plugin --profile <name> <pnpm args>` | Manage a profile's plugins by forwarding to pnpm in the profile directory. |

View File

@@ -9,7 +9,7 @@
| 命令 | 用途 |
|---|---|
| `dsh --profile <name>` | 启动位于 `$DSH_HOME/profiles/<name>` 的指定 profile。 |
| `dsh run [--profile <name>] [--patch <path>...] "task"` | 运行一个新的持久化会话打印最终答案并退出profile 默认为 `headless`。 |
| `dsh run [--profile <name>] [--patch <path>...] "task"` | 直接在 core 上运行一个新的持久化会话打印最终答案并退出profile 默认为 `headless`,且不挂载 Web server。 |
| `dsh web` | `--profile web` 的别名,附带 Web flag 系列(`--host``--port``--dev` 等)。 |
| `dsh plugin --profile <name> <pnpm args>` | 通过在 profile 目录中转发给 pnpm 来管理该 profile 的插件。 |

View File

@@ -32,6 +32,8 @@ flowchart LR
cfg --> plugin_dsh_base_user_interaction
plugin_dsh_base_agent["agent<br/>@deepseek-ai/dsh-agent"]
cfg --> plugin_dsh_base_agent
plugin_dsh_base_agent_default_model["agent-default-model<br/>@deepseek-ai/dsh-agent-default-model"]
cfg --> plugin_dsh_base_agent_default_model
plugin_dsh_base_tasks["tasks<br/>@deepseek-ai/dsh-tasks-local"]
cfg --> plugin_dsh_base_tasks
plugin_dsh_base_llm_retry["llm-retry<br/>@deepseek-ai/dsh-llm-retry"]
@@ -174,6 +176,7 @@ flowchart LR
| `session-title-llm` | `@deepseek-ai/dsh-session-title-first-message-llm` |
| `user-interaction` | `@deepseek-ai/dsh-user-interaction` |
| `agent` | `@deepseek-ai/dsh-agent` |
| `agent-default-model` | `@deepseek-ai/dsh-agent-default-model` |
| `tasks` | `@deepseek-ai/dsh-tasks-local` |
| `llm-retry` | `@deepseek-ai/dsh-llm-retry` |
| `settings` | `@deepseek-ai/dsh-settings-local` |

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/cli/reference/README.md
README.md: e616b420d60e0fa372bf38950207245cb60ced98
README.zh.md: 81320ecbd19012123ab7e69967cbadb088356cfe
README.md: bb3e1d77f00e03f6a21f4da3c04994f208e84293
README.zh.md: 3802f940ef6663ec215cf09293a9edda5ea4c512

View File

@@ -10,7 +10,7 @@ This reference defines the profile, one-shot run, web-alias, plugin-management,
Bundle names resolve from the dsh installation first, then from the profile directory. In-box bundles (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`) therefore always come from the same installation as the running `dsh`; out-of-tree bundles come from the profile's pnpm-managed `node_modules`. A bare plugin `name` in any patch row resolves through the profile directory's Node parent-walk, which reaches the maintained installation fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch).
The `web` and `headless` profiles auto-initialize from shipped templates on first use (`web`: base + web-app; `headless`: base + web-app + headless). Any other missing profile fails loud with a hint to run `dsh plugin --profile <name> add <package>`.
The `web` and `headless` profiles auto-initialize from shipped templates on first use (`web`: base + web-app; `headless`: base + headless). On load, the exact installation-owned headless tuple (base + web-app + headless) normalizes to the shipped template; extra, missing, or reordered bundle lists are user-owned and remain untouched. Any other missing profile fails loud with a hint to run `dsh plugin --profile <name> add <package>`.
Profile boot accepts no positional task. A profile that mounts the one-shot runner row (`headless-runner`) therefore fails loud with the canonical `dsh run --profile <name> "<task>"` command instead of reaching the row's raw required-field error.
@@ -21,13 +21,13 @@ dsh --profile web --dump-default-config
dsh --profile web --patch ./extra.yml --dump-config
```
`--dump-default-config` prints only the bundle layers; `--dump-config` adds the profile's `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml`, and `--patch` overlays. Both print provenance comments per layer; `!!js` expressions remain unevaluated, and unmatched patch targets are reported on stderr.
`--dump-default-config` prints only the bundle layers; `--dump-config` adds the profile's `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml`, and `--patch` overlays. Both print comments naming the file that supplied each row and every overlay that changed it; `!!js` expressions remain unevaluated, and unmatched patch targets are reported on stderr.
## One-shot run
`dsh run [--profile <name>] [--patch <path>...] <task...>` joins the task arguments with spaces, rejects a missing or blank task, and defaults `--profile` to `headless`. Repeatable `--patch` overlays occupy the same layer position as profile-boot overlays. A custom selected profile must mount `headless-runner`; otherwise launch fails before boot with a diagnostic naming that missing row.
The launcher patches the task text into the runner row, which drives one fresh persisted session through the in-process API carrier, prints the final assistant text on stdout, and exits 0 on a completed turn, else 1. At the idle boundary, the runner waits until its mux consumer has observed the session's final event sequence before deriving that output and exit reason. The session's Web host runs on an OS-assigned port and is announced on stderr, so the run is observable in a browser.
The launcher patches the task text into the runner row. After Loader settlement, the runner reads the shared `ctx.agentDefaultModel` default, creates one fresh persisted Agent through `ctx.agents`, submits the task, waits for quiescence, and flushes the Session before deriving the last non-empty assistant text and final `turn/end` reason from its durable interval. It prints the text on stdout and exits 0 for `completed`, else 1. The shipped headless profile mounts no ApiProxy, Host, HTTP server, Web runtime, or browser client; a successful run writes nothing to stderr and opens no listening port.
## Plugin management

View File

@@ -10,7 +10,7 @@
组合包名称先从 dsh 安装解析,再从 profile 目录解析。因此内置组合包(`@deepseek-ai/dsh-base``@deepseek-ai/dsh-web-app``@deepseek-ai/dsh-headless`)总是来自与正在运行的 `dsh` 相同的安装;树外组合包来自 profile 由 pnpm 管理的 `node_modules`。任何 patch 行中的裸插件 `name` 通过 profile 目录的 Node 父目录逐级查找解析,该查找可达到持续维护的安装后备目录 `$DSH_HOME/profiles/node_modules`(安装的应用和组合包所依赖的每个包对应一个符号链接,每次启动时修复)。
`web``headless` profile 首次使用时会从随附模板自动初始化(`web`base + web-app`headless`base + web-app + headless。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile <name> add <package>`
`web``headless` profile 首次使用时会从随附模板自动初始化(`web`base + web-app`headless`base + headless。加载时,与安装所管理的 headless 元组base + web-app + headless完全一致的列表会规范化为随附模板包含额外项、缺少项或调整过顺序的组合包列表由用户拥有保持不变。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile <name> add <package>`
Profile 启动不接受位置参数任务。因此,挂载了一次性运行器行(`headless-runner`)的 profile 会显式报错,并提示规范命令 `dsh run --profile <name> "<task>"`,而不会触发该行原始的必填字段错误。
@@ -21,13 +21,13 @@ dsh --profile web --dump-default-config
dsh --profile web --patch ./extra.yml --dump-config
```
`--dump-default-config` 只打印组合包各层;`--dump-config` 额外加上 profile 的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml``--patch` overlay。两者都会按层打印来源注释`!!js` 表达式保持未求值,找不到目标的 patch 会报告到 stderr。
`--dump-default-config` 只打印组合包各层;`--dump-config` 额外加上 profile 的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml``--patch` overlay。两者都会打印注释,标明每行由哪个文件提供,以及哪些 overlay 修改过它`!!js` 表达式保持未求值,找不到目标的 patch 会报告到 stderr。
## 一次性运行
`dsh run [--profile <name>] [--patch <path>...] <task...>` 会用空格拼接任务参数,拒绝缺失或空白任务,并让 `--profile` 默认为 `headless`。可重复使用的 `--patch` overlay 与 profile 启动的 overlay 位于同一层。所选的自定义 profile 必须挂载 `headless-runner`;否则启动器会在启动前失败,并在诊断中指明缺少该行。
启动器把任务文本 patch 进运行器行,运行器再通过进程内 API 载体驱动一个全新的持久化会话,在 stdout 打印最终 assistant 文本,并在轮次完成时以 0 退出,否则以 1 退出。到达 idle 边界时,运行器会等到 mux 消费方观察到会话的最终事件序号,再生成输出与退出原因。会话的 Web 宿主运行在 OS 分配的端口上并公布到 stderr因此该次运行可在浏览器中观察
启动器把任务文本 patch 进运行器行。Loader 结算后,运行器读取共享的 `ctx.agentDefaultModel` 默认值,通过 `ctx.agents` 创建一个全新的持久化 Agent智能体提交任务、等待完全停稳并对 Session 执行 flush再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 stdout 打印文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出。随附 headless profile 不挂载 ApiProxy、Host、HTTP 服务器、Web 运行时或浏览器客户端;成功运行不会向 stderr 写入任何内容,也不会打开监听端口
## 插件管理

View File

@@ -1,7 +1,7 @@
/**
* Config-dump entry for `dsh --profile <name> --dump-config`: compose the
* profile's patch layers through the include plugin's patch algorithm without
* booting or evaluating `!!js`, with one provenance layer per bundle, the
* booting or evaluating `!!js`, with one source layer per bundle, the
* profile's own patch file, and each `--patch` overlay.
* @module @deepseek-ai/dsh/dump-config
*/
@@ -20,7 +20,7 @@ const NAME = 'dsh'
/* v8 ignore start -- built-bin acceptance drives this boot-free dispatch */
/**
* Print a profile composition with provenance comments.
* Print a profile composition with comments naming each source file and patch layer.
* @param profile - the profile name.
* @param defaultOnly - omit the profile's user layer and `--patch` overlays
* (the recovery diagnostic for a broken `cordis.patch.yml`, which is then

View File

@@ -222,7 +222,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
})
expect(result.code, result.stderr).toBe(0)
expect(result.stdout).toBe('published dsh run reached the mock')
expect(result.stderr).toMatch(/^dsh: observing at http:\/\/127\.0\.0\.1:\d+$/u)
expect(result.stderr).toBe('')
expect(server.requests.length).toBeGreaterThan(0)
expect(server.requests.every(request => request.path === '/chat/completions')).toBe(true)
expect(JSON.stringify(server.requests.map(request => request.body))).toContain('answer from the published entry')
@@ -470,6 +470,20 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
expect(stdout).toContain("name: '@deepseek-ai/dsh-host-webserver'")
}, 30_000)
it('prints a headless profile with no Host, HTTP, or browser rows', async () => {
const { stdout, code, stderr } = await runBuiltBin(
['--profile', 'headless', '--dump-default-config'],
{ DSH_HOME: home },
)
expect(code).toBe(0)
expect(stderr).toBe('')
expect(stdout).toContain("name: '@deepseek-ai/dsh-agent-default-model'")
expect(stdout).toContain("name: '@deepseek-ai/dsh-headless'")
expect(stdout).not.toContain("name: '@deepseek-ai/dsh-host-")
expect(stdout).not.toContain("name: '@deepseek-ai/dsh-web-app'")
expect(stdout).not.toContain("name: '@deepseek-ai/dsh-client-")
}, 30_000)
it('composes the profile user layer and a --patch overlay in order', async () => {
// Auto-init the web profile first, then write its user layer.
const init = await runBuiltBin(['--profile', 'web', '--dump-default-config'], { DSH_HOME: home })
@@ -504,7 +518,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
expect(code).toBe(0)
expect(stdout).toContain('provider: configured-provider')
expect(stdout).not.toContain('personal-provider')
// Both layers patched the row; provenance lists them in application order.
// Both layers patched the row; the comment lists them in application order.
expect(stdout).toContain(`patched by ${profilePatch}, ${overlay}`)
expect(stderr).toContain('patch: entry "absent-row" not found')
}, 30_000)

View File

@@ -8,6 +8,7 @@ import { existsSync } from 'node:fs'
*/
export function apply(ctx) {
const keepAlive = setInterval(() => {}, 60_000)
process.stderr.write('dsh-test: never-dispose ready\n')
ctx.effect(() => async () => {
clearInterval(keepAlive)
const armFile = process.env.DSH_TEST_SHUTDOWN_ARM_FILE

View File

@@ -22,7 +22,7 @@ if pid == 0:
os.chdir(cwd)
os.execvpe(node, [node, *json.loads(launch_args_json)], env)
markers = [b"dsh: observing at ", b"dsh-test: never-dispose started"]
markers = [b"dsh-test: never-dispose ready", b"dsh-test: never-dispose started"]
output = bytearray()
marker_index = 0
deadline = time.monotonic() + float(timeout_seconds)
@@ -73,7 +73,7 @@ async function runHeadlessPtySmoke(): Promise<string> {
name: 'dsh-profile-headless',
private: true,
dependencies: {},
dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@deepseek-ai/dsh-headless'] } },
dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-headless'] } },
}, undefined, 2))
await writeFile(join(profileDir, 'cordis.patch.yml'), [
'- insert:',
@@ -124,7 +124,8 @@ async function runHeadlessPtySmoke(): Promise<string> {
describe.skipIf(process.platform === 'win32')('headless process shutdown (real Loader tree in a PTY)', () => {
it('lets a second Ctrl+C force exit while the first signal is draining', async () => {
const output = await runHeadlessPtySmoke()
expect(output).toContain('dsh: observing at ')
expect(output).not.toContain('dsh: observing at ')
expect(output).toContain('dsh-test: never-dispose ready')
expect(output).toContain('dsh-test: never-dispose started')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
})