Răsfoiți Sursa

Merge pull request #674 from deepseek-harness/nih-imp-sse

refactor(llm-deepseek): replace hand-rolled SSE parser with eventsource-parser
Tianyi Cui 2 luni în urmă
părinte
comite
5be0118784
36 a modificat fișierele cu 145 adăugiri și 233 ștergeri
  1. 2 2
      .agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.i18n.yaml
  2. 3 3
      .agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md
  3. 3 3
      .agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md
  4. 2 2
      .agents/notes/implemented/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.i18n.yaml
  5. 28 0
      .agents/notes/implemented/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md
  6. 28 0
      .agents/notes/implemented/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.zh.md
  7. 0 33
      .agents/notes/proposed/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md
  8. 0 33
      .agents/notes/proposed/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.zh.md
  9. 2 2
      .agents/notes/rejected/simplification/2026-07-26-dependency-swaps-rejected-by-nih-audit.i18n.yaml
  10. 1 1
      .agents/notes/rejected/simplification/2026-07-26-dependency-swaps-rejected-by-nih-audit.md
  11. 1 1
      .agents/notes/rejected/simplification/2026-07-26-dependency-swaps-rejected-by-nih-audit.zh.md
  12. 1 1
      AGENTS.md
  13. 1 1
      docs/config-catalog.md
  14. 3 3
      docs/cookbook/adding-an-llm-adapter.i18n.yaml
  15. 1 1
      docs/cookbook/adding-an-llm-adapter.md
  16. 1 1
      docs/cookbook/adding-an-llm-adapter.zh.md
  17. 3 3
      docs/core-data-structures/llm-streaming.i18n.yaml
  18. 3 3
      docs/core-data-structures/llm-streaming.md
  19. 3 3
      docs/core-data-structures/llm-streaming.zh.md
  20. 2 2
      packages/llm/README.i18n.yaml
  21. 1 1
      packages/llm/README.md
  22. 1 1
      packages/llm/README.zh.md
  23. 2 2
      packages/llm/llm-deepseek/README.i18n.yaml
  24. 1 1
      packages/llm/llm-deepseek/README.md
  25. 1 1
      packages/llm/llm-deepseek/README.zh.md
  26. 1 0
      packages/llm/llm-deepseek/package.json
  27. 1 1
      packages/llm/llm-deepseek/src/adapter.ts
  28. 14 46
      packages/llm/llm-deepseek/src/sse.ts
  29. 1 1
      packages/llm/llm-deepseek/tests/adapter.e2e.ts
  30. 23 73
      packages/llm/llm-deepseek/tests/sse.spec.ts
  31. 2 2
      packages/llm/llm/README.i18n.yaml
  32. 1 1
      packages/llm/llm/README.md
  33. 1 1
      packages/llm/llm/README.zh.md
  34. 2 2
      packages/llm/llm/src/index.ts
  35. 2 2
      packages/sdk/helper/src/features/builtin/provider.ts
  36. 3 0
      pnpm-lock.yaml

+ 2 - 2
.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write
 #   pnpm run verify-translation-pairing --write
-2026-06-13-twin-llm-adapters.md: 5c3308b281ce71407002e95dd6e794da2a421fa8
-2026-06-13-twin-llm-adapters.zh.md: 93b084973bccaeb802508e4e939a259c281f2608
+2026-06-13-twin-llm-adapters.md: b922891d4438553fd96a7f4f4226f378e66e8ad2
+2026-06-13-twin-llm-adapters.zh.md: d98b57a0a2e7c92453046022e8cb50aa52994c0f

+ 3 - 3
.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md

@@ -12,10 +12,10 @@ English | [中文](2026-06-13-twin-llm-adapters.zh.md)
 
 
 Ship **two** adapters against the one contract from the start, deliberately built on different internals:
 Ship **two** adapters against the one contract from the start, deliberately built on different internals:
 
 
-- `dsh-llm-deepseek` — hand-rolled `fetch` + SSE parsing against the DeepSeek API.
+- `dsh-llm-deepseek` — direct `fetch` + in-repo translation against the DeepSeek API; SSE framing is delegated to `eventsource-parser` ([the SSE-parser swap](../simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md)). The twin identity is owning the fetch/translate internals rather than delegating to a full provider SDK, not hand-rolling transport plumbing.
 - `dsh-llm-pi-ai` — the same endpoint through the `@earendil-works/pi-ai` library (its own event vocabulary).
 - `dsh-llm-pi-ai` — the same endpoint through the `@earendil-works/pi-ai` library (its own event vocabulary).
 
 
-The rule they enforce: **anything the StreamChunk vocabulary cannot express for BOTH implementations is a core-vocabulary bug**, caught immediately rather than at the next provider. The pair pinned down conventions now documented on `StreamChunk` in `dsh-llm/src/types.ts`: usage emitted before finish, nothing after finish, tool-call `arguments` as raw JSON strings end-to-end, and the two sanctioned error paths (throw from `stream()` *or* end with `finish {kind:'error'|'aborted'}`) that a consumer must handle on both sides — a divergence the library-backed adapter surfaced that a single hand-rolled adapter would have hidden.
+The rule they enforce: **anything the StreamChunk vocabulary cannot express for BOTH implementations is a core-vocabulary bug**, caught immediately rather than at the next provider. The pair pinned down conventions now documented on `StreamChunk` in `dsh-llm/src/types.ts`: usage emitted before finish, nothing after finish, tool-call `arguments` as raw JSON strings end-to-end, and the two sanctioned error paths (throw from `stream()` *or* end with `finish {kind:'error'|'aborted'}`) that a consumer must handle on both sides — a divergence the library-backed adapter surfaced that a single direct-fetch adapter would have hidden.
 
 
 ## Alternatives considered
 ## Alternatives considered
 
 
@@ -24,4 +24,4 @@ The rule they enforce: **anything the StreamChunk vocabulary cannot express for
 
 
 ## Consequences
 ## Consequences
 
 
-The twin doubles adapter and key-gated e2e maintenance—both cover V4 Flash and Pro across representative reasoning modes—in exchange for continuous seam-neutrality validation and a second implementation example. Both use `apiKey`, `baseURL`, and `models`; the hand-rolled adapter exposes `thinking`/`reasoningEffort`, while pi-ai exposes one `reasoning` level. A future conformance suite could justify retiring one adapter through a superseding Agent Note.
+The twin doubles adapter and key-gated e2e maintenance—both cover V4 Flash and Pro across representative reasoning modes—in exchange for continuous seam-neutrality validation and a second implementation example. Both use `apiKey`, `baseURL`, and `models`; the direct-fetch adapter exposes `thinking`/`reasoningEffort`, while pi-ai exposes one `reasoning` level. A future conformance suite could justify retiring one adapter through a superseding Agent Note.

+ 3 - 3
.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md

@@ -12,10 +12,10 @@ Status: implemented
 
 
 从一开始就针对同一份契约交付**两个**适配器,刻意基于不同的内部实现构建:
 从一开始就针对同一份契约交付**两个**适配器,刻意基于不同的内部实现构建:
 
 
-- `dsh-llm-deepseek`:手写 `fetch` + SSE(Server-Sent Events)解析,直接对接 DeepSeek API。
+- `dsh-llm-deepseek`:直接 `fetch` + 仓库内翻译逻辑对接 DeepSeek API;SSE(Server-Sent Events)分帧委托给 `eventsource-parser`([SSE 解析器替换](../simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md))。孪生身份在于自行持有 fetch/translate 内部实现而非委托给完整的提供方 SDK,不在于手写传输层管道。
 - `dsh-llm-pi-ai`:通过 `@earendil-works/pi-ai` 库访问同一端点(该库有自己的事件词汇)。
 - `dsh-llm-pi-ai`:通过 `@earendil-works/pi-ai` 库访问同一端点(该库有自己的事件词汇)。
 
 
-二者共同执行的规则是:**凡 StreamChunk 词汇无法为两个实现同时表达的内容,都是核心词汇的缺陷**——立即暴露,而非等到下一个提供方接入时才发现。这对孪生体确定了现已记录在 `dsh-llm/src/types.ts` 中 `StreamChunk` 上的约定:usage 在 finish 之前发出、finish 之后不再有任何事件、工具调用的 `arguments` 全程以原始 JSON 字符串传递,以及消费方必须在两侧都处理的两条合法错误路径(`stream()` 抛异常,*或者*以 `finish {kind:'error'|'aborted'}` 结束)。后一项分歧正是由基于库的适配器暴露出来的,单一手写适配器会将其隐藏。
+二者共同执行的规则是:**凡 StreamChunk 词汇无法为两个实现同时表达的内容,都是核心词汇的缺陷**——立即暴露,而非等到下一个提供方接入时才发现。这对孪生体确定了现已记录在 `dsh-llm/src/types.ts` 中 `StreamChunk` 上的约定:usage 在 finish 之前发出、finish 之后不再有任何事件、工具调用的 `arguments` 全程以原始 JSON 字符串传递,以及消费方必须在两侧都处理的两条合法错误路径(`stream()` 抛异常,*或者*以 `finish {kind:'error'|'aborted'}` 结束)。后一项分歧正是由基于库的适配器暴露出来的,单一直接 fetch 适配器会将其隐藏。
 
 
 ## 曾考虑的替代方案
 ## 曾考虑的替代方案
 
 
@@ -24,4 +24,4 @@ Status: implemented
 
 
 ## 后果
 ## 后果
 
 
