Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/unreleased/dsh-0.1.7-rc1-realignment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Changed
---
- The CI keyless-runtime-handshake job now verifies the npm route against `@deepseek-ai/dsh@0.1.7-rc.1`: the crate's wire and launch contracts were re-verified green against that upstream tag before the pin moved. The exact version under test still lives in `.github/workflows/ci.yml`.
- The fake-runtime suite now locks pass-through of the newest upstream session-event payloads: flattened `role:'tool'` tool results and `developer/message` events flow through parsing, `Session::run` collection, and the notification callback verbatim.
- No public API change.
21 changes: 10 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,13 @@ jobs:
# and it is not a required check for docs-only PRs (making it a required
# gate is a PM decision, not a workflow change).
#
# Pin rationale: the SDK launch/wire contract is frozen to deepseek-harness @
# c389f96bf3a9b6807cb71ed6bdad5849be0df6d8
# (`dsh-v0.1.3-alpha.2-133-gc389f96bf3`, 2026-09-08) — 0.1.3-alpha.2 is the
# closest published tag on that upstream line (existence verified via
# `npm view @deepseek-ai/dsh versions --json`). The newer 0.1.5-alpha.1
# (2026-09-08) has no verified contract alignment, so it is deliberately
# not used. Exact pin, never a dist-tag: the handshake result is
# reproducible run to run.
# Pin rationale: the SDK launch/wire contract was re-verified against
# deepseek-harness `dsh-v0.1.7-rc.1` (`46a7f68b`, 2026-09-23) — the launch
# and wire spec re-verification greps are green at that tag (see the
# `.mstar/specs/` drift amendment for the one content-block cluster,
# covered by the crate's `Unknown` passthrough + fixture suite), and V4
# session events pass through as opaque values. Exact pin, never a
# dist-tag: the handshake result is reproducible run to run.
keyless-runtime-handshake:
name: keyless runtime handshake (pinned dsh)
runs-on: ubuntu-latest
Expand All @@ -87,11 +86,11 @@ jobs:
path: ~/.npm
# keyed on the exact pin: the tarball for that version is immutable,
# so a hit is the full npm cache the pinned global install needs
key: ${{ runner.os }}-npm-dsh-cli-0.1.3-alpha.2
key: ${{ runner.os }}-npm-dsh-cli-0.1.7-rc.1

- name: Install pinned dsh runtime
# exact pin (never a dist-tag); rationale in the job comment above
run: npm install -g @deepseek-ai/dsh@0.1.3-alpha.2
run: npm install -g @deepseek-ai/dsh@0.1.7-rc.1

- name: Resolve runtime binary
# Fail loudly when the pinned runtime cannot be resolved: an empty
Expand All @@ -100,7 +99,7 @@ jobs:
run: |
DSH_RUNTIME_BIN="$(command -v dsh)"
if [ -z "$DSH_RUNTIME_BIN" ]; then
echo "::error::dsh not found on PATH after npm install -g @deepseek-ai/dsh@0.1.3-alpha.2"
echo "::error::dsh not found on PATH after npm install -g @deepseek-ai/dsh@0.1.7-rc.1"
exit 1
fi
echo "DSH_RUNTIME_BIN=$DSH_RUNTIME_BIN" >> "$GITHUB_ENV"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ This document captures the reusable discipline only; the specs own the facts.

5. **Divergences must be documented, never implied.** The crate follows the runtime's `~/.dsh` fallback where the Python SDK raises. Documenting the divergence is what stops a future contributor from "fixing" the crate back into a wrapper's behavior (launch spec §3.3).

