@deepseek-ai/dsh-headless 0.1.5-rc.2 → 0.1.6-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/bundle/headless/README.md
5
- README.md: 98a9cb2294d8b05a40287c990b3ff53f755747cf
6
- README.zh.md: 0f4721d856357a85964a13433c3bbc701f8fb299
5
+ README.md: d3eb8054e5a4e597b71855f9718ca57252d926da
6
+ README.zh.md: b3f4d445132f8a59f8924f2668ad2eb2041f6a3f
package/README.md CHANGED
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- `dsh-headless` runs one dsh task from the command line and prints the final answer, then exits — no GUI, no server, no browser. Type `dsh --profile headless "run the tests"` and the agent works through the task with the same model, tools, and safety defaults as every other surface. It is ideal for scripts, CI, and one-off jobs: the process opens no ports and leaves nothing running behind. The exit code tells you the outcome — 0 when the task completed, 1 when it aborted or errored. The main boundary: one task per invocation, with no interactive follow-up.
12
+ `dsh-headless` runs one dsh task from the command line and prints the final answer, then exits — no GUI, no server, no browser. Type `dsh --profile headless "run the tests"` and the agent handles it with the same model, tools, and safety defaults as every other surface. It suits scripts, CI, and one-off jobs: it opens no ports and leaves nothing running behind. It also offers a JSON event stream (`--json`) and `--session-id` to resume a conversation. Exit code 0 means the task completed; 1 means it aborted or errored. The boundary: one task per invocation, no interactive follow-up.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -25,7 +25,7 @@ English | [中文](README.zh.md)
25
25
  <a id="use-this-package"></a>
26
26
  ## Use this package
27
27
 
28
- Run one task, get the final answer, and exit. The task is the command line itself, so the whole invocation is the smallest working example.
28
+ Run one task, get the final answer, and exit. The task is the command-line argument, or stdin when you omit it; the whole invocation is the smallest working example.
29
29
 
30
30
  ### Running a one-shot task
31
31
 
@@ -33,21 +33,37 @@ Run one task, get the final answer, and exit. The task is the command line itsel
33
33
  dsh --profile headless "run the tests"