-孪生体使适配器和需要密钥的 e2e 维护量翻倍——两者都覆盖 V4 Flash 和 Pro 在各代表性推理(reasoning)模式下的行为——换来的是持续的 seam 中立性验证和第二份实现示例。两个适配器均使用 `apiKey`、`baseURL` 和 `models`;手写适配器暴露 `thinking`/`reasoningEffort`,pi-ai 适配器暴露一个 `reasoning` 级别。未来如果有一致性测试套件,可以通过后续 Agent Note 论证退役其中一个适配器。
+孪生体使适配器和需要密钥的 e2e 维护量翻倍——两者都覆盖 V4 Flash 和 Pro 在各代表性推理(reasoning)模式下的行为——换来的是持续的 seam 中立性验证和第二份实现示例。两个适配器均使用 `apiKey`、`baseURL` 和 `models`;直接 fetch 适配器暴露 `thinking`/`reasoningEffort`,pi-ai 适配器暴露一个 `reasoning` 级别。未来如果有一致性测试套件,可以通过后续 Agent Note 论证退役其中一个适配器。

+ 2 - 2
.agents/notes/proposed/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.i18n.yaml → .agents/notes/implemented/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write
 #   pnpm run verify-translation-pairing --write
-2026-07-26-eventsource-parser-for-deepseek-sse.md: 8a93b7f6c7aa0d428f25e87c44e1d29e884ecc81
-2026-07-26-eventsource-parser-for-deepseek-sse.zh.md: b16109d9458f487c7e463cf02e6b2d22fbbde015
+2026-07-26-eventsource-parser-for-deepseek-sse.md: e7835bc738b3dec5aefd6011848525f6604e852e
+2026-07-26-eventsource-parser-for-deepseek-sse.zh.md: 933b993d479026d8f2bd2dc3173abd9e60823806

+ 28 - 0
.agents/notes/implemented/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md

@@ -0,0 +1,28 @@
+# Agent Note: Replace the hand-rolled SSE parser in llm-deepseek with eventsource-parser
+
+Status: implemented
+
+English | [中文](2026-07-26-eventsource-parser-for-deepseek-sse.zh.md)
+
+## Problem
+
+`packages/llm/llm-deepseek/src/sse.ts` hand-implemented Server-Sent Events parsing: a streaming `TextDecoder`, event-block splitting on `\r?\n\r?\n`, `data:` payload extraction and joining, comment/field skipping, the `[DONE]` sentinel, a `STREAM_CLOSED` error on EOF without it, and a flush of a final unterminated event block. The file was ~67 lines with ~108 lines of dedicated tests (`tests/sse.spec.ts`) re-proving SSE spec behavior — UTF-8 split across chunks, CRLF handling, multi-`data:` joining, no-space-after-colon — that a maintained parser already guarantees. Its only consumer is `adapter.ts` (`yield* translate(parseSse(response.body))`).
+
+This is exactly the surface `eventsource-parser` owns: the de-facto standard SSE parser (it underlies the Vercel AI SDK and the MCP SDK), zero-dependency, actively maintained, and already present in this repo's lockfile transitively via `@modelcontextprotocol/sdk` — so adopting it directly adds no new supply-chain surface in practice.
+
+## Decision
+
+`sse.ts` delegates SSE framing to `EventSourceParserStream` from `eventsource-parser/stream`: `parseSse` pipes the response body through `new TextDecoderStream()` then `new EventSourceParserStream()` and keeps only the DeepSeek protocol shim — yield each event's `data`, terminate on `[DONE]`, and throw `LlmError('STREAM_CLOSED')` when the stream ends without the sentinel. All required builtins (`TextDecoderStream`, `pipeThrough`, async-iterable `ReadableStream`) exist at the Node ^22.19 engine floor. The spec-conformance tests are gone; `tests/sse.spec.ts` pins only the `[DONE]`/`STREAM_CLOSED`/EOF contract. `eventsource-parser` is `llm-deepseek`'s second runtime dependency after schemastery. The [twin-adapters note](../architecture/2026-06-13-twin-llm-adapters.md) and the `dsh-llm` JSDoc that branded this adapter "hand-rolled fetch + SSE parsing" now describe it as direct fetch with library-framed SSE.
+
+The library also strips a leading BOM (the hand-rolled parser would fail to match `data:` after one) and offers `maxBufferSize` hardening the hand-rolled parser lacked.
+
+## Alternatives considered
+
+- **Keep the hand-rolled parser.** Defensible under the [twin-adapters decision](../architecture/2026-06-13-twin-llm-adapters.md): the adapter is deliberately the hand-rolled design-verification twin of the pi-ai adapter. But that note's load-bearing distinction is owning the fetch/translate internals versus delegating to a full provider SDK; a ~700-byte SSE micro-parser is transport plumbing, not the design under verification. The twin-adapters note now states that reading explicitly.
+- **`createParser({onEvent})` callback API instead of the stream.** Works fed by a manual `TextDecoder` loop, but the `pipeThrough` composition deletes more of the hand-rolled code.
+
+## Consequences
+
+- The remaining shim only encodes the DeepSeek `[DONE]`/`STREAM_CLOSED` protocol; SSE framing edge cases are eventsource-parser's contract and are no longer re-proven here.
+- One deliberate robustness deviation is dropped: the hand-rolled parser flushed a final event block that lacked its terminating blank line, so a trailing `data: [DONE]` without `\n\n` still yielded DONE. eventsource-parser is spec-strict and only dispatches on the blank line, so that shape is now `STREAM_CLOSED`. Real providers and `dsh-llm-mock-server` always terminate events properly — the flush was a robustness nicety, not an observed provider shape — and `tests/sse.spec.ts` pins the new truncation verdict for that tail.
+- The documented "hand-rolled" identity of the twin adapter narrows to the fetch/translate internals; the twin-adapters note was updated in the same change rather than leaving the claim stale.

+ 28 - 0
.agents/notes/implemented/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.zh.md

@@ -0,0 +1,28 @@
+# Agent Note: 用 eventsource-parser 替换 llm-deepseek 中手写的 SSE 解析器
+
+Status: implemented
+
+[English](2026-07-26-eventsource-parser-for-deepseek-sse.md) | 中文
+
+## 问题
+
+`packages/llm/llm-deepseek/src/sse.ts` 曾手写实现 SSE(Server-Sent Events)解析:一个流式 `TextDecoder`、按 `\r?\n\r?\n` 切分事件块、提取并拼接 `data:` 载荷、跳过注释与其他字段、`[DONE]` 哨兵、在未见哨兵即 EOF 时抛出 `STREAM_CLOSED` 错误,以及对最后一个未终结事件块的 flush。该文件约 67 行,另有约 108 行专属测试(`tests/sse.spec.ts`)重复验证 SSE 规范行为——UTF-8 字符被切分到多个分片、CRLF 处理、多条 `data:` 拼接、冒号后无空格——而这些行为,持续维护的解析器早已有保证。它唯一的消费方是 `adapter.ts`(`yield* translate(parseSse(response.body))`)。
+
+这恰好是 `eventsource-parser` 负责的接口面:事实标准的 SSE 解析器(Vercel AI SDK 和 MCP SDK 都构建在它之上),零依赖,持续维护,并且已通过 `@modelcontextprotocol/sdk` 作为传递依赖出现在本仓库的 lockfile 中——因此直接采用它实际上不增加新的供应链接触面。
+
+## 决策
+
+`sse.ts` 将 SSE 分帧委托给 `eventsource-parser/stream` 的 `EventSourceParserStream`:`parseSse` 把响应 body 依次管道接入 `new TextDecoderStream()` 和 `new EventSourceParserStream()`,只保留 DeepSeek 协议垫层——逐个产出事件的 `data`,遇到 `[DONE]` 终止,流在未见哨兵时结束则抛出 `LlmError('STREAM_CLOSED')`。所需的全部内置能力(`TextDecoderStream`、`pipeThrough`、可异步迭代的 `ReadableStream`)在 Node ^22.19 引擎下限即已存在。规范符合性测试已删除;`tests/sse.spec.ts` 只固定 `[DONE]`/`STREAM_CLOSED`/EOF 契约。`eventsource-parser` 是 `llm-deepseek` 继 schemastery 之后的第二个运行时依赖。曾把该适配器标为「手写 fetch + SSE 解析」的[孪生适配器 Agent Note(agent 决策记录)](../architecture/2026-06-13-twin-llm-adapters.md)与 `dsh-llm` JSDoc,现在将其描述为直接 fetch 加库分帧的 SSE。
+
+该库还会剥离开头的 BOM(手写解析器在 BOM 之后会无法匹配 `data:`),并提供手写解析器缺少的 `maxBufferSize` 加固能力。
+
+## 曾考虑的替代方案
+
+- **保留手写解析器。** 依据[孪生适配器决策](../architecture/2026-06-13-twin-llm-adapters.md),这一选择有辩护余地:该适配器有意作为 pi-ai 适配器的手写设计验证孪生体。但那份 Agent Note 起支撑作用的区分在于「自行持有 fetch/translate 内部实现」与「委托给完整的提供方 SDK」;一个约 700 字节的 SSE 微型解析器属于传输层管道,不是被验证的设计本身。孪生适配器 Agent Note 现已明确写出这一解读。
+- **改用 `createParser({onEvent})` 回调 API 而非流。** 配合手动的 `TextDecoder` 循环可以工作,但 `pipeThrough` 组合方式能删除更多手写代码。
+
+## 后果
+
+- 剩下的垫层只编码 DeepSeek 的 `[DONE]`/`STREAM_CLOSED` 协议;SSE 分帧边界情形属于 eventsource-parser 的契约,不再在这里重复验证。
+- 放弃了一处有意为之的健壮性偏离:手写解析器会 flush 缺少终结空行的最后一个事件块,因此末尾的 `data: [DONE]` 即使没有 `\n\n` 也仍产出 DONE。eventsource-parser 严格遵循规范,只在空行处分发事件,所以这种形态现在是 `STREAM_CLOSED`。真实提供方和 `dsh-llm-mock-server` 总是正确终结事件——该 flush 只是健壮性上的锦上添花,并非实际观测到的提供方形态——`tests/sse.spec.ts` 固定了对该尾部的新截断判定。
+- 孪生适配器有文档记录的「手写」身份收窄到 fetch/translate 内部实现;孪生适配器 Agent Note 在同一次变更中更新,而不是让声明陈旧下去。