6. **Guard the launch contract with a keyless handshake plus an exact-pin CI job.** `tests/real_runtime.rs::real_runtime_handshake` resolves a real `dsh` (from `DSH_RUNTIME_BIN` or `PATH`), boots it with a temp `dsh_home`, and proves `start()` → `initialize` → `close()` with no API key; the live-turn tier stays behind `DEEPSEEK_API_KEY`. The CI job `keyless-runtime-handshake` (`.github/workflows/ci.yml`) installs one exact published version (`@deepseek-ai/dsh@0.1.3-alpha.2`, never a dist-tag) and its resolution step fails loudly when the binary is absent — an empty `DSH_RUNTIME_BIN` would otherwise make the test tier skip and the job go green without ever running the handshake. The keyless tier proves boot only, not a live turn; the README states that limit.
6. **Guard the launch contract with a keyless handshake plus an exact-pin CI job.** `tests/real_runtime.rs::real_runtime_handshake` resolves a real `dsh` (from `DSH_RUNTIME_BIN` or `PATH`), boots it with a temp `dsh_home`, and proves `start()` → `initialize` → `close()` with no API key; the live-turn tier stays behind `DEEPSEEK_API_KEY`. The CI job `keyless-runtime-handshake` (`.github/workflows/ci.yml` — the single owner of that pin) installs one exact published `@deepseek-ai/dsh` version (read the pinned `npm install -g` line there; never a dist-tag) and its resolution step fails loudly when the binary is absent — an empty `DSH_RUNTIME_BIN` would otherwise make the test tier skip and the job go green without ever running the handshake. The keyless tier proves boot only, not a live turn; the README states that limit.

## Why This Matters

Expand Down
28 changes: 26 additions & 2 deletions .mstar/specs/dsh-sdk-wire-parity-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Normative rules:

### 5.1 The six variants (normative)

`ContentBlockMap` has **six** members — `text`, `reasoning`, `image`, `file`, `tool-call`, `tool-result` (`packages/llm/llm/src/types.ts:112-118`). The crate MUST model all six as typed variants:
`ContentBlockMap` has **six** members — `text`, `reasoning`, `image`, `file`, `tool-call`, `tool-result` (`packages/llm/llm/src/types.ts:112-118`, frozen basis `c389f96bf3`). The crate MUST model all six as typed variants — a crate rule that stays normative even though the upstream map has since drifted (§5.1.1):

| Variant | Shape | Upstream evidence |
|---|---|---|
Expand All @@ -87,6 +87,16 @@ Normative rules:

`ImageAttachmentRef` MUST be `{attachmentId, mediaType, bytes, width, height, name?, originalDimensions?}` where `originalDimensions?: {width, height}` (`packages/attachment/attachment/src/types.ts:11-34`). `originalDimensions` is present only when normalization reduced the image (`packages/attachment/attachment/src/types.ts:24-31`); the crate MUST model it so that parse → serialize round-trips without loss (Track B F-4: re-serializing a parsed image block currently drops it).

#### 5.1.1 Drift amendment (2026-09-24): `ContentBlockMap` at `dsh-v0.1.7-rc.1`

Re-verification at tag `dsh-v0.1.7-rc.1` (commit `46a7f68b0922371ce7144b668b90e377d8e799f4`, 2026-09-23) found exactly one drifted cluster: the LLM-level `ContentBlockMap`. Upstream facts at that tag (all citations `path:line` at the tag):

- `ContentBlockMap` (`packages/llm/llm/src/types.ts:138-146`) has **seven** members: `text`, `reasoning`, `image`, `file`, `tool-call`, `tool-addition` (`types.ts:115-124`), `tool-removal` (`types.ts:127-130`). `tool-result` is no longer a member, and `ToolResultBlock` is deleted (no occurrence under `packages/llm`). The two new developer tool-change blocks are "reserved for Session V4 persistence; providers and UI reject them until their producers and consumers are implemented together" (map doc comment, `types.ts:132-137`).
- Tool results moved from a content-block `type` to a **message role**: `createToolResultMessage` produces `role: 'tool'` messages carrying `toolCallId`, `content: readonly ContentBlock[]`, and message-level `isError` (`packages/llm/llm/src/message.ts:287-305`). The old `{type, toolCallId, content, isError}` block shape still appears in SDK-client wire fixtures (`packages/sdk/client/tests/sdk-client.spec.ts:141`) and as a retired nested-block rejection case in provider serialization tests (`packages/llm/llm-deepseek/tests/serialize.spec.ts:453`).
- `ImageBlock` gained an optional `offloaded?: true` (`packages/llm/llm/src/types.ts:88`) — a merge-extensible addition.