34
34
  ```
35
35
 
36
- The agent works through the task, streams each non-empty provider reasoning delta to stderr under a `dsh: reasoning:` heading, then prints the final answer on stdout and exits. Consecutive reasoning deltas stay in one section, and the runner closes that section before later output when the provider supplied no trailing newline. A successful run without reasoning keeps stderr empty; a failure exits 1 and prints `dsh: <code>: <message>` to stderr. A missing or blank task is rejected before anything runs. The task text is supplied through the single `task` setting:
36
+ The agent works through the task, streams each non-empty provider reasoning delta to stderr under a `dsh: reasoning:` heading, then prints the final answer on stdout and exits. Consecutive reasoning deltas stay in one section, and the runner closes that section before later output when the provider supplied no trailing newline. A successful run without reasoning keeps stderr empty; a failure exits 1 and prints `dsh: <code>: <message>` to stderr. The task comes from the positional argument, or from stdin when the argument is omitted or is a lone `-`; a blank positional argument or an empty pipe is rejected before anything runs. A positional task is used as-is and stdin is not read, so put the whole prompt in the pipe when you want piped input; a piped task is sent verbatim, its trailing newline included.
37
+
38
+ ```sh
39
+ { echo "Summarize these changes:"; git diff --stat; } | dsh --profile headless
40
+ ```
41
+
42
+ The task and run options are supplied through three settings:
37
43
 
38
44
  | Field | Default | Meaning |
39
45
  |---|---|---|
40
- | `task` | required | The task text for the single run |
46
+ | `task` | stdin | The task text; stdin supplies it when omitted or `-` |
47
+ | `sessionId` | `session-<uuid>` | Exact Session identity to adopt; an unknown id fails |
48
+ | `json` | `false` | Project the run as newline-delimited events on stdout |
41
49
 
42
50
  The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-headless) is the exhaustive source for every accepted field and its JSDoc.
43
51
 
52
+ ### Choosing the session identity
53
+
54
+ Every invocation defaults to a fresh `session-<uuid>` identity, which `--json` reports in its opening `session` event. Pass `--session-id <id>` to continue that conversation: the runner adopts the persisted Session with that id, and an id with no stored Session fails before the task runs rather than quietly opening an empty history. Adoption requires the composed `sessionPersistence` and `sessionQuery` services, so a profile that omits either fails loudly instead of returning an id whose history dies with the process. An Agent already live under the requested id in this process is refused: another owner may still drive it, so the runner cannot claim an exclusive run interval over it. The identity is opaque, so the exact string is used, whitespace included. The working directory is resolved through the mounted filesystem provider (`fs.resolve('.')` and `fs.processPath()`), or the process cwd when no filesystem service is mounted; new Sessions record that directory. Adoption compares the recorded cwd with the same provider-resolved directory and refuses a Session that is a subagent or forked session, that recorded no working directory, that runs under an agent preset this profile does not compose, or whose preset record is malformed — the check reads the preset the Session log currently records, so a Session that switched preset while blank is rejected too. A supervisor therefore cannot silently drive someone else's conversation under a different composition; any mismatch fails before the task runs.
55
+
56
+ ### Machine-readable output
57
+
58
+ `--json` replaces the final-text stdout line with a newline-delimited JSON event stream, while stderr keeps only the `dsh:` diagnostics. The stream opens with `session` (carrying the identity the run used) and closes with `final`, and carries `status`, `text`, `thinking`, `tool_call`, and `tool_result` events in between. `text` and `thinking` are projected from committed assistant messages, so a retried or discarded attempt never reaches the stream; they arrive when the step commits, not per token, and default-mode stderr reasoning remains the only live text channel. The terminal `final` event carries the same lossless answer as the default mode and is not capped; every other string and object key is capped at 8 KiB and flagged with `truncated`, and one event line, its newline included, is capped at 32 KiB — an over-long event keeps its scalar fields, drops structured ones, and at the extreme reduces to `type` and `truncated`, while a payload nested 64 levels or deeper is cut at that depth. An empty tool-argument string projects as `{}`, matching what the executor runs, while arguments that JSON cannot round-trip — an overflowing number such as `1e400` — keep their raw text rather than the `null` that `JSON.stringify` would report. A failure the runner raises outside a turn writes an `error` event and ends the stream without `final`, in addition to the `dsh:` stderr line; a profile whose own plugins fail to load exits before the runner mounts, so that case keeps only the loader's stderr diagnostics. A turn that fails in-turn still ends with a `final` event (often empty) and no `error` event, so a well-formed stream can still describe a failed run: treat exit code 1 and the `turn_end` reason as the failure signal.
59
+
44
60
  ### When to use it
45
61
 
46
- Use headless for scripted or automated dsh runs — CI steps, batch jobs, quick answers from a terminal. Avoid it when you need a multi-turn interactive session or a GUI; the browser surface ([dsh-web-app](../web-app/README.md)) serves that. The process stays alive only for the run, opens no listening port, and exits on its own, so it fits pipelines that wait on the process.
62
+ Use headless for scripted or automated dsh runs — CI steps, batch jobs, quick answers from a terminal. Avoid it when you need a multi-turn interactive session or a GUI; the browser surface ([dsh-web-app](../web-app/README.md)) serves that. The process stays alive only for the run, opens no listening port, and exits on its own, so it fits pipelines that wait on the process. When a supervisor needs progress rather than just the answer, `--json` gives it the event stream and `--session-id` lets a later invocation continue the same conversation.
47
63
 
48
64
  ### Help and task errors
49
65
 
50
- `dsh --profile headless --help` prints the command's help text and exits without running anything. A missing or whitespace-only task is a usage error: nothing runs and the process exits 1.
66
+ `dsh --profile headless --help` prints the command's help text and exits without running anything. A whitespace-only positional task is a usage error on its own — nothing runs and the process exits 1, even when stdin is not a terminal, so an accidental blank argument never consumes a pipe. A task that is absent entirely is a usage error only when stdin is a terminal; otherwise the runner reads the task from stdin and rejects an empty result the same way. A lone `-` is the only stdin marker; mixing it with other task words is a usage error rather than a task that starts with a dash. In `--json` mode every usage error — including commander's own grammar rejections such as an unknown option or a missing option value — also writes an `error` event to stdout before the process exits, so a line-oriented supervisor sees a well-formed stream even when the runner never mounts; the event `message` carries the text without commander's `error: ` prefix.
51
67
 
52
68
  -----
53
69
 
@@ -57,29 +73,31 @@ Use headless for scripted or automated dsh runs — CI steps, batch jobs, quick
57
73
  <details>
58
74
  <summary>Implementation internals — click to expand</summary>
59
75
 
60
- The runner is a direct driver over the core API carrier: it creates one fresh Agent through the registry and folds the owned durable event interval into one process-level outcome.
76
+ The runner is a direct driver over the core API carrier: it resolves the Agent identity — a fresh `session-<uuid>` by default, or the persisted Session `--session-id` names — and folds the owned durable event interval into one process-level outcome.
61
77
 
62
78
  ### Run flow
63
79
 
64
- The runner awaits the complete application (`ctx.get('loader')?.await()`) so the composed tools and adapters are not half-mounted, reads the shared [`agentDefaultModel`](../../core/agent-default-model/README.md) selection, creates one fresh persisted Agent with that provider and model, and submits the task as an ordinary user message. It streams that Agent's non-empty reasoning deltas to stderr, waits for quiescence, then flushes the Session and folds the owned interval (`firstSeq` onward) into the last non-empty `assistant/message` text and final `turn/end` reason. It writes the final text to stdout and requests exit.
80
+ The runner awaits the complete application (`ctx.get('loader')?.await()`) so the composed tools and adapters are not half-mounted, reads the shared [`agentDefaultModel`](../../core/agent-default-model/README.md) selection, resolves the task from config or stdin, then resolves the Agent identity: a fresh `session-<uuid>` by default, or the persisted Session `--session-id` names, which it adopts through [`sessionQuery`](../../session-query/session-query/README.md) and refuses when no log exists. It submits the task as an ordinary user message. Without `--json` it streams that Agent's non-empty reasoning deltas to stderr; with `--json` it projects the run instead. It waits for quiescence, then flushes the Session and folds the owned interval (`firstSeq` onward) into the last non-empty `assistant/message` text and final `turn/end` reason. It writes the final text to stdout (or the `final` event) and requests exit.
65
81
 
66
82
  ### Patch surface over base
67
83
 
68
- The patch rides over `dsh-base`: it inherits the projection cache, sets the coding persona prefix and separate cwd suffix on the base `system-prompt` row, keeps the same temporary process-wide PTC mode opt-in (`DSH_TOOLS_MODE`) as the Web surface, disables the shared HMR row, inserts PTC mode's worker as a core execution capability, and mounts the startup provider and the runner. The cache checkpoints each persisted one-shot session for later consumers; its durability barrier flushes each covered log prefix before publishing the cache row and may split otherwise coalesced JSONL runs. The startup provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), reads the positional argument, prints the app's `--help`, and provides `headlessStartup`; the runner injects that service and reads its task from lazy config.
84
+ The patch rides over `dsh-base`: it inherits the projection cache and shared PTC runtime, sets the coding persona prefix and separate cwd suffix on the base `system-prompt` row, keeps the same temporary process-wide PTC mode opt-in (`DSH_TOOLS_MODE`) as the Web surface, disables the shared HMR row, and mounts the startup provider and the runner. The cache checkpoints each persisted one-shot session for later consumers; its durability barrier flushes each covered log prefix before publishing the cache row and may split otherwise coalesced JSONL runs. The startup provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), reads the positional argument and the `--session-id`/`--json` options, prints the app's `--help`, and provides `headlessStartup`; the runner injects that service and reads its task and run options from lazy config.
69
85
 
70
86
  ### Exit mapping
71
87
 
72
- A completed final `turn/end` exits 0; any other outcome — aborted, error, or no turn in the owned interval — exits 1. An `error` reason also writes `dsh: <code>: <message>` to stderr. A direct driver failure (for example, Agent creation) writes `dsh: <message>` to stderr and exits 1.
88
+ A completed final `turn/end` exits 0; any other outcome — aborted, error, or no turn in the owned interval — exits 1. An `error` reason also writes `dsh: <code>: <message>` to stderr. A direct driver failure (for example, Agent creation or an unusable `--session-id`) writes `dsh: <message>` to stderr and exits 1, and in `--json` mode also emits an `error` event.
73
89
 
74
90
  ### Source map
75
91
 
76
92
  | File | Role |
77
93
  |---|---|
78
- | [`src/index.ts`](src/index.ts) | The `headless-runner` plugin: run flow, output contract, exit mapping |
79
- | [`src/startup.ts`](src/startup.ts) | The `headless-startup` provider: task positional and `--help` |
94
+ | [`src/index.ts`](src/index.ts) | The `headless-runner` plugin: run flow, session resolution, output contract, exit mapping |
95
+ | [`src/startup.ts`](src/startup.ts) | The `headless-startup` provider: task positional, `--session-id`, `--json`, and `--help` |
96
+ | [`src/json-stream.ts`](src/json-stream.ts) | The `--json` projection: event vocabulary, commit-point emission, string bounding |
80
97
  | [`cordis.patch.yml`](cordis.patch.yml) | The one-shot patch over `dsh-base` |
81
98
  | — | No runtime invariant companion is published; the runner's observable contract (provider reasoning on stderr, final text on stdout, exit code by turn-end reason) is process-level and owned by the launcher e2e; it registers nothing and holds no mutable relation to audit inside the tree. |
82
- | [`tests/headless.spec.ts`](tests/headless.spec.ts) | Run flow, aggregation, flush, and exit mapping |
99
+ | [`tests/headless.spec.ts`](tests/headless.spec.ts) | Run flow, aggregation, flush, session adoption, and exit mapping |
100
+ | [`tests/json-stream.spec.ts`](tests/json-stream.spec.ts) | Projection ordering, commit-point emission, bounding, and disposal |
83
101
  | [`tests/startup.spec.ts`](tests/startup.spec.ts) | Command-line parsing over a real Loader tree |
84
102
 
85
103
  ### Invariant ownership
@@ -121,9 +139,11 @@ These limits tell you when headless does not fit and what it needs from the `dsh
121
139
 