+ 0 - 33
.agents/notes/proposed/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md

@@ -1,33 +0,0 @@
-# Agent Note: Replace the hand-rolled SSE parser in llm-deepseek with eventsource-parser
-
-Status: proposed
-
-English | [中文](2026-07-26-eventsource-parser-for-deepseek-sse.zh.md)
-
-## Problem
-
-`packages/llm/llm-deepseek/src/sse.ts` hand-implements Server-Sent Events parsing: a streaming `TextDecoder`, event-block splitting on `\r?\n\r?\n`, `data:` payload extraction and joining, comment/field skipping, the `[DONE]` sentinel, a `STREAM_CLOSED` error on EOF without it, and a flush of a final unterminated event block. The file is ~67 lines with ~108 lines of dedicated tests (`tests/sse.spec.ts`) re-proving SSE spec behavior — UTF-8 split across chunks, CRLF handling, multi-`data:` joining, no-space-after-colon — that a maintained parser already guarantees. Its only consumer is `adapter.ts` (`yield* translate(parseSse(response.body))`).
-
-This is exactly the surface `eventsource-parser` owns: the de-facto standard SSE parser (it underlies the Vercel AI SDK and the MCP SDK), zero-dependency, actively maintained, and already present in this repo's lockfile transitively via `@modelcontextprotocol/sdk` — so adopting it directly adds no new supply-chain surface in practice.
-
-## Proposal
-
-Replace `sse.ts` with `EventSourceParserStream` from `eventsource-parser/stream`: `response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream())`, keeping only the DeepSeek protocol shim (~10–25 lines): yield each event's `data`, terminate on `[DONE]`, and throw `LlmError('STREAM_CLOSED')` when the stream ends without the sentinel. All required builtins (`TextDecoderStream`, `pipeThrough`, async-iterable `ReadableStream`) exist at the Node ^22.19 engine floor. Delete the spec-conformance tests; keep the `[DONE]`/`STREAM_CLOSED`/EOF contract tests. Add `eventsource-parser` to `llm-deepseek`'s dependencies (its second runtime dep after schemastery). Update the [twin-adapters note](../../implemented/architecture/2026-06-13-twin-llm-adapters.md) and the `dsh-llm` JSDoc that brand this adapter "hand-rolled fetch + SSE parsing" in the same PR.
-
-The library also strips a leading BOM (the hand-rolled parser would fail to match `data:` after one) and offers `maxBufferSize` hardening the current parser lacks.
-
-## Alternatives considered
-
-- **Keep the hand-rolled parser.** Defensible under the [twin-adapters decision](../../implemented/architecture/2026-06-13-twin-llm-adapters.md): the adapter is deliberately the hand-rolled design-verification twin of the pi-ai adapter. But the note's load-bearing distinction is owning the fetch/translate internals versus delegating to a full provider SDK; a ~700-byte SSE micro-parser is transport plumbing, not the design under verification. Whether that reading stands is the twin-note owner's call — this proposal explicitly needs their sign-off.
-- **`createParser({onEvent})` callback API instead of the stream.** Works fed by a manual `TextDecoder` loop, but the `pipeThrough` composition deletes more of the hand-rolled code.
-
-## Acceptance criteria
-
-- `sse.ts`'s parsing internals are gone; the remaining shim only encodes the DeepSeek `[DONE]`/`STREAM_CLOSED` protocol.
-- `llm-deepseek` unit tests and the real-API e2e suite pass; keyless snapshots are unchanged (parsing is transport-internal and payload extraction is equivalent).
-- The twin-adapters note and `dsh-llm` JSDoc no longer claim hand-rolled SSE parsing.
-
-## Risks
-
-- One deliberate robustness deviation is lost: the hand-rolled parser flushes a final event block that lacks its terminating blank line, and `tests/sse.spec.ts` pins that a trailing `data: [DONE]` without `\n\n` still yields DONE. eventsource-parser is spec-strict and only dispatches on the blank line, so that shape becomes `STREAM_CLOSED`. Real providers and `dsh-llm-mock-server` always terminate events properly, so the pinned behavior is a robustness nicety, not an observed provider shape — drop the test, or keep a tiny buffer-tail check if the deviation is judged load-bearing.
-- Dilutes the documented "hand-rolled" identity of the twin adapter; mitigated by updating the note in the same change rather than leaving the claim stale.

+ 0 - 33
.agents/notes/proposed/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.zh.md

@@ -1,33 +0,0 @@
-# Agent Note: 用 eventsource-parser 替换 llm-deepseek 中手写的 SSE 解析器
-
-Status: proposed
-
-[English](2026-07-26-eventsource-parser-for-deepseek-sse.md) | 中文
-
-## 问题
-
-`packages/llm/llm-deepseek/src/sse.ts` 手写实现了 SSE(Server-Sent Events)解析:一个流式 `TextDecoder`、按 `\r?\n\r?\n` 切分事件块、提取并拼接 `data:` 载荷、跳过注释与其他字段、`[DONE]` 哨兵、在未见哨兵即 EOF 时抛出 `STREAM_CLOSED` 错误,以及对最后一个未终结事件块的 flush。该文件约 67 行,另有约 108 行专属测试(`tests/sse.spec.ts`)重复验证 SSE 规范行为——UTF-8 字符被切分到多个分片、CRLF 处理、多条 `data:` 拼接、冒号后无空格——而这些行为,持续维护的解析器早已有保证。它唯一的消费方是 `adapter.ts`(`yield* translate(parseSse(response.body))`)。
-
-这恰好是 `eventsource-parser` 负责的接口面:事实标准的 SSE 解析器(Vercel AI SDK 和 MCP SDK 都构建在它之上),零依赖,持续维护,并且已通过 `@modelcontextprotocol/sdk` 作为传递依赖出现在本仓库的 lockfile 中——因此直接采用它实际上不增加新的供应链接触面。
-
-## 提案
-
-用 `eventsource-parser/stream` 的 `EventSourceParserStream` 替换 `sse.ts`:`response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream())`,只保留 DeepSeek 协议垫层(约 10–25 行):逐个产出事件的 `data`,遇到 `[DONE]` 终止,流在未见哨兵时结束则抛出 `LlmError('STREAM_CLOSED')`。所需的全部内置能力(`TextDecoderStream`、`pipeThrough`、可异步迭代的 `ReadableStream`)在 Node ^22.19 引擎下限即已存在。删除规范符合性测试;保留 `[DONE]`/`STREAM_CLOSED`/EOF 契约测试。将 `eventsource-parser` 加入 `llm-deepseek` 的依赖(这是它继 schemastery 之后的第二个运行时依赖)。在同一个 PR(Pull Request)中更新[孪生适配器 Agent Note(agent 决策记录)](../../implemented/architecture/2026-06-13-twin-llm-adapters.md)以及 `dsh-llm` 中把该适配器标为「手写 fetch + SSE 解析」的 JSDoc。
-
-该库还会剥离开头的 BOM(手写解析器在 BOM 之后会无法匹配 `data:`),并提供当前解析器缺少的 `maxBufferSize` 加固能力。
-
-## 曾考虑的替代方案
-
-- **保留手写解析器。** 依据[孪生适配器决策](../../implemented/architecture/2026-06-13-twin-llm-adapters.md),这一选择有辩护余地:该适配器有意作为 pi-ai 适配器的手写设计验证孪生体。但那份 Agent Note 起支撑作用的区分在于「自行持有 fetch/translate 内部实现」与「委托给完整的提供方 SDK」;一个约 700 字节的 SSE 微型解析器属于传输层管道,不是被验证的设计本身。这一解读是否成立由孪生 Agent Note 的所有者裁定——本提案明确需要其签署确认。
-- **改用 `createParser({onEvent})` 回调 API 而非流。** 配合手动的 `TextDecoder` 循环可以工作,但 `pipeThrough` 组合方式能删除更多手写代码。
-
-## 验收标准
-
-- `sse.ts` 的解析内部实现消失;剩下的垫层只编码 DeepSeek 的 `[DONE]`/`STREAM_CLOSED` 协议。
-- `llm-deepseek` 单元测试与真实 API 的 e2e 套件通过;无密钥快照不变(解析属于传输层内部,载荷提取等价)。
-- 孪生适配器 Agent Note 与 `dsh-llm` 的 JSDoc 不再声称手写 SSE 解析。
-
-## 风险
-
-- 会失去一处有意为之的健壮性偏离:手写解析器会 flush 缺少终结空行的最后一个事件块,`tests/sse.spec.ts` 固定了「末尾的 `data: [DONE]` 即使没有 `\n\n` 也仍产出 DONE」这一行为。eventsource-parser 严格遵循规范,只在空行处分发事件,因此这种形态会变成 `STREAM_CLOSED`。真实提供方和 `dsh-llm-mock-server` 总是正确终结事件,所以被固定的行为只是健壮性上的锦上添花,并非实际观测到的提供方形态:可以删除该测试;若判定该偏离确有支撑作用,也可以保留一个小型的缓冲区尾部检查。
-- 稀释了孪生适配器有文档记录的「手写」身份;缓解方式是在同一次变更中更新那份 Agent Note,而不是让声明陈旧下去。