**Crate divergence (normative).** The crate's surface is unchanged and remains normative as it stands: `ContentBlock` (`src/protocol.rs:35-78`) keeps its **six typed variants plus the `Unknown` passthrough**. Unknown upstream kinds — `tool-addition`, `tool-removal`, and any future map member — MUST degrade to `ContentBlock::Unknown(Value)` verbatim, per §1's unknown-tolerant rule and §5.2's escape hatch. `ContentBlock::ToolResult` stays a typed variant (the block shape survives in wire fixtures, and message-level tool-result `content` is still `ContentBlock[]`). `ImageBlock.offloaded` is not modeled (unknown fields on a known block are ignored per §5.2). This is peer-parity with the Python baseline, which is untyped at this level. A contributor MUST NOT "fix" the crate toward the upstream map — adding `tool-addition` / `tool-removal` variants, removing `ToolResult`, or modeling `offloaded` — without a superseding spec decision (PM decision 2026-09-24, plan `11-dsh-0.1.7-rc1-realignment`).

### 5.2 `file` is an addition, not a replacement

`ContentBlock::Unknown` MUST remain the fallback for unrecognized tags and malformed bodies. Adding `File` narrows what falls through; it MUST NOT remove the escape hatch, and the crate MUST NOT `deny_unknown_fields` anywhere on the wire path.
Expand All @@ -103,6 +113,10 @@ The rustdoc "known variants" count MUST say six, not five.

Because the crate keeps `session.event.event` untyped (§3.2), the run path is unaffected: `final_response` keeps the reversed-scan derivation of §6.2, and `finish_reason` reads the last root `turn/end`'s `data.reason.kind`. The crate MUST NOT claim `assistant/chunk` support and MUST NOT parse the embedded v2 stream (non-goal). Documentation MUST state the v2 vocabulary.

#### 5.3.1 Supersession amendment (2026-09-24): `SESSION_FORMAT_VERSION` at `dsh-v0.1.7-rc.1`

The frozen statement above records the frozen basis and is correct there: `SESSION_FORMAT_VERSION = 2` at `c389f96bf3` (`packages/core/session/src/types.ts:86`, re-observed 2026-09-24). The fact is superseded within the studied range `dsh-v0.1.5-rc.2 → dsh-v0.1.7-rc.1`: the constant is **3** at `dsh-v0.1.5-rc.2` (`types.ts:88` at that tag) and **4** at `dsh-v0.1.7-rc.1` (`types.ts:89` at the baseline tag) — a one-way upgrade: the version is "a single monotonic integer", historical generations are translated forward on read, and there is no version negotiation (constant doc comment `packages/core/session/src/types.ts:68-89`; header translation `:93`; both at the baseline tag). No crate rule in §5.3 changes: `session.event.event` stays opaque (§3.2), so the advance to V4 alters no wire or parsing obligation and the run path is unaffected exactly as stated above; the MUST NOT rules (no `assistant/chunk` claim, no embedded-stream parsing) apply to every generation. Evidence: plan `11-dsh-0.1.7-rc1-realignment` Task 1 report (`.mstar/sdd/11-dsh-0.1.7-rc1-realignment/task-1-report.md`) plus pinned-ref re-observation of both anchors on 2026-09-24 (`git show <tag>:packages/core/session/src/types.ts` → `SESSION_FORMAT_VERSION` 3 at `dsh-v0.1.5-rc.2`, 4 at `dsh-v0.1.7-rc.1`); §5.1.1 records the matching "reserved for Session V4 persistence" note. (Amendment 2026-09-24, plan `11-dsh-0.1.7-rc1-realignment`, QC finding F-002.)