122
140
  - **One task per run** — after the task is answered the process exits; there is no interactive follow-up, so split multi-step work into separate runs.
123
141
  - **Runs through the `dsh` launcher** — starting the headless profile another way fails at startup, because only the launcher can request the process exit.
124
- - **No pre-token heartbeat** — stderr stays silent until the provider emits a non-empty reasoning delta; a delayed first token exposes no earlier progress signal.
125
- - **Reasoning enters stderr logs** — redirection and supervisors may retain substantially more and potentially sensitive model output; route stderr to a controlled sink when needed.
126
- - **Only reasoning and the final answer are printed** — a run without an assistant message prints an empty stdout line and exits 1; intermediate tool output is not printed.
142
+ - **No pre-token heartbeat** — in default mode stderr stays silent until the provider emits a non-empty reasoning delta; a delayed first token exposes no earlier progress signal.
143
+ - **Reasoning enters stderr logs** — in default mode, redirection and supervisors may retain substantially more and potentially sensitive model output; route stderr to a controlled sink when needed.
144
+ - **Default stdout carries only the final answer** — a run without an assistant message prints an empty stdout line and exits 1; intermediate tool output is not printed unless you opt into `--json`.
145
+ - **Adoption is cwd-, ownership-, and preset-scoped** — `--session-id` refuses a Session recorded in another working directory, one that recorded no working directory, one that is a subagent or forked session, or one that runs under an agent preset this profile does not compose or whose preset record is malformed, and requires the composed Session query and persistence services; an identity already live in the process is refused too, because the runner cannot own an exclusive run interval over it.
146
+ - **The event stream is a projection, not the log** — `--json` caps every string except the terminal `final` at 8 KiB and omits events the projection does not model, so it is not a lossless copy of the Session log.
127
147
 