+ 2 - 2
.agents/notes/rejected/simplification/2026-07-26-dependency-swaps-rejected-by-nih-audit.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write
 #   pnpm run verify-translation-pairing --write
-2026-07-26-dependency-swaps-rejected-by-nih-audit.md: a1d15b89f85b41e1044d9597dee6a1a0190240e6
-2026-07-26-dependency-swaps-rejected-by-nih-audit.zh.md: 4893784effdee7605f9194a80010b5a5033edbc1
+2026-07-26-dependency-swaps-rejected-by-nih-audit.md: f55bc2a9b7fb2a9599760734fca2a295665b95a2
+2026-07-26-dependency-swaps-rejected-by-nih-audit.zh.md: f740e0717cffd1b2ff4f3f5db8c9775afdfe79f0

+ 1 - 1
.agents/notes/rejected/simplification/2026-07-26-dependency-swaps-rejected-by-nih-audit.md

@@ -18,7 +18,7 @@ Adopt the following dependency swaps. Rejected — per-item evidence below; a fu
 - **`vscode-languageserver-types` for lsp-local's wire-type subset**: ~80 type lines and ~45 guard lines, but upstream guards differ in both directions (accept `uri: undefined` the repo must reject; require `targetRange` the repo tolerates absent), and the initialize-result shapes live in `vscode-languageserver-protocol`, dragging `vscode-jsonrpc` in as a runtime dep — ~1 MB for 80 spec-exact lines.
 - **`vscode-languageserver-types` for lsp-local's wire-type subset**: ~80 type lines and ~45 guard lines, but upstream guards differ in both directions (accept `uri: undefined` the repo must reject; require `targetRange` the repo tolerates absent), and the initialize-result shapes live in `vscode-languageserver-protocol`, dragging `vscode-jsonrpc` in as a runtime dep — ~1 MB for 80 spec-exact lines.
 - **`json-rpc-2.0` for `dsh-jsonrpc`**: deletable correlation/dispatch is real (~100–130 lines) but the NDJSON wire must stay bit-identical for the hand-rolled Python SDK client, the package is single-maintainer, and the [GUI RPC note](../../implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md) already treats this package as a frozen narrow surface. `vscode-jsonrpc` is a worse fit still (Content-Length framing, cancellation vocabulary the protocol lacks).
 - **`json-rpc-2.0` for `dsh-jsonrpc`**: deletable correlation/dispatch is real (~100–130 lines) but the NDJSON wire must stay bit-identical for the hand-rolled Python SDK client, the package is single-maintainer, and the [GUI RPC note](../../implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md) already treats this package as a frozen narrow surface. `vscode-jsonrpc` is a worse fit still (Content-Length framing, cancellation vocabulary the protocol lacks).
 - **`jsonrpcclient` for the Python SDK client**: v4 builds/parses messages only — ~20 lines — while the 500 lines that matter (subprocess lifecycle, threaded reader, id correlation, bidirectional server-role responses) stay; the library is in low-maintenance mode.
 - **`jsonrpcclient` for the Python SDK client**: v4 builds/parses messages only — ~20 lines — while the 500 lines that matter (subprocess lifecycle, threaded reader, id correlation, bidirectional server-role responses) stay; the library is in low-maintenance mode.
-- **`eventsource-parser` for apiproxy's `readSse`**: only ~15 lines of framing are deletable, both wire ends are in-repo so spec conformance is moot, and it would add a dep to a browser-safe package. (Contrast with the [llm-deepseek proposal](../../proposed/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md), where a real provider sits across the wire.)
+- **`eventsource-parser` for apiproxy's `readSse`**: only ~15 lines of framing are deletable, both wire ends are in-repo so spec conformance is moot, and it would add a dep to a browser-safe package. (Contrast with the [llm-deepseek proposal](../../implemented/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md), where a real provider sits across the wire.)
 
 
 **Retry, timers, async:**
 **Retry, timers, async:**
 
 

+ 1 - 1
.agents/notes/rejected/simplification/2026-07-26-dependency-swaps-rejected-by-nih-audit.zh.md

@@ -18,7 +18,7 @@ Status: rejected — 下列每一项替换在证据上都未达到净简化门
 - **以 `vscode-languageserver-types` 承担 lsp-local 的协议类型子集**:约 80 行类型加约 45 行守卫,但上游守卫在两个方向上都与本仓库不一致(接受本仓库必须拒绝的 `uri: undefined`;强制要求本仓库容忍缺失的 `targetRange`),而且 initialize 结果的形状住在 `vscode-languageserver-protocol` 里,会把 `vscode-jsonrpc` 拖成运行时依赖——为 80 行严格贴合规范的代码付出约 1 MB。
 - **以 `vscode-languageserver-types` 承担 lsp-local 的协议类型子集**:约 80 行类型加约 45 行守卫,但上游守卫在两个方向上都与本仓库不一致(接受本仓库必须拒绝的 `uri: undefined`;强制要求本仓库容忍缺失的 `targetRange`),而且 initialize 结果的形状住在 `vscode-languageserver-protocol` 里,会把 `vscode-jsonrpc` 拖成运行时依赖——为 80 行严格贴合规范的代码付出约 1 MB。
 - **以 `json-rpc-2.0` 替换 `dsh-jsonrpc`**:可删除的关联/分发代码确实存在(约 100–130 行),但 NDJSON 协议格式(wire format)必须与手写的 Python SDK 客户端逐位一致,该包只有单一维护者,且 [GUI RPC 决策](../../implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)已把这个包当作冻结的窄接口面对待。`vscode-jsonrpc` 更不合适(Content-Length 分帧、该协议并不具备的取消词汇)。
 - **以 `json-rpc-2.0` 替换 `dsh-jsonrpc`**:可删除的关联/分发代码确实存在(约 100–130 行),但 NDJSON 协议格式(wire format)必须与手写的 Python SDK 客户端逐位一致,该包只有单一维护者,且 [GUI RPC 决策](../../implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)已把这个包当作冻结的窄接口面对待。`vscode-jsonrpc` 更不合适(Content-Length 分帧、该协议并不具备的取消词汇)。
 - **以 `jsonrpcclient` 承担 Python SDK 客户端**:v4 只做消息的构造/解析——约 20 行——而真正要紧的 500 行(子进程生命周期、线程化读取器、id 关联、双向的服务端角色应答)全都保留;该库处于低维护模式。
 - **以 `jsonrpcclient` 承担 Python SDK 客户端**:v4 只做消息的构造/解析——约 20 行——而真正要紧的 500 行(子进程生命周期、线程化读取器、id 关联、双向的服务端角色应答)全都保留;该库处于低维护模式。
-- **以 `eventsource-parser` 替换 apiproxy 的 `readSse`**:可删除的分帧只有约 15 行,线路两端都在仓库内,规范符合性无关紧要,而且这会给一个浏览器安全的包添加依赖。(对比 [llm-deepseek 提案](../../proposed/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md):那里线路对面是真实的提供方。)
+- **以 `eventsource-parser` 替换 apiproxy 的 `readSse`**:可删除的分帧只有约 15 行,线路两端都在仓库内,规范符合性无关紧要,而且这会给一个浏览器安全的包添加依赖。(对比 [llm-deepseek 提案](../../implemented/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md):那里线路对面是真实的提供方。)
 
 
 **重试、定时器与异步:**
 **重试、定时器与异步:**
 
 

+ 1 - 1
AGENTS.md

@@ -12,7 +12,7 @@ DeepSeek Harness SDK is a plugin-based agent harness on vendored Cordis: **every
 vendor/      Vendored Cordis source — manifest + sync procedure in vendor/README.md
 vendor/      Vendored Cordis source — manifest + sync procedure in vendor/README.md
 packages/    @deepseek-ai/dsh-<pkg> workspaces at packages/<group>/<pkg>/
 packages/    @deepseek-ai/dsh-<pkg> workspaces at packages/<group>/<pkg>/
   core/        product API spine: session, system-prompt, tools, agent, agent-loop
   core/        product API spine: session, system-prompt, tools, agent, agent-loop
-  llm/         LLM seam + DeepSeek adapters (hand-rolled + pi-ai design twin)
+  llm/         LLM seam + DeepSeek adapters (direct-fetch + pi-ai design twin)
   bash/        bash executor seam + local impl + model-facing bash tools
   bash/        bash executor seam + local impl + model-facing bash tools
   pty/         persistent PTY seam/backend/tools
   pty/         persistent PTY seam/backend/tools
   fs/          filesystem seam + local impl + policy gate + read/write/edit tools
   fs/          filesystem seam + local impl + policy gate + read/write/edit tools

+ 1 - 1
docs/config-catalog.md