---

## 6. Field-level parity table (Python baseline)
Expand Down Expand Up @@ -269,4 +283,14 @@ A change to this contract MUST be verifiable by:

Rust-side paths (`src/protocol.rs`, `src/api.rs`, `src/client/`) are cited at the v0.1 tree as the current state this contract amends.

Re-verification rule: if upstream advances past `c389f96bf3`, re-run the greps behind §2, §3, §5.1, and §6.3 against the new ref before restating any line here. A moved line number is not a contract change; a changed rule is.
Drift-amendment citations (upstream @ `dsh-v0.1.7-rc.1` = `46a7f68b0922371ce7144b668b90e377d8e799f4`, recorded 2026-09-24 — §5.1.1 and §5.3.1; every other row above stays frozen at `c389f96bf3`):

| Path | Lines used |
|---|---|
| `packages/llm/llm/src/types.ts` | 79-89, 115-124, 127-130, 132-137, 138-146 |
| `packages/llm/llm/src/message.ts` | 287-305 |
| `packages/core/session/src/types.ts` | 68-89, 93 |
| `packages/sdk/client/tests/sdk-client.spec.ts` | 141 |
| `packages/llm/llm-deepseek/tests/serialize.spec.ts` | 453 |

Re-verification rule: if upstream advances past `c389f96bf3`, re-run the greps behind §2, §3, §5.1, §5.3, and §6.3 against the new ref before restating any line here. A moved line number is not a contract change; a changed rule is. §5.1 was re-verified 2026-09-24 at `dsh-v0.1.7-rc.1`; the §5.1 greps are green at that ref with these expected outputs: `ContentBlockMap` at `packages/llm/llm/src/types.ts:138-146` with the seven members of §5.1.1; no `tool-result` map member and no `ToolResultBlock` anywhere under `packages/llm`; tool results as message-level `role: 'tool'` at `packages/llm/llm/src/message.ts:287-305`; `ImageBlock.offloaded?: true` at `packages/llm/llm/src/types.ts:88`. §5.3's version fact was re-verified the same day at the same ref (supersession §5.3.1); its grep — `git show <ref>:packages/core/session/src/types.ts | grep -n "SESSION_FORMAT_VERSION"` — returns the expected superseded value: `SESSION_FORMAT_VERSION = 4` at `packages/core/session/src/types.ts:89` (2 at the frozen basis `c389f96bf3` `:86`; 3 at `dsh-v0.1.5-rc.2` `:88`). The remaining §2/§3/§6.3 inventory rows re-ran green at the same ref (moved line numbers only, no changed rules; the protocol package is byte-identical across `dsh-v0.1.5-rc.2` → `dsh-v0.1.7-rc.1`).
4 changes: 2 additions & 2 deletions README.i18n.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
# editing either side, bring the other along and re-record with:
# git hash-object README.md
# git hash-object README.zh.md
README.md: 6619ccec6f8154765e6a68a02d8f48fb6784e6bf
README.zh.md: 71493d1f2a04b10f9608f5b91c68f8ec6fd49aab
README.md: 376d7e3618152fe5daf5e0bb29a898458028893b
README.zh.md: 5ce7915c8aac1e6f767eb185ad156a0b77edd8f4
29 changes: 18 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,12 @@ Build the runtime executable from source with the
`build-exe-for-python-sdk` script from the
[official repository](https://github.com/deepseek-ai/deepseek-harness), then
point `DSH_RUNTIME_BIN` (or `Config::dsh_bin`) at the built executable.
Building from source is the only route that reproduces the exact contract
basis this crate was verified against (`git checkout c389f96bf3` in the
official repository before building), and it is the route for any platform
that has no published wheel.
Building from source is the only route that reproduces the upstream ref
the contract specs are frozen at (`git checkout c389f96bf3` in the
official repository before building) — the specs' citation basis, distinct
from the runtime version under test, which CI owns via its own pin in
`.github/workflows/ci.yml`. Building from source is also the route for any
platform that has no published wheel.

### How the SDK resolves the runtime

Expand Down Expand Up @@ -287,17 +289,22 @@ parity); only the `session/prompt` request is bounded by
`Config::request_timeout`. Callers needing a bound wrap the call in
`tokio::time::timeout` — this bounds the local wait, not the runtime's turn.