128
148
  <a id="dev-note"></a>
129
149
  ### Dev Note
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@ kind: "package-bundle"
9
9
 
10
10
  ## 概述
11
11
 
12
- `dsh-headless` 从命令行运行一个 dsh 任务并打印最终答案,然后退出——没有 GUI、没有服务器、没有浏览器。输入 `dsh --profile headless "run the tests"`,agent(智能体)会以与其他表层相同的模型、工具与安全默认值完成该任务。它非常适合脚本、CI 与一次性任务:进程不打开任何端口,也不会留下任何后台运行的东西。退出码告诉你结果——任务完成时为 0,中止或出错时为 1。主要边界:每次调用只运行一个任务,没有交互式后续。
12
+ `dsh-headless` 从命令行运行一个 dsh 任务并打印最终答案,然后退出——没有 GUI、没有服务器、没有浏览器。输入 `dsh --profile headless "run the tests"`,agent(智能体)会以与所有其他表层相同的模型、工具与安全默认值完成该任务。它非常适合脚本、CI 与一次性任务:进程不打开任何端口,也不会留下任何后台运行的东西。监督进程还可以通过按行 JSON 事件流(`--json`)驱动它,并用该事件流报告的标识(`--session-id`)在同一段对话上继续唤醒。退出码告诉你结果——任务完成时为 0,中止或出错时为 1。主要边界:每次调用只运行一个任务,没有交互式后续。
13
13
 
14
14
  ## 目录
15
15
 
@@ -25,7 +25,7 @@ kind: "package-bundle"
25
25
  <a id="use-this-package"></a>
26
26
  ## 使用本包
27
27
 
28
- 运行一个任务,获得最终答案,然后退出。任务就是命令行本身,因此整条命令就是最小的可运行示例。
28
+ 运行一个任务,获得最终答案,然后退出。任务就是命令行参数,省略时则来自 stdin;整条命令就是最小的可运行示例。
29
29
 
30
30
  ### 运行一次性任务
31
31
 
@@ -33,21 +33,37 @@ kind: "package-bundle"
33
33
  dsh --profile headless "run the tests"