@@ -586,7 +586,7 @@ export interface Config {
   streamIdleTimeoutMs?: number
   streamIdleTimeoutMs?: number
 }
 }
 
 
-/** One optional model entry advertised by the hand-written adapter. */
+/** One optional model entry advertised by the direct-fetch adapter. */
 export interface DeepSeekCatalogModel {
 export interface DeepSeekCatalogModel {
   /** Wire model id accepted by the configured endpoint. */
   /** Wire model id accepted by the configured endpoint. */
   id: string
   id: string

+ 3 - 3
docs/cookbook/adding-an-llm-adapter.i18n.yaml

@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write
-adding-an-llm-adapter.md: 659fdab30e753c9a7acf9e06f85ebfc83480ec50
-adding-an-llm-adapter.zh.md: 5e2a84c82f9a8025c951b1bb2b44ba8b8950f5e3
+#   pnpm run verify-translation-pairing --write docs/cookbook/adding-an-llm-adapter.md
+adding-an-llm-adapter.md: a7f9dced70041653a0cb815147a07b6386d79e3e
+adding-an-llm-adapter.zh.md: 3515927585201326b713bb03cd863886ce7846bd

+ 1 - 1
docs/cookbook/adding-an-llm-adapter.md

@@ -2,7 +2,7 @@
 
 
 English | [中文](adding-an-llm-adapter.zh.md)
 English | [中文](adding-an-llm-adapter.zh.md)
 
 
-How to connect a new model provider. Reference implementations: `packages/llm/llm-deepseek` (hand-rolled HTTP/SSE) and `packages/llm/llm-pi-ai` (wrapping an LLM library). Read the `StreamChunk` doc in `packages/llm/llm/src/types.ts` first — it records the protocol conventions both adapters were verified against.
+How to connect a new model provider. Reference implementations: `packages/llm/llm-deepseek` (direct HTTP, SSE framed by `eventsource-parser`) and `packages/llm/llm-pi-ai` (wrapping an LLM library). Read the `StreamChunk` doc in `packages/llm/llm/src/types.ts` first — it records the protocol conventions both adapters were verified against.
 
 
 ## The shape
 ## The shape
 
 

+ 1 - 1
docs/cookbook/adding-an-llm-adapter.zh.md

@@ -2,7 +2,7 @@
 
 
 [English](adding-an-llm-adapter.md) | 中文
 [English](adding-an-llm-adapter.md) | 中文
 
 
-如何接入一个新的模型提供方。参考实现:`packages/llm/llm-deepseek`(手写 HTTP/SSE)与 `packages/llm/llm-pi-ai`(封装 LLM 库)。请先阅读 `packages/llm/llm/src/types.ts` 中的 `StreamChunk` 文档——它记录了两个适配器都经过验证的协议约定。
+如何接入一个新的模型提供方。参考实现:`packages/llm/llm-deepseek`(直接 HTTP,SSE 由 `eventsource-parser` 分帧)与 `packages/llm/llm-pi-ai`(封装 LLM 库)。请先阅读 `packages/llm/llm/src/types.ts` 中的 `StreamChunk` 文档——它记录了两个适配器都经过验证的协议约定。
 
 
 ## 基本形态
 ## 基本形态
 
 

+ 3 - 3
docs/core-data-structures/llm-streaming.i18n.yaml

@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write
-llm-streaming.md: ffe520eca12af0fa2d483b9f231c943107d74649
-llm-streaming.zh.md: d4d41040ceaa7e2b2ca08c47ae83fefbaba3f7d4
+#   pnpm run verify-translation-pairing --write docs/core-data-structures/llm-streaming.md
+llm-streaming.md: 9151ba569af6b640144c1850d616b6f762b68a8b
+llm-streaming.zh.md: ae4a6843b9bf4f09169b17271c7edea76f8f2051

+ 3 - 3
docs/core-data-structures/llm-streaming.md

@@ -67,7 +67,7 @@ Every adapter MUST obey these, and every consumer may rely on them:
 - **Every provider HTTP request carries the app-attribution header.** Adapters send `attributionHeaders()` (below) - the `User-Agent` baseline - and prove it with a wire-level test (mock server asserting the received header, or the library's header hook for a library-backed adapter).
 - **Every provider HTTP request carries the app-attribution header.** Adapters send `attributionHeaders()` (below) - the `User-Agent` baseline - and prove it with a wire-level test (mock server asserting the received header, or the library's header hook for a library-backed adapter).
 - **Replay state is adapter-owned.** A successful `finish` may carry lossless-JSON state needed to reconstruct a native provider response. The loop stores it with the assembled assistant message unless an `agent/step-result` listener rewrote the content. On a later request, `LlmService` passes the state only when the historical provider and target provider are currently registered to the exact same adapter instance. That adapter validates the state and owns any cross-model or cross-provider conversion; other adapters receive the provider-neutral content and provenance without the private state.
 - **Replay state is adapter-owned.** A successful `finish` may carry lossless-JSON state needed to reconstruct a native provider response. The loop stores it with the assembled assistant message unless an `agent/step-result` listener rewrote the content. On a later request, `LlmService` passes the state only when the historical provider and target provider are currently registered to the exact same adapter instance. That adapter validates the state and owns any cross-model or cross-provider conversion; other adapters receive the provider-neutral content and provenance without the private state.
 
 
-This contract is pinned down by two deliberately independent implementations: `dsh-llm-deepseek` (hand-rolled fetch/SSE) and `dsh-llm-pi-ai` (a generic multi-provider adapter through `@earendil-works/pi-ai`). The library-backed adapter exercises the finish-chunk error path, while transport-boundary tests prove each idle watchdog stops its actual request.
+This contract is pinned down by two deliberately independent implementations: `dsh-llm-deepseek` (direct fetch, SSE framing via `eventsource-parser`) and `dsh-llm-pi-ai` (a generic multi-provider adapter through `@earendil-works/pi-ai`). The library-backed adapter exercises the finish-chunk error path, while transport-boundary tests prove each idle watchdog stops its actual request.
 
 
 ## `AppIdentity` — app attribution
 ## `AppIdentity` — app attribution
 
 
@@ -179,8 +179,8 @@ interface PreparedLlmCall {
 /**
 /**
  * Provider-wire adapter for the harness message and stream vocabulary. Register implementations
  * Provider-wire adapter for the harness message and stream vocabulary. Register implementations
  * with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include
  * with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include
- * `attributionHeaders()`; prove that at the wire or library header-hook boundary. The hand-rolled
- * DeepSeek and pi-ai adapters intentionally exercise this contract through different internals.
+ * `attributionHeaders()`; prove that at the wire or library header-hook boundary. The direct-fetch
+ * DeepSeek and library-backed pi-ai adapters intentionally exercise this contract through different internals.
  */
  */
 declare abstract class LlmAdapter {
 declare abstract class LlmAdapter {
   /**
   /**

+ 3 - 3
docs/core-data-structures/llm-streaming.zh.md

@@ -67,7 +67,7 @@ interface LlmFailure {
 - **每个提供方 HTTP 请求都携带应用归属头。** 适配器发送 `attributionHeaders()`(见下文)作为 `User-Agent` 基线,并通过协议级测试加以证明(mock 服务器断言收到的 header,或对基于库的适配器使用库的 header 钩子)。
 - **每个提供方 HTTP 请求都携带应用归属头。** 适配器发送 `attributionHeaders()`(见下文)作为 `User-Agent` 基线,并通过协议级测试加以证明(mock 服务器断言收到的 header,或对基于库的适配器使用库的 header 钩子)。
 - **回放状态归适配器所有。** 成功的 `finish` 可以携带重建提供方原生响应所需的无损 JSON 状态。除非 `agent/step-result` listener 改写了内容,否则循环会将其与组装后的 assistant 消息一起存储。后续请求中,仅当历史提供方与目标提供方当前注册到完全相同的适配器实例时,`LlmService` 才会传递该状态。该适配器负责校验状态并拥有所有跨模型或跨提供方转换;其他适配器只会收到提供方无关的内容与 provenance,不会收到私有状态。
 - **回放状态归适配器所有。** 成功的 `finish` 可以携带重建提供方原生响应所需的无损 JSON 状态。除非 `agent/step-result` listener 改写了内容,否则循环会将其与组装后的 assistant 消息一起存储。后续请求中,仅当历史提供方与目标提供方当前注册到完全相同的适配器实例时,`LlmService` 才会传递该状态。该适配器负责校验状态并拥有所有跨模型或跨提供方转换;其他适配器只会收到提供方无关的内容与 provenance,不会收到私有状态。
 
 
-该契约由两个有意保持独立的实现锁定:`dsh-llm-deepseek`(手写 fetch/SSE(Server-Sent Events))和 `dsh-llm-pi-ai`(通过 `@earendil-works/pi-ai` 实现的通用多提供方适配器)。基于库的适配器覆盖 finish 分片错误路径,而传输边界测试证明每个空闲 watchdog 都会停止其实际请求。
+该契约由两个有意保持独立的实现锁定:`dsh-llm-deepseek`(直接 fetch,SSE(Server-Sent Events)分帧经由 `eventsource-parser`)和 `dsh-llm-pi-ai`(通过 `@earendil-works/pi-ai` 实现的通用多提供方适配器)。基于库的适配器覆盖 finish 分片错误路径,而传输边界测试证明每个空闲 watchdog 都会停止其实际请求。
 
 
 ## `AppIdentity`:应用归属
 ## `AppIdentity`:应用归属
 
 
@@ -179,8 +179,8 @@ interface PreparedLlmCall {
 /**
 /**
  * Provider-wire adapter for the harness message and stream vocabulary. Register implementations
  * Provider-wire adapter for the harness message and stream vocabulary. Register implementations
  * with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include
  * with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include
- * `attributionHeaders()`; prove that at the wire or library header-hook boundary. The hand-rolled
- * DeepSeek and pi-ai adapters intentionally exercise this contract through different internals.
+ * `attributionHeaders()`; prove that at the wire or library header-hook boundary. The direct-fetch
+ * DeepSeek and library-backed pi-ai adapters intentionally exercise this contract through different internals.
  */
  */
 declare abstract class LlmAdapter {
 declare abstract class LlmAdapter {
   /**
   /**

+ 2 - 2
packages/llm/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write
 #   pnpm run verify-translation-pairing --write
-README.md: 0278a4a582e535125d001e09736b89f13be72a0c
-README.zh.md: e3e2b9559d69e4be10cd4d373bbda2dd47396b72
+README.md: 13a04aa9f73fec5824069644449009989d6fd924
+README.zh.md: 3e417c5f8be1f7831b99940c2a4aec815dc2c5b6

+ 1 - 1
packages/llm/README.md

@@ -9,7 +9,7 @@ The LLM seam and its provider adapters. The interface package (`llm`) owns the a
 | `llm/` | Abstract LLM service + content-block vocabulary + chunk assembler | `ctx.llm` |
 | `llm/` | Abstract LLM service + content-block vocabulary + chunk assembler | `ctx.llm` |
 | `token-meter/` | Replay-aware request and surface token measurement | `ctx.tokenMeter` |
 | `token-meter/` | Replay-aware request and surface token measurement | `ctx.tokenMeter` |
 | `llm-retry/` | Bounded transient request retry policy | (listens to `agent/request-error`) |
 | `llm-retry/` | Bounded transient request retry policy | (listens to `agent/request-error`) |
-| `llm-deepseek/` | DeepSeek API adapter (hand-rolled fetch/SSE) | (registers on `ctx.llm`) |
+| `llm-deepseek/` | DeepSeek API adapter (direct fetch + eventsource-parser SSE) | (registers on `ctx.llm`) |
 | `llm-pi-ai/` | Multi-provider adapter via `@earendil-works/pi-ai` | (registers on `ctx.llm`) |
 | `llm-pi-ai/` | Multi-provider adapter via `@earendil-works/pi-ai` | (registers on `ctx.llm`) |
 
 
 The interface lives at `llm/llm/`; adapters, retry policy, and the reusable token meter are flat siblings under the group. Requests route by `provider`, while `model` is passed through to the selected adapter. The route-owning adapter optionally resolves exact provider/model context capacity; the token meter remains model-agnostic. A new provider adapter registers one or more provider routes on `ctx.llm` without touching the interface or consumers. See [twin LLM adapters](../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the two shipping implementations, the [replay token meter Agent Note](../../.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md) for measurement ownership, and the [routed model context Agent Note](../../.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.md) for capacity and compaction-policy ownership.
 The interface lives at `llm/llm/`; adapters, retry policy, and the reusable token meter are flat siblings under the group. Requests route by `provider`, while `model` is passed through to the selected adapter. The route-owning adapter optionally resolves exact provider/model context capacity; the token meter remains model-agnostic. A new provider adapter registers one or more provider routes on `ctx.llm` without touching the interface or consumers. See [twin LLM adapters](../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the two shipping implementations, the [replay token meter Agent Note](../../.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md) for measurement ownership, and the [routed model context Agent Note](../../.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.md) for capacity and compaction-policy ownership.

+ 1 - 1
packages/llm/README.zh.md

@@ -9,7 +9,7 @@ LLM seam 及其提供方适配器。接口包(`llm`)拥有抽象服务、内
 | `llm/` | 抽象 LLM 服务 + 内容块词汇 + 分片组装器 | `ctx.llm` |
 | `llm/` | 抽象 LLM 服务 + 内容块词汇 + 分片组装器 | `ctx.llm` |
 | `token-meter/` | 感知回放的请求与表层 token 测量 | `ctx.tokenMeter` |
 | `token-meter/` | 感知回放的请求与表层 token 测量 | `ctx.tokenMeter` |
 | `llm-retry/` | 有界的暂时性请求重试策略 | (监听 `agent/request-error`) |
 | `llm-retry/` | 有界的暂时性请求重试策略 | (监听 `agent/request-error`) |
-| `llm-deepseek/` | DeepSeek API 适配器(手写 fetch/SSE) | (注册到 `ctx.llm`) |
+| `llm-deepseek/` | DeepSeek API 适配器(直接 fetch + eventsource-parser SSE) | (注册到 `ctx.llm`) |
 | `llm-pi-ai/` | 通过 `@earendil-works/pi-ai` 实现的多提供方适配器 | (注册到 `ctx.llm`) |
 | `llm-pi-ai/` | 通过 `@earendil-works/pi-ai` 实现的多提供方适配器 | (注册到 `ctx.llm`) |
 
 
 接口位于 `llm/llm/`;适配器、重试策略和可复用的 token 计量器都是该分组下的扁平兄弟包。请求按 `provider` 路由,而 `model` 会原样传给选中的适配器。拥有路由的适配器可以解析精确的提供方/模型上下文容量;token 计量器仍与模型无关。新的提供方适配器只需在 `ctx.llm` 上注册一个或多个提供方路由,无需改动接口或消费方。两个已交付实现见[双生 LLM 适配器](../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md),测量归属见[回放 token 计量器 Agent Note](../../.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md),容量与压缩策略归属见[路由模型上下文 Agent Note](../../.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.md)。
 接口位于 `llm/llm/`;适配器、重试策略和可复用的 token 计量器都是该分组下的扁平兄弟包。请求按 `provider` 路由,而 `model` 会原样传给选中的适配器。拥有路由的适配器可以解析精确的提供方/模型上下文容量;token 计量器仍与模型无关。新的提供方适配器只需在 `ctx.llm` 上注册一个或多个提供方路由,无需改动接口或消费方。两个已交付实现见[双生 LLM 适配器](../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md),测量归属见[回放 token 计量器 Agent Note](../../.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md),容量与压缩策略归属见[路由模型上下文 Agent Note](../../.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.md)。

+ 2 - 2
packages/llm/llm-deepseek/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md
 #   pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md
-README.md: cfa11f34a69b1098d0ff894dd2193cfbf9809be3
-README.zh.md: b3ec9413427a4cb1a22152e0a1d9c9e432cdad8d
+README.md: 4358620295547248ca87c42e07022c5eab0c947b
+README.zh.md: 4ecc5dd2e5980751ca6e724b5041efefc8114077

+ 1 - 1
packages/llm/llm-deepseek/README.md

@@ -2,7 +2,7 @@
 
 
 English | [中文](README.zh.md)
 English | [中文](README.zh.md)
 
 
-DeepSeek chat-completions adapter for the harness LLM seam: hand-rolled `fetch` + SSE translation from the official wire format (source of truth: the API docs — guides/thinking_mode, guides/tool_calls, api/create-chat-completion) into the `StreamChunk` protocol.
+DeepSeek chat-completions adapter for the harness LLM seam: direct `fetch` + SSE (framed by `eventsource-parser`) translating the official wire format (source of truth: the API docs — guides/thinking_mode, guides/tool_calls, api/create-chat-completion) into the `StreamChunk` protocol.
 
 
 A second, library-backed implementation of the same seam exists in `@deepseek-ai/dsh-llm-pi-ai`. This package always owns the `deepseek` provider route; mounting a pi-ai profile with `provider: deepseek` in the same context throws `LlmError('DUPLICATE_ADAPTER')` by design.
 A second, library-backed implementation of the same seam exists in `@deepseek-ai/dsh-llm-pi-ai`. This package always owns the `deepseek` provider route; mounting a pi-ai profile with `provider: deepseek` in the same context throws `LlmError('DUPLICATE_ADAPTER')` by design.
 
 

+ 1 - 1
packages/llm/llm-deepseek/README.zh.md

@@ -2,7 +2,7 @@
 
 
 [English](README.md) | 中文
 [English](README.md) | 中文
 
 
-harness LLM seam 的 DeepSeek chat-completions 适配器:手写 `fetch` + SSE,将官方协议格式(真源:API 文档 guides/thinking_mode、guides/tool_calls、api/create-chat-completion)转换为 `StreamChunk` 协议。
+harness LLM seam 的 DeepSeek chat-completions 适配器:直接 `fetch` + SSE(由 `eventsource-parser` 分帧),将官方协议格式(真源:API 文档 guides/thinking_mode、guides/tool_calls、api/create-chat-completion)转换为 `StreamChunk` 协议。
 
 
 同一 seam 的第二个库支持实现位于 `@deepseek-ai/dsh-llm-pi-ai`。本包始终拥有 `deepseek` 提供方路由;在同一上下文中装载 `provider: deepseek` 的 pi-ai profile 会按设计抛出 `LlmError('DUPLICATE_ADAPTER')`。
 同一 seam 的第二个库支持实现位于 `@deepseek-ai/dsh-llm-pi-ai`。本包始终拥有 `deepseek` 提供方路由;在同一上下文中装载 `provider: deepseek` 的 pi-ai profile 会按设计抛出 `LlmError('DUPLICATE_ADAPTER')`。
 
 

+ 1 - 0
packages/llm/llm-deepseek/package.json

@@ -33,6 +33,7 @@
     "cordis": "^4.0.0-rc.7"
     "cordis": "^4.0.0-rc.7"
   },
   },
   "dependencies": {
   "dependencies": {
+    "eventsource-parser": "^3.1.0",
     "schemastery": "^3.18.0"
     "schemastery": "^3.18.0"
   },
   },
   "devDependencies": {
   "devDependencies": {

+ 1 - 1
packages/llm/llm-deepseek/src/adapter.ts

@@ -20,7 +20,7 @@ import { parseSse } from './sse.ts'
 import { translate } from './translate.ts'
 import { translate } from './translate.ts'
 import type { WireError } from './types.ts'
 import type { WireError } from './types.ts'
 
 
-/** One optional model entry advertised by the hand-written adapter. */
+/** One optional model entry advertised by the direct-fetch adapter. */
 export interface DeepSeekCatalogModel {
 export interface DeepSeekCatalogModel {
   /** Wire model id accepted by the configured endpoint. */
   /** Wire model id accepted by the configured endpoint. */
   id: string
   id: string

+ 14 - 46
packages/llm/llm-deepseek/src/sse.ts

@@ -1,65 +1,33 @@
 /**
 /**
- * Decode an SSE byte stream into event `data` payloads. Network reads may split UTF-8 or lines;
- * CRLF, comments, non-data fields, and multi-data events are handled per SSE rules. The literal
- * `[DONE]` is yielded so the caller owns final flushing, and EOF before it raises {@link LlmError}.
+ * Decode an SSE byte stream into event `data` payloads. Framing — chunk
+ * reassembly, UTF-8/CRLF/BOM handling, comment and non-data field skipping,
+ * multi-`data:` joining — is `eventsource-parser`'s; this module keeps only
+ * the DeepSeek protocol: the literal `[DONE]` is yielded so the caller owns
+ * final flushing, and EOF before it raises {@link LlmError}. Framing is
+ * spec-strict: an event dispatches only on its blank-line terminator, so an
+ * unterminated tail at EOF is truncation, not a flushable payload.
  *
  *
- * Minimal SSE (text/event-stream) parser for the chat-completions stream.
  * @module dsh-llm-deepseek/sse
  * @module dsh-llm-deepseek/sse
  */
  */
 
 
+import { EventSourceParserStream } from 'eventsource-parser/stream'
 import { LlmError } from '@deepseek-ai/dsh-llm'
 import { LlmError } from '@deepseek-ai/dsh-llm'
 
 
 /** The terminal payload DeepSeek (and OpenAI) send after the last chunk. */
 /** The terminal payload DeepSeek (and OpenAI) send after the last chunk. */
 export const DONE = '[DONE]'
 export const DONE = '[DONE]'
 
 
-/** Extract the joined data payload from one raw SSE event block. */
-function eventData(block: string): string | undefined {
-  const data: string[] = []
-  for (const rawLine of block.split('\n')) {
-    const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine
-    if (line.startsWith('data:')) {
-      // The spec strips ONE leading space after the colon.
-      data.push(line.startsWith('data: ') ? line.slice(6) : line.slice(5))
-    }
-    // Comments (':…') and other fields (event:, id:, retry:) are ignored.
-  }
-  if (data.length === 0) return undefined
-  return data.join('\n')
-}
-
 /**
 /**
- * Parse a byte stream into SSE data payloads. Yields `[DONE]` as the final
+ * Parse an SSE byte stream into data payloads. Yields `[DONE]` as the final
  * value and returns; throws `LlmError('STREAM_CLOSED')` when the stream ends
  * value and returns; throws `LlmError('STREAM_CLOSED')` when the stream ends
  * without it (truncated response — the model call cannot be trusted).
  * without it (truncated response — the model call cannot be trusted).
  * @param stream - raw SSE bytes; reads may split anywhere, including mid-UTF-8 sequence.
  * @param stream - raw SSE bytes; reads may split anywhere, including mid-UTF-8 sequence.
  * @returns each event's data payload in arrival order, the `[DONE]` sentinel last.
  * @returns each event's data payload in arrival order, the `[DONE]` sentinel last.
  */
  */
-export async function* parseSse(stream: AsyncIterable<Uint8Array>): AsyncGenerator<string> {
-  const decoder = new TextDecoder()
-  let buffer = ''
-
-  for await (const bytes of stream) {
-    buffer += decoder.decode(bytes, { stream: true })
-    // Events are separated by a blank line (\n\n; tolerate \r\n\r\n via the
-    // per-line \r strip in eventData and a normalized split here).
-    let boundary: number
-    while ((boundary = buffer.search(/\r?\n\r?\n/)) !== -1) {
-      const matched = /\r?\n\r?\n/.exec(buffer.slice(boundary))
-      const block = buffer.slice(0, boundary)
-      // matched cannot be null: search() just found the same pattern at 0.
-      buffer = buffer.slice(boundary + (matched as RegExpExecArray)[0].length)
-      const data = eventData(block)
-      if (data === undefined) continue
-      yield data
-      if (data === DONE) return
-    }
-  }
-
-  // Flush any final un-terminated event (servers usually end with \n\n, but
-  // a trailing block without one is still parseable).
-  buffer += decoder.decode()
-  const data = eventData(buffer)
-  if (data !== undefined) {
+export async function* parseSse(stream: ReadableStream<BufferSource>): AsyncGenerator<string> {
+  const events = stream
+    .pipeThrough(new TextDecoderStream())
+    .pipeThrough(new EventSourceParserStream())
+  for await (const { data } of events) {
     yield data
     yield data
     if (data === DONE) return
     if (data === DONE) return
   }
   }

+ 1 - 1
packages/llm/llm-deepseek/tests/adapter.e2e.ts

@@ -7,7 +7,7 @@ import type { Config } from '@deepseek-ai/dsh-llm-deepseek'
 import { assemble, type AssembledResult } from './assemble.ts'
 import { assemble, type AssembledResult } from './assemble.ts'
 
 
 /**
 /**
- * Real-API e2e for the hand-rolled adapter: V4 Flash + V4 Pro across
+ * Real-API e2e for the direct-fetch adapter: V4 Flash + V4 Pro across
  * thinking modes and both official effort levels. Key-gated — skips
  * thinking modes and both official effort levels. Key-gated — skips
  * entirely without $DEEPSEEK_API_KEY (see vitest.e2e.config.ts).
  * entirely without $DEEPSEEK_API_KEY (see vitest.e2e.config.ts).
  */
  */

+ 23 - 73
packages/llm/llm-deepseek/tests/sse.spec.ts

@@ -2,12 +2,21 @@ import { describe, expect, it } from 'vitest'
 import { LlmError } from '@deepseek-ai/dsh-llm'
 import { LlmError } from '@deepseek-ai/dsh-llm'
 import { DONE, parseSse } from '../src/sse.ts'
 import { DONE, parseSse } from '../src/sse.ts'
 
 
-/** Build a byte stream from string fragments (fragments = network reads). */
-async function* bytes(...fragments: (string | Uint8Array)[]): AsyncGenerator<Uint8Array> {
+/**
+ * DeepSeek protocol contract only: the [DONE] sentinel and STREAM_CLOSED on
+ * EOF without it. SSE framing (chunk splits, CRLF, multi-data joins, comments)
+ * is eventsource-parser's contract, not re-proven here.
+ */
+
+/** Build an SSE byte stream from string fragments (fragments = network reads). */
+function bytes(...fragments: string[]): ReadableStream<Uint8Array<ArrayBuffer>> {
   const encoder = new TextEncoder()
   const encoder = new TextEncoder()
-  for (const fragment of fragments) {
-    yield typeof fragment === 'string' ? encoder.encode(fragment) : fragment
-  }
+  return new ReadableStream({
+    start(controller) {
+      for (const fragment of fragments) controller.enqueue(encoder.encode(fragment))
+      controller.close()
+    },
+  })
 }
 }
 
 
 async function collect(stream: AsyncIterable<string>): Promise<string[]> {
 async function collect(stream: AsyncIterable<string>): Promise<string[]> {
@@ -17,57 +26,14 @@ async function collect(stream: AsyncIterable<string>): Promise<string[]> {
 }
 }
 
 
 describe('parseSse', () => {
 describe('parseSse', () => {
-  it('parses simple events and the DONE sentinel', async () => {
+  it('yields event payloads and the DONE sentinel', async () => {
     const events = await collect(parseSse(bytes('data: {"a":1}\n\ndata: [DONE]\n\n')))
     const events = await collect(parseSse(bytes('data: {"a":1}\n\ndata: [DONE]\n\n')))
     expect(events).toEqual(['{"a":1}', DONE])
     expect(events).toEqual(['{"a":1}', DONE])
   })
   })
 
 
-  it('handles events split across reads at arbitrary positions', async () => {
-    const events = await collect(parseSse(bytes('da', 'ta: {"a"', ':1}\n', '\ndata: [DO', 'NE]\n\n')))
-    expect(events).toEqual(['{"a":1}', DONE])
-  })
-
-  it('handles multi-byte UTF-8 split across reads', async () => {
-    const encoded = new TextEncoder().encode('data: {"text":"日本語"}\n\ndata: [DONE]\n\n')
-    // Split inside the 3-byte sequence for 日.
-    const splitAt = 16
-    const events = await collect(parseSse(bytes(encoded.slice(0, splitAt), encoded.slice(splitAt))))
-    expect(events).toEqual(['{"text":"日本語"}', DONE])
-  })
-
-  it('tolerates CRLF line endings', async () => {
-    const events = await collect(parseSse(bytes('data: {"a":1}\r\n\r\ndata: [DONE]\r\n\r\n')))
-    expect(events).toEqual(['{"a":1}', DONE])
-  })
-
-  it('joins multi-data events with newlines (SSE spec)', async () => {
-    const events = await collect(parseSse(bytes('data: line1\ndata: line2\n\ndata: [DONE]\n\n')))
-    expect(events).toEqual(['line1\nline2', DONE])
-  })
-
-  it('ignores comments and non-data fields', async () => {
-    const events = await collect(parseSse(bytes(': keepalive\nevent: chunk\nid: 7\ndata: {"a":1}\n\ndata: [DONE]\n\n')))
-    expect(events).toEqual(['{"a":1}', DONE])
-  })
-
-  it('skips blocks without data fields', async () => {
-    const events = await collect(parseSse(bytes(': ping\n\ndata: {"a":1}\n\ndata: [DONE]\n\n')))
-    expect(events).toEqual(['{"a":1}', DONE])
-  })
-
-  it('preserves data lines without the optional space', async () => {
-    const events = await collect(parseSse(bytes('data:{"a":1}\n\ndata:[DONE]\n\n')))
-    expect(events).toEqual(['{"a":1}', DONE])
-  })
-
-  it('parses several events from one read', async () => {
-    const events = await collect(parseSse(bytes('data: 1\n\ndata: 2\n\ndata: [DONE]\n\n')))
-    expect(events).toEqual(['1', '2', DONE])
-  })
-
-  it('flushes a final un-terminated DONE at stream end', async () => {
-    const events = await collect(parseSse(bytes('data: {"a":1}\n\ndata: [DONE]')))
-    expect(events).toEqual(['{"a":1}', DONE])
+  it('stops yielding after DONE even when more data follows', async () => {
+    const events = await collect(parseSse(bytes('data: [DONE]\n\ndata: {"late":1}\n\n')))
+    expect(events).toEqual([DONE])
   })
   })
 
 
   it('throws STREAM_CLOSED when the stream ends without DONE', async () => {
   it('throws STREAM_CLOSED when the stream ends without DONE', async () => {
@@ -83,26 +49,10 @@ describe('parseSse', () => {
     await expect(collect(parseSse(bytes('data: {"a"')))).rejects.toThrow(/without \[DONE\]/)
     await expect(collect(parseSse(bytes('data: {"a"')))).rejects.toThrow(/without \[DONE\]/)
   })
   })
 
 
-  it('stops yielding after DONE even when more data follows', async () => {
-    const events = await collect(parseSse(bytes('data: [DONE]\n\ndata: {"late":1}\n\n')))
-    expect(events).toEqual([DONE])
-  })
-})
-
-describe('parseSse edge branches', () => {
-  it('handles a lone CR-terminated data line', async () => {
-    // Exercises the \r-strip branch on a line that is ONLY "data:…\r".
-    const events = await collect(parseSse(bytes('data: {"a":1}\r\n\r\ndata:[DONE]\r\n\r\n')))
-    expect(events).toEqual(['{"a":1}', DONE])
-  })
-
-  it('strips CR from non-data field lines too', async () => {
-    const events = await collect(parseSse(bytes('event: chunk\r\ndata: {"a":1}\n\ndata: [DONE]\n\n')))
-    expect(events).toEqual(['{"a":1}', DONE])
-  })
-
-  it('treats bare "data:" lines as empty payload entries', async () => {
-    const events = await collect(parseSse(bytes('data:\ndata: x\n\ndata: [DONE]\n\n')))
-    expect(events).toEqual(['\nx', DONE])
+  it('treats a final DONE missing its blank-line terminator as truncation', async () => {
+    // Spec-strict framing: an event dispatches only on its blank-line
+    // terminator, so an unterminated tail at EOF is STREAM_CLOSED — real
+    // providers always terminate events, so a missing terminator is truncation.
+    await expect(collect(parseSse(bytes('data: {"a":1}\n\ndata: [DONE]')))).rejects.toThrow(/without \[DONE\]/)
   })
   })
 })
 })

+ 2 - 2
packages/llm/llm/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/llm/llm/README.md
 #   pnpm run verify-translation-pairing --write packages/llm/llm/README.md
-README.md: e11b997cddaacaea7fec00f73b7f5b47c60a943d
-README.zh.md: e074d7f764c130b4ea3df343f566b27ef3a5dd3d
+README.md: 3efb3ece3caadeaceaa3c504ba4b10ddb951127a
+README.zh.md: 4af8b8d08cc96ff0e36b10b15e1d86afd43004c9

+ 1 - 1
packages/llm/llm/README.md

@@ -64,7 +64,7 @@ Every product adapter sends application identity on provider HTTP requests. `att
 
 
 ### Real adapters
 ### Real adapters
 
 
-Two adapters implement `LlmAdapter` on different internals: [`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) uses hand-rolled fetch/SSE for the `deepseek` route, while [`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) dynamically resolves configured provider/model pairs through `@earendil-works/pi-ai`. Both follow the `StreamChunk` conventions in `types.ts`: usage precedes finish, tool arguments remain raw strings, and errors take one of two sanctioned paths. See [the twin LLM adapters](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the design rationale.
+Two adapters implement `LlmAdapter` on different internals: [`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) uses direct fetch with `eventsource-parser` SSE framing for the `deepseek` route, while [`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) dynamically resolves configured provider/model pairs through `@earendil-works/pi-ai`. Both follow the `StreamChunk` conventions in `types.ts`: usage precedes finish, tool arguments remain raw strings, and errors take one of two sanctioned paths. See [the twin LLM adapters](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the design rationale.
 
 
 ## Model Experience
 ## Model Experience
 
 

+ 1 - 1
packages/llm/llm/README.zh.md

@@ -64,7 +64,7 @@
 
 
 ### 真实适配器
 ### 真实适配器
 
 
-两个适配器使用不同内部机制实现 `LlmAdapter`:[`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) 针对 `deepseek` 路由使用手写 fetch/SSE,[`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) 则通过 `@earendil-works/pi-ai` 动态解析已配置提供方/模型对。两者都遵循 `StreamChunk` 约定,定义见 `types.ts`:usage 先于 finish,工具参数保持原始字符串,错误使用两种已批准路径之一。设计理由见 [双 LLM 适配器](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md)。
+两个适配器使用不同内部机制实现 `LlmAdapter`:[`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) 针对 `deepseek` 路由使用直接 fetch 加 `eventsource-parser` SSE 分帧,[`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) 则通过 `@earendil-works/pi-ai` 动态解析已配置提供方/模型对。两者都遵循 `StreamChunk` 约定,定义见 `types.ts`:usage 先于 finish,工具参数保持原始字符串,错误使用两种已批准路径之一。设计理由见 [双 LLM 适配器](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md)。
 
 
 ## 模型体验
 ## 模型体验
 
 

+ 2 - 2
packages/llm/llm/src/index.ts

@@ -121,8 +121,8 @@ export interface PreparedLlmCall {
 /**
 /**
  * Provider-wire adapter for the harness message and stream vocabulary. Register implementations
  * Provider-wire adapter for the harness message and stream vocabulary. Register implementations
  * with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include
  * with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include
- * `attributionHeaders()`; prove that at the wire or library header-hook boundary. The hand-rolled
- * DeepSeek and pi-ai adapters intentionally exercise this contract through different internals.
+ * `attributionHeaders()`; prove that at the wire or library header-hook boundary. The direct-fetch
+ * DeepSeek and library-backed pi-ai adapters intentionally exercise this contract through different internals.
  */
  */
 export abstract class LlmAdapter {
 export abstract class LlmAdapter {
   /**
   /**

+ 2 - 2
packages/sdk/helper/src/features/builtin/provider.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Required hand-rolled DeepSeek and custom pi-ai provider behavior.
+ * Required direct-fetch DeepSeek and custom pi-ai provider behavior.
  *
  *
  * @module @deepseek-ai/dsh-helper/features/builtin/provider
  * @module @deepseek-ai/dsh-helper/features/builtin/provider
  */
  */
@@ -74,7 +74,7 @@ export class ProviderFeature extends ExclusiveOptionFeature {
   override readonly required = true
   override readonly required = true
   override readonly options = [new DeepSeekOption(), new CustomOption()]
   override readonly options = [new DeepSeekOption(), new CustomOption()]
 
 
-  /** Prefer the hand-rolled adapter and its public endpoint defaults. */
+  /** Prefer the direct-fetch adapter and its public endpoint defaults. */
   override defaultOptions(): readonly string[] {
   override defaultOptions(): readonly string[] {
     return ['deepseek']
     return ['deepseek']
   }
   }

+ 3 - 0
pnpm-lock.yaml

@@ -2624,6 +2624,9 @@ importers:
 
 
   packages/llm/llm-deepseek:
   packages/llm/llm-deepseek:
     dependencies:
     dependencies:
+      eventsource-parser:
+        specifier: ^3.1.0
+        version: 3.1.0
       schemastery:
       schemastery:
         specifier: ^3.18.0
         specifier: ^3.18.0
         version: 3.18.0
         version: 3.18.0