### Session format v2
### Session format

The runtime's `session.event` vocabulary is **session format v2** (no wire
change). The crate documents the v2 vocabulary and keeps event payloads
untyped, so the run path is unaffected:
The crate keeps `session.event` payloads **untyped**, so upstream
session-format advances (v2 … v4) pass through without touching the run
path. The v2 transition is kept below as a historical record of one such
advance:

- `assistant/message` now carries an embedded
`stream: AssistantStreamRecord[]`;
- `assistant/message` carries an embedded `stream: AssistantStreamRecord[]`;
- `assistant/attempt` was added;
- `assistant/chunk` was removed — the crate does not claim `assistant/chunk`
support and does not parse the embedded v2 stream (non-goal).
support and does not parse the embedded stream (non-goal).

Test-locking is scoped to exactly two v4 shapes: the flattened
`tool/result` event (a flat `role: "tool"` message) and the
`developer/message` passthrough — both locked as verbatim passthrough in
the crate's run-semantics suite.

### Content block vocabulary

Expand Down
23 changes: 15 additions & 8 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,10 @@ x64、Linux arm64、macOS arm64、macOS x64、Windows x64**(Windows 使用
用[官方仓库](https://github.com/deepseek-ai/deepseek-harness)中的
`build-exe-for-python-sdk` 脚本构建运行时可执行文件,然后把
`DSH_RUNTIME_BIN`(或 `Config::dsh_bin`)指向构建产物。从源码构建是唯一
能精确复现本 crate 验证所依据的契约基线的途径(构建前先在官方仓库中
`git checkout c389f96bf3`),也是任何没有已发布 wheel 的平台的途径。
能精确复现契约规格冻结所依据的上游 ref 的途径(构建前先在官方仓库中
`git checkout c389f96bf3`)——这是规格的引用基线,不同于被测运行时版本;
后者由 CI 以其自身固定的版本持有(见 `.github/workflows/ci.yml`)。从源
码构建同时也是任何没有已发布 wheel 的平台的途径。

### SDK 如何解析运行时

Expand Down Expand Up @@ -259,15 +261,20 @@ pub async fn run(
请用 `tokio::time::timeout` 包住该调用——这只约束本地等待,不约束运行时
侧的执行。

### 会话格式 v2
### 会话格式

运行时的 `session.event` 词汇是**会话格式 v2**(非线上变更)。crate 记录
v2 词汇并保持事件载荷无类型,因此 run 路径不受影响:
crate 保持 `session.event` 载荷**无类型**,因此上游会话格式的演进
(v2 … v4)原样透传,不触及 run 路径。v2 变迁作为其中一次演进的历史
记录保留在下方:

- `assistant/message` 现在携带内嵌的 `stream: AssistantStreamRecord[]`;
- `assistant/message` 携带内嵌的 `stream: AssistantStreamRecord[]`;
- 新增 `assistant/attempt`;
- 移除 `assistant/chunk`——crate 不声称支持 `assistant/chunk`,本迭代也
不解析内嵌的 v2 stream(非目标)。
- 移除 `assistant/chunk`——crate 不声称支持 `assistant/chunk`,也不解析
内嵌 stream(非目标)。

测试锁定精确限定于两个 v4 形态:扁平化的 `tool/result` 事件(扁平的
`role: "tool"` 消息)与 `developer/message` 透传——二者均由 crate 的
run-semantics 测试套件作为逐字透传锁定。

### 内容块词汇

Expand Down
Loading
Loading