34
34
  ```
35
35
 
36
- agent(智能体)会完成该任务,把提供方的每个非空推理增量流式写入 stderr 的 `dsh: reasoning:` 段,然后把最终答案写入 stdout 并退出。连续推理增量保持在同一段中;提供方未给尾换行时,runner 会在后续输出前结束该段。没有推理内容的成功运行保持 stderr 为空;失败时退出码为 1,并以 `dsh: <code>: <message>` 向 stderr 写入错误。缺失或空白任务会在任何内容运行前被拒绝。任务文本通过唯一的 `task` 设置提供:
36
+ agent 会完成该任务,把提供方的每个非空推理(reasoning)增量流式写入 stderr 的 `dsh: reasoning:` 段,然后把最终答案写入 stdout 并退出。连续推理增量保持在同一段中;提供方未给尾换行时,runner 会在后续输出前结束该段。没有推理内容的成功运行保持 stderr 为空;失败时退出码为 1,并以 `dsh: <code>: <message>` 向 stderr 写入错误。任务来自位置参数,参数省略或为单独的 `-` 时则来自 stdin;空白位置参数或空管道会在任何执行开始之前被拒绝。位置参数会原样作为任务,stdin 不会被读取,因此想让管道内容进入提示词时,请把完整提示写进管道;管道任务会原样发送,包括结尾换行。
37
+
38
+ ```sh
39
+ { echo "Summarize these changes:"; git diff --stat; } | dsh --profile headless
40
+ ```
41
+
42
+ 任务与运行选项通过三个设置提供:
37
43
 
38
44
  | 字段 | 默认值 | 含义 |
39
45
  |---|---|---|
40
- | `task` | 必填 | 单次运行的任务文本 |
46
+ | `task` | stdin | 任务文本;省略或传 `-` 时由 stdin 提供 |
47
+ | `sessionId` | `session-<uuid>` | 要沿用的精确 Session 标识;未知 id 会失败 |
48
+ | `json` | `false` | 把本次运行投影为 stdout 上的按行 JSON 事件 |
49
+
50
+ 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-headless)是所有受支持字段及其 JSDoc 的完整真源。
51
+
52
+ ### 选择 Session 标识
53
+
54
+ 每次调用默认使用全新的 `session-<uuid>` 标识,`--json` 会在开头的 `session` 事件里报告它。传入 `--session-id <id>` 延续这段对话:runner 沿用该 id 对应的持久化 Session,而该 id 没有持久化 Session 时会在任务运行前失败,而不是悄悄开出一段空历史。沿用要求已组合 `sessionPersistence` 与 `sessionQuery` 服务,因此缺少任一服务的 profile 会显式失败,而不会返回一个历史随进程消失的 id。本进程中已存在持有该 id 的存活 Agent 时会被拒绝:它的原 owner 可能仍在驱动它,runner 无法取得独占的运行区间。标识是不透明的,因此会原样使用调用方给出的字符串,包括空白字符。工作目录通过已挂载的文件系统提供方解析(`fs.resolve('.')` 与 `fs.processPath()`);未挂载文件系统服务时使用进程 cwd,新 Session 会记录该目录。沿用会将已记录 cwd 与同一提供方解析出的目录比较,并拒绝子 agent 或 fork 会话、未记录工作目录的会话、运行在本 profile 不组合的 agent preset 下的会话,以及 preset 记录畸形的会话——该检查读取 Session 日志当前记录的 preset,因此在空白期切换过 preset 的会话同样会被拒绝。因此监督进程无法在另一套组合下悄悄驱动他人的会话;任一不匹配都会在任务运行前失败。
55
+
56
+ ### 机器可读输出
41
57
 
42
- 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-headless)是每个受支持字段及其 JSDoc 的穷尽式真源。
58
+ `--json` 用按行 JSON 事件流取代 stdout 的最终文本行,stderr 仅保留 `dsh:` 诊断信息。事件流以 `session`(携带本次运行使用的标识)开头、以 `final` 结尾,其间为 `status`、`text`、`thinking`、`tool_call` 与 `tool_result` 事件。`text` 与 `thinking` 只从已提交的 assistant 消息投影,因此被重试或丢弃的尝试不会进入事件流;它们在步骤提交时到达,而不是逐 token 到达,默认模式的 stderr 推理仍是唯一的实时文本通道。终止 `final` 事件携带与默认模式相同的无损答案,不做限长;其他每个字符串与对象键上限为 8 KiB,超出时标记 `truncated`,单条事件行(含换行)上限为 32 KiB——超长事件保留标量字段、丢弃结构化字段,极端情况下只剩 `type` 与 `truncated`,嵌套达到 64 层及以上的负载会在该深度被截断。空工具参数字符串会投影为 `{}`,与执行器实际运行的值一致;而 JSON 无法往返的参数——例如溢出为 `Infinity` 的数字 `1e400`——会保留原始文本,而不是 `JSON.stringify` 会报告的 `null`。runner 在轮次之外抛出的失败会写出 `error` 事件并在没有 `final` 的情况下结束事件流,同时向 stderr 写入 `dsh:` 行;若 profile 自身的插件加载失败,进程会在 runner 挂载前退出,该情形只保留 loader 的 stderr 诊断。轮次内失败的运行仍会以 `final` 事件(通常为空)结束且没有 `error` 事件,因此格式良好的事件流也可能描述一次失败的运行:请把退出码 1 与 `turn_end` 原因作为失败信号。
43
59
 
44
60
  ### 何时使用
45
61
 
46
- 在脚本化或自动化的 dsh 运行中使用 headless——CI 步骤、批处理任务、从终端快速获取答案。当需要多轮交互会话或 GUI 时请避免它;浏览器表层([dsh-web-app](../web-app/README.zh.md))负责这类场景。进程只为本次运行而存活,不打开监听端口,并且自行退出,因此适合等待进程结束的流水线。
62
+ 在脚本化或自动化的 dsh 运行中使用 headless——CI 步骤、批处理任务、从终端快速获取答案。当需要多轮交互会话或 GUI 时请避免它;浏览器表层([dsh-web-app](../web-app/README.zh.md))负责这类场景。进程只为本次运行而存活,不打开监听端口,并且自行退出,因此适合等待进程结束的流水线。当监督进程需要进度而不只是答案时,`--json` 提供事件流,`--session-id` 则让后续调用继续同一段对话。
47
63
 
48
64
  ### 帮助与任务错误
49
65
 
50
- `dsh --profile headless --help` 打印该命令的帮助文本并直接退出,不运行任何内容。缺失或只有空白的任务属于用法错误:什么都不运行,进程退出 1。
66
+ `dsh --profile headless --help` 打印该命令的帮助文本并直接退出,不运行任何内容。只有空白的位置参数本身就属于用法错误——什么都不运行,进程退出 1,即使 stdin 不是终端也一样,因此误传的空白参数绝不会消费管道内容。完全缺失任务时,仅在 stdin 是终端时属于用法错误;stdin 不是终端时,runner 改从 stdin 读取任务,并以同样方式拒绝空结果。单独的 `-` 是唯一的 stdin 标记;把它与其他任务词混用属于用法错误,而不是以连字符开头的任务。在 `--json` 模式下,所有用法错误——包括 commander 自身的语法拒绝,例如未知选项或选项缺少取值——都会在进程退出前向 stdout 写出 `error` 事件,因此按行解析的监督进程即使 runner 从未挂载也能看到格式良好的事件流;事件 `message` 中的文本不带 commander 的 `error: ` 前缀。
51
67
 
52
68
  -----
53
69
 
@@ -57,29 +73,31 @@ agent(智能体)会完成该任务,把提供方的每个非空推理增量
57
73
  <details>
58
74
  <summary>实现细节——点击展开</summary>
59
75
 
60
- runner 是核心 API 载体之上的直接驱动器:它通过注册表创建一个全新的 Agent(智能体),并把所属的持久化事件区间折叠成一个进程级结果。
76
+ runner 是核心 API 载体之上的直接驱动器:它确定 Agent 标识——默认是全新的 `session-<uuid>`,或 `--session-id` 指名的持久化 Session——并把所属的持久化事件区间折叠成一个进程级结果。
61
77
 
62
78
  ### 运行流程
63
79
 
64
- runner 等待整个应用结算(`ctx.get('loader')?.await()`),确保已组合的工具与适配器不会半挂载,读取共享的 [`agentDefaultModel`](../../core/agent-default-model/README.zh.md) 选择,用该 provider 与模型创建一个全新的持久化 Agent(智能体),并把任务作为普通用户消息提交。它把该 Agent 的非空推理增量流式写入 stderr、等待完全停稳,然后 flush Session,并把所属区间(从 `firstSeq` 起)折叠为最后一条非空 `assistant/message` 文本与最终 `turn/end` 原因。最后,它把最终文本写入 stdout 并请求退出。
80
+ runner 等待整个应用结算(`ctx.get('loader')?.await()`),确保已组合的工具与适配器不会半挂载,读取共享的 [`agentDefaultModel`](../../core/agent-default-model/README.zh.md) 选择,从配置或 stdin 解析任务,然后确定 Agent 标识:默认是全新的 `session-<uuid>`,或是 `--session-id` 指名的持久化 Session——通过 [`sessionQuery`](../../session-query/session-query/README.zh.md) 沿用,日志不存在时拒绝。它把任务作为普通用户消息提交。不带 `--json` 时,它把该 Agent 的非空推理增量流式写入 stderr;带 `--json` 时改为投影本次运行。它等待完全停稳,然后对会话执行 flush,并把所属区间(从 `firstSeq` 起)折叠为最后一条非空 `assistant/message` 文本与最终 `turn/end` 原因。最后,它把最终文本写入 stdout(或 `final` 事件)并请求退出。
65
81
 
66
- ### 叠加在 base 之上的 patch 表层
82
+ ### 基于 base 的 patch 内容
67
83
 
68
- patch 叠加在 `dsh-base` 之上:继承投影缓存,在基础 `system-prompt` 行上设置编码 persona 前缀与独立的 cwd 后缀,保留与 Web 表层相同的临时进程级 PTC mode 开关(`DSH_TOOLS_MODE`),禁用共享的 HMR 行,把 PTC mode 的 worker 作为核心执行能力插入,并挂载启动提供方与 runner。缓存为每个已持久化的一次性会话写入检查点,供后续消费方使用;其持久性屏障会在发布缓存行前 flush 所覆盖的日志前缀,因此可能拆分原本会合并的 JSONL 行。启动提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`([`dsh-cmdline`](../../boot/cmdline/README.zh.md)),读取位置参数、打印应用自己的 `--help`,并提供 `headlessStartup`;runner 注入该服务,再从惰性配置中读取任务。
84
+ patch 叠加在 `dsh-base` 之上:继承投影缓存与共享 PTC 运行时,在基础 `system-prompt` 行上设置编码 persona 前缀与独立的 cwd 后缀,保留与 Web 表层相同的临时进程级 PTC mode 开关(`DSH_TOOLS_MODE`),禁用共享的 HMR(热模块替换)行,并挂载启动提供方与 runner。缓存为每个已持久化的一次性会话写入检查点,供后续消费方使用;其持久性屏障会在发布缓存行前 flush 所覆盖的日志前缀,因此可能拆分原本会合并的 JSONL 连续段。启动提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`([`dsh-cmdline`](../../boot/cmdline/README.zh.md)),读取位置参数与 `--session-id`/`--json` 选项、打印应用自己的 `--help`,并提供 `headlessStartup`;runner 注入该服务,再从惰性配置中读取任务与运行选项。
69
85
 
70
86
  ### 退出映射
71
87
 
72
- 最终 `turn/end` 完成时退出码为 0;任何其他结果——aborted、error,或所属区间内没有轮次——退出码为 1。结束原因为 `error` 时还会向 stderr 写入 `dsh: <code>: <message>`。直接驱动器失败(例如 Agent 创建失败)向 stderr 写入 `dsh: <message>` 并退出 1。
88
+ 最终 `turn/end` 完成时退出码为 0;任何其他结果——aborted、error,或所属区间内没有轮次——退出码为 1。结束原因为 `error` 时还会向 stderr 写入 `dsh: <code>: <message>`。直接驱动器失败(例如 Agent 创建失败或不可用的 `--session-id`)向 stderr 写入 `dsh: <message>` 并退出 1,且在 `--json` 模式下额外发出一个 `error` 事件。
73
89
 
74
90
  ### 源码地图
75
91
 
76
92
  | 文件 | 职责 |
77
93
  |---|---|
78
- | [`src/index.ts`](src/index.ts) | `headless-runner` 插件:运行流程、输出约定、退出映射 |
79
- | [`src/startup.ts`](src/startup.ts) | `headless-startup` 提供方:任务位置参数与 `--help` |
94
+ | [`src/index.ts`](src/index.ts) | `headless-runner` 插件:运行流程、Session 解析、输出约定、退出映射 |
95
+ | [`src/startup.ts`](src/startup.ts) | `headless-startup` 提供方:任务位置参数、`--session-id`、`--json` 与 `--help` |
96
+ | [`src/json-stream.ts`](src/json-stream.ts) | `--json` 投影:事件词汇、提交点发射、字符串限长 |
80
97
  | [`cordis.patch.yml`](cordis.patch.yml) | 叠加在 `dsh-base` 之上的一次性 patch |
81
- | — | 不发布运行时不变式伴生入口;可观察的行为属于进程级组合,本包只持有静态 patch 列表。 |
82
- | [`tests/headless.spec.ts`](tests/headless.spec.ts) | 运行流程、汇总、flush 与退出映射 |
98
+ | — | 不发布运行时不变式伴生入口;runner 的可观察约定(stderr 中的提供方推理、stdout 中的最终文本、按轮次结束原因决定的退出码)属于进程级,并由启动器 e2e 负责;runner 不注册任何内容,树内也没有任何可变关系可审计。 |
99
+ | [`tests/headless.spec.ts`](tests/headless.spec.ts) | 运行流程、汇总、flush、Session 沿用与退出映射 |
100
+ | [`tests/json-stream.spec.ts`](tests/json-stream.spec.ts) | 投影顺序、提交点发射、限长与释放 |
83
101
  | [`tests/startup.spec.ts`](tests/startup.spec.ts) | 在真实 Loader 树上的命令行解析 |
84
102
 
85
103
  ### 不变式归属
@@ -93,11 +111,11 @@ patch 叠加在 `dsh-base` 之上:继承投影缓存,在基础 `system-promp
93
111
  <a id="further-exploration"></a>
94
112
  ## 进一步探索
95
113
 
96
- 当你想深入了解共享核心、兄弟 GUI 或命令行交接时,阅读以下页面。
114
+ 当你想深入了解共享核心、同级 GUI 或命令行交接时,阅读以下页面。
97
115
 
98
- - [组合包包映射](../README.zh.md)——基于同一核心构建的表层。
116
+ - [组合包索引](../README.zh.md)——基于同一核心构建的表层。
99
117
  - [dsh-base](../base/README.zh.md)——headless 运行其上的共享核心。
100
- - [dsh-web-app](../web-app/README.zh.md)——用于多轮工作的交互式浏览器兄弟表层。
118
+ - [dsh-web-app](../web-app/README.zh.md)——用于多轮工作的同级交互式浏览器入口。
101
119
  - [dsh-cmdline](../../boot/cmdline/README.zh.md)——启动器如何把命令行交给应用。
102
120
  - [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-headless)——每个受支持配置字段及其源声明。
103
121
 
@@ -110,20 +128,22 @@ patch 叠加在 `dsh-base` 之上:继承投影缓存,在基础 `system-promp
110
128
 
111
129
  #### KV Cache 影响
112
130
 
113
- runner 不向请求前缀添加任何内容;它只是把一条用户消息驱动经过组合出的配置树。
131
+ runner 不向请求前缀添加任何内容;它只是驱动组合出的配置树处理一条用户消息。
114
132
 
115
133
  ## 已知限制与延期工作
116
134
 
117
135
  <a id="known-limitations-and-deferred-work"></a>
118
136
 
119
137
 
120
- 这些限制告诉你 headless 何时不适用、它需要 `dsh` 启动器提供什么。它们是当前包约束,不是通用的 CLI 对比或任务积压。
138
+ 这些限制告诉你 headless 何时不适用、它需要 `dsh` 启动器提供什么。它们是当前包约束,不是通用的 CLI(命令行界面)对比或任务积压。
121
139
 
122
140
  - **每次运行一个任务**——任务得到回答后进程即退出;没有交互式后续,因此多步工作请拆成多次运行。
123
141
  - **通过 `dsh` 启动器运行**——以其他方式启动 headless profile 会在启动时失败,因为只有启动器能请求进程退出。
124
- - **首个 token 前没有心跳**——提供方发出第一个非空推理增量前,stderr 保持静默;延迟首个 token 的提供方不会更早给出进度信号。
125
- - **推理进入 stderr 日志**——重定向与监督进程可能保留更多且可能敏感的模型输出;需要时应把 stderr 路由到受控位置。
126
- - **只打印推理和最终答案**——没有 assistant 消息的运行向 stdout 打印空行并以 1 退出;中间工具输出不会打印。
142
+ - **首个 token 前没有心跳**——默认模式下,提供方发出第一个非空推理增量前,stderr 保持静默;延迟首个 token 的提供方不会更早给出进度信号。
143
+ - **推理进入 stderr 日志**——默认模式下,重定向与监督进程可能保留显著更多且可能敏感的模型输出;需要时应把 stderr 路由到受控位置。
144
+ - **默认 stdout 只承载最终答案**——没有 assistant 消息的运行向 stdout 打印空行并以 1 退出;中间工具输出不会打印,除非显式启用 `--json`。
145
+ - **沿用受 cwd、归属与 preset 限制**——`--session-id` 会拒绝记录在其他工作目录、未记录工作目录、属于子 agent 或 fork 会话,或运行在本 profile 不组合的 agent preset 下的 Session、preset 记录畸形的 Session,并要求已组合的 Session 查询与持久化服务;本进程中已存活的身份同样会被拒绝,因为 runner 无法对它取得独占的运行区间。
146
+ - **事件流是投影而非日志**——`--json` 除终止 `final` 外把每个字符串与对象键限制在 8 KiB,并省略投影未建模的事件,因此它不是 Session 日志的无损副本。
127
147
 
128
148
  <a id="dev-note"></a>
129
149
  ### 开发备注
package/cordis.patch.yml CHANGED
@@ -1,8 +1,10 @@
1
1
  # The dsh-headless bundle patch: one-shot task mode directly over dsh-base.
2
2
  # It mounts no Host, HTTP server, Web runtime, or browser plugin. An ordinary
3
3
  # provider plugin injects `cmdlineArgs`, parses the task positional
4
- # (`dsh --profile headless "<task>"`) and this app's --help, then the direct
5
- # driver creates an Agent through the core registry and prints its durable result.
4
+ # (`dsh --profile headless "<task>"`), the `--session-id` and `--json` options,
5
+ # and this app's --help, then the direct driver creates a fresh Agent, or adopts
6
+ # the exact Session the flag names, through the core registry and prints its
7
+ # durable result.
6
8
 
7
9
  - id: system-prompt
8
10
  config:
@@ -16,16 +18,17 @@
16
18
  mode: !!js process.env.DSH_TOOLS_MODE
17
19
 
18
20
  - insert:
19
- # PTC mode is a core execution capability, not a Web component.
20
- - id: code-runtime
21
- name: '@deepseek-ai/dsh-code-runtime-worker-thread'
22
-
23
21
  - id: headless-startup
24
22
  name: '@deepseek-ai/dsh-headless/startup'
25
23
 
26
- # Reads its task from the ordinary headlessStartup provider.
24
+ # Reads its task and run options from the ordinary headlessStartup provider.
27
25
  - id: headless-runner
28
26
  name: '@deepseek-ai/dsh-headless'
29
27
  inject: [headlessStartup]
30
28
  config:
31
29
  task: !!js ctx.headlessStartup.task
30
+ sessionId: !!js ctx.headlessStartup.sessionId
31
+ json: !!js ctx.headlessStartup.json
32
+
33
+ - id: hmr
34
+ disabled: true