From 5f1a390631fd7d3499ccebe0c7fb27db3e08a25a Mon Sep 17 00:00:00 2001 From: Tang Bohao Date: Thu, 24 Sep 2026 00:50:12 +0800 Subject: [PATCH 1/7] docs(spec): record the 0.1.7-rc.1 content-block drift and the crate divergence --- .mstar/specs/dsh-sdk-wire-parity-surface.md | 23 +++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.mstar/specs/dsh-sdk-wire-parity-surface.md b/.mstar/specs/dsh-sdk-wire-parity-surface.md index 6782edc..23a3380 100644 --- a/.mstar/specs/dsh-sdk-wire-parity-surface.md +++ b/.mstar/specs/dsh-sdk-wire-parity-surface.md @@ -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 | |---|---|---| @@ -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. @@ -269,4 +279,13 @@ 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 only; 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/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, 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`. 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`). From 4dbb95503967c1330fe343cddb91d2842cc3c79f Mon Sep 17 00:00:00 2001 From: Tang Bohao Date: Thu, 24 Sep 2026 00:57:33 +0800 Subject: [PATCH 2/7] ci(runtime): pin the keyless handshake to @deepseek-ai/dsh@0.1.7-rc.1 --- .github/workflows/ci.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a39bf52..57e4fc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 @@ -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" From f91685c6277281fa46ee4732980078a125693028 Mon Sep 17 00:00:00 2001 From: Tang Bohao Date: Thu, 24 Sep 2026 01:04:36 +0800 Subject: [PATCH 3/7] test(wire): lock V4 tool-result and developer-message passthrough --- tests/run_semantics.rs | 211 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) diff --git a/tests/run_semantics.rs b/tests/run_semantics.rs index 6e09a00..a5c3e05 100644 --- a/tests/run_semantics.rs +++ b/tests/run_semantics.rs @@ -980,3 +980,214 @@ fn callback_panic_propagates_out_of_run() { "the propagated panic must be the callback's, not a harness panic: {message}" ); } + +#[tokio::test] +async fn v4_flat_tool_result_event_passes_through_verbatim() { + // dsh-v0.1.7-rc.1 reshaped the session format (V4): a tool result is no + // longer a `tool-result` content block — it is a `tool/result` event + // whose `data.message` is a FLAT `role:'tool'` message + // (`createToolResultMessage`, plan 11 task 1 WS-13/WS-15; no nested + // block wrapper, no `role:'user'`). The crate treats `session.event` + // payloads as opaque, so this locks that the new shape rides through + // parsing, `Session::run` collection, and the notification callback + // without a SdkProtocol error, and that the assistant/message + + // turn/end anchors still own `final_response`/`finish_reason`. + let tool_result = json!({ + "type": "tool/result", + "data": {"message": { + "id": "tool-msg-1", + "role": "tool", + "source": {"kind": "tool", "callId": "call-1"}, + "toolCallId": "call-1", + "content": [{"type": "text", "text": "tool output"}], + "isError": false, + }} + }); + let mut script = run_prefix("msg-v4-tool"); + script.extend([ + emit("session.event", root_event(receipt_event("msg-v4-tool"))), + emit("session.event", root_event(tool_result.clone())), + emit( + "session.event", + root_event(assistant_event( + json!([{"type": "text", "text": "after the tool ran"}]), + )), + ), + emit("session.event", root_event(turn_end("completed"))), + emit("session.status", idle(ROOT_SESSION)), + exit(0), + ]); + + let observed: Arc>> = Arc::new(Mutex::new(Vec::new())); + let mut h = harness(&script).await; + let session = h.start_session(Some(ROOT_SESSION.to_string())); + let observed_for_callback = observed.clone(); + let result = session + .run( + Input::Text("hello".to_string()), + Some(&move |notification: &Notification| { + observed_for_callback + .lock() + .expect("callback lock") + .push(notification.clone()); + }), + ) + .await + .expect("the V4 tool/result event must not fail the run"); + h.close().await.expect("clean close"); + + // The run completed to root idle: every interval event landed in + // transport order, the tool/result payload verbatim as emitted. + let types: Vec<&str> = result + .events + .iter() + .filter_map(|e| e.get("type").and_then(Value::as_str)) + .collect(); + assert_eq!( + types, + [ + "agent/inbox/spliced", + "tool/result", + "assistant/message", + "turn/end" + ] + ); + assert_eq!( + result.events[1], tool_result, + "the flattened V4 tool/result event must land verbatim in events" + ); + + // The callback observed the whole tree, tool/result included. + let observed = observed.lock().expect("observed lock"); + let methods: Vec<&str> = observed.iter().map(|n| n.method.as_str()).collect(); + assert_eq!( + methods, + [ + "session.event", // receipt + "session.event", // tool/result + "session.event", // assistant/message + "session.event", // turn/end + "session.status", // root idle + ] + ); + assert_eq!( + observed[1].payload.get("event"), + Some(&tool_result), + "the callback must observe the tool/result event verbatim" + ); + assert_eq!( + observed[4].payload.get("sessionId").and_then(Value::as_str), + Some(ROOT_SESSION) + ); + assert_eq!( + observed[4].payload.get("status").and_then(Value::as_str), + Some("idle") + ); + + // The derivation anchors are unaffected: the assistant/message + turn/end + // still own final_response/finish_reason despite the new event type. + assert_eq!(result.final_response, "after the tool ran"); + assert_eq!(result.finish_reason.as_deref(), Some("completed")); +} + +#[tokio::test] +async fn v4_developer_message_event_passes_through_verbatim() { + // The other V4 addition: `developer/message` — a new event type whose + // `data.message.role` is `"developer"` (plan 11 task 1; the crate is + // shape-agnostic, any object works). Same pass-through lock as the + // tool/result sibling: no SdkProtocol, verbatim in `events` and in the + // callback, and the assistant/message + turn/end anchors still win. + let developer_message = json!({ + "type": "developer/message", + "data": {"message": { + "id": "dev-msg-1", + "role": "developer", + "content": [{"type": "text", "text": "developer guidance"}], + }} + }); + let mut script = run_prefix("msg-v4-dev"); + script.extend([ + emit("session.event", root_event(receipt_event("msg-v4-dev"))), + emit("session.event", root_event(developer_message.clone())), + emit( + "session.event", + root_event(assistant_event( + json!([{"type": "text", "text": "after the developer note"}]), + )), + ), + emit("session.event", root_event(turn_end("completed"))), + emit("session.status", idle(ROOT_SESSION)), + exit(0), + ]); + + let observed: Arc>> = Arc::new(Mutex::new(Vec::new())); + let mut h = harness(&script).await; + let session = h.start_session(Some(ROOT_SESSION.to_string())); + let observed_for_callback = observed.clone(); + let result = session + .run( + Input::Text("hello".to_string()), + Some(&move |notification: &Notification| { + observed_for_callback + .lock() + .expect("callback lock") + .push(notification.clone()); + }), + ) + .await + .expect("the V4 developer/message event must not fail the run"); + h.close().await.expect("clean close"); + + // The run completed to root idle: every interval event landed in + // transport order, the developer/message payload verbatim as emitted. + let types: Vec<&str> = result + .events + .iter() + .filter_map(|e| e.get("type").and_then(Value::as_str)) + .collect(); + assert_eq!( + types, + [ + "agent/inbox/spliced", + "developer/message", + "assistant/message", + "turn/end" + ] + ); + assert_eq!( + result.events[1], developer_message, + "the V4 developer/message event must land verbatim in events" + ); + + // The callback observed the whole tree, developer/message included. + let observed = observed.lock().expect("observed lock"); + let methods: Vec<&str> = observed.iter().map(|n| n.method.as_str()).collect(); + assert_eq!( + methods, + [ + "session.event", // receipt + "session.event", // developer/message + "session.event", // assistant/message + "session.event", // turn/end + "session.status", // root idle + ] + ); + assert_eq!( + observed[1].payload.get("event"), + Some(&developer_message), + "the callback must observe the developer/message event verbatim" + ); + assert_eq!( + observed[4].payload.get("sessionId").and_then(Value::as_str), + Some(ROOT_SESSION) + ); + assert_eq!( + observed[4].payload.get("status").and_then(Value::as_str), + Some("idle") + ); + + // The derivation anchors are unaffected: the assistant/message + turn/end + // still own final_response/finish_reason despite the new event type. + assert_eq!(result.final_response, "after the developer note"); + assert_eq!(result.finish_reason.as_deref(), Some("completed")); +} From b765606d57e979056a752e3bf600b03b31942c81 Mon Sep 17 00:00:00 2001 From: Tang Bohao Date: Thu, 24 Sep 2026 01:14:00 +0800 Subject: [PATCH 4/7] docs(readme): confirm runtime facts at the 0.1.7-rc1 baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README claim walk at the @deepseek-ai/dsh@0.1.7-rc.1 baseline: every protocol/verification claim in the bilingual pair verifies TRUE against the Task 1 re-verification evidence (serverInfo 0.0.1 hard-coded, no cancel RPC, no version negotiation, 5-target wheel matrix, Route A/B wording, ci.yml pointer) — zero stale facts, zero README diff, hashes in README.i18n.yaml unchanged. Ships the consumer-facing changelog fragment for the verified-runtime bump (CI keyless handshake now verifies @deepseek-ai/dsh@0.1.7-rc.1; fake-runtime suite locks flattened role:'tool' tool results and developer/message pass-through; no public API change) and updates the drift-realignment knowledge doc's CI-pin example to the new pin. --- .changes/unreleased/dsh-0.1.7-rc1-realignment.md | 6 ++++++ .../best-practices/dsh-upstream-drift-realignment.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changes/unreleased/dsh-0.1.7-rc1-realignment.md diff --git a/.changes/unreleased/dsh-0.1.7-rc1-realignment.md b/.changes/unreleased/dsh-0.1.7-rc1-realignment.md new file mode 100644 index 0000000..fa7aba7 --- /dev/null +++ b/.changes/unreleased/dsh-0.1.7-rc1-realignment.md @@ -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. diff --git a/.mstar/knowledge/best-practices/dsh-upstream-drift-realignment.md b/.mstar/knowledge/best-practices/dsh-upstream-drift-realignment.md index feda1df..7488d51 100644 --- a/.mstar/knowledge/best-practices/dsh-upstream-drift-realignment.md +++ b/.mstar/knowledge/best-practices/dsh-upstream-drift-realignment.md @@ -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`) installs one exact published version (`@deepseek-ai/dsh@0.1.7-rc.1`, 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 From b806d22b94b03ce7fa7aa96e2d3f62a4a511a4d8 Mon Sep 17 00:00:00 2001 From: Tang Bohao Date: Thu, 24 Sep 2026 01:24:48 +0800 Subject: [PATCH 5/7] docs(readme): make the session-format statement version-neutral --- README.i18n.yaml | 4 ++-- README.md | 17 ++++++++++------- README.zh.md | 15 +++++++++------ 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/README.i18n.yaml b/README.i18n.yaml index cd28cf1..d21812b 100644 --- a/README.i18n.yaml +++ b/README.i18n.yaml @@ -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: 936391b1d05bca6b6759e5bc4686560b0ff4749b +README.zh.md: 3c923f40c3cdc56f5c3801e041f7176e508b2d1a diff --git a/README.md b/README.md index 6619cce..936391b 100644 --- a/README.md +++ b/README.md @@ -287,17 +287,20 @@ 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). + +The v4 event shapes are test-locked as verbatim passthrough in the crate's +run-semantics suite. ### Content block vocabulary diff --git a/README.zh.md b/README.zh.md index 71493d1..3c923f4 100644 --- a/README.zh.md +++ b/README.zh.md @@ -259,15 +259,18 @@ 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 事件形态的逐字透传已由 crate 的 run-semantics 测试套件锁定。 ### 内容块词汇 From 84ac9f6e66515da543c58edad11ddf625b4cc05d Mon Sep 17 00:00:00 2001 From: Tang Bohao Date: Thu, 24 Sep 2026 01:44:24 +0800 Subject: [PATCH 6/7] docs: address QC suggestions F-001, F-003, S-1 (pin pointer, scoped test-lock claim, Route C basis wording) --- .../dsh-upstream-drift-realignment.md | 2 +- README.i18n.yaml | 4 ++-- README.md | 16 ++++++++++------ README.zh.md | 10 +++++++--- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.mstar/knowledge/best-practices/dsh-upstream-drift-realignment.md b/.mstar/knowledge/best-practices/dsh-upstream-drift-realignment.md index 7488d51..fb6fd45 100644 --- a/.mstar/knowledge/best-practices/dsh-upstream-drift-realignment.md +++ b/.mstar/knowledge/best-practices/dsh-upstream-drift-realignment.md @@ -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.7-rc.1`, 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 diff --git a/README.i18n.yaml b/README.i18n.yaml index d21812b..ff3d5ff 100644 --- a/README.i18n.yaml +++ b/README.i18n.yaml @@ -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: 936391b1d05bca6b6759e5bc4686560b0ff4749b -README.zh.md: 3c923f40c3cdc56f5c3801e041f7176e508b2d1a +README.md: 376d7e3618152fe5daf5e0bb29a898458028893b +README.zh.md: 5ce7915c8aac1e6f767eb185ad156a0b77edd8f4 diff --git a/README.md b/README.md index 936391b..376d7e3 100644 --- a/README.md +++ b/README.md @@ -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 @@ -299,8 +301,10 @@ advance: - `assistant/chunk` was removed — the crate does not claim `assistant/chunk` support and does not parse the embedded stream (non-goal). -The v4 event shapes are test-locked as verbatim passthrough in the crate's -run-semantics suite. +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 diff --git a/README.zh.md b/README.zh.md index 3c923f4..5ce7915 100644 --- a/README.zh.md +++ b/README.zh.md @@ -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 如何解析运行时 @@ -270,7 +272,9 @@ crate 保持 `session.event` 载荷**无类型**,因此上游会话格式的 - 移除 `assistant/chunk`——crate 不声称支持 `assistant/chunk`,也不解析 内嵌 stream(非目标)。 -v4 事件形态的逐字透传已由 crate 的 run-semantics 测试套件锁定。 +测试锁定精确限定于两个 v4 形态:扁平化的 `tool/result` 事件(扁平的 +`role: "tool"` 消息)与 `developer/message` 透传——二者均由 crate 的 +run-semantics 测试套件作为逐字透传锁定。 ### 内容块词汇 From 35a715f49ae010b397d2bb2d8a83a90fe0296de2 Mon Sep 17 00:00:00 2001 From: Tang Bohao Date: Thu, 24 Sep 2026 01:44:35 +0800 Subject: [PATCH 7/7] docs(spec): record the session-format V4 supersession and re-verification row (F-002) --- .mstar/specs/dsh-sdk-wire-parity-surface.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.mstar/specs/dsh-sdk-wire-parity-surface.md b/.mstar/specs/dsh-sdk-wire-parity-surface.md index 23a3380..0d908a3 100644 --- a/.mstar/specs/dsh-sdk-wire-parity-surface.md +++ b/.mstar/specs/dsh-sdk-wire-parity-surface.md @@ -113,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 :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) @@ -279,13 +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. -Drift-amendment citations (upstream @ `dsh-v0.1.7-rc.1` = `46a7f68b0922371ce7144b668b90e377d8e799f4`, recorded 2026-09-24 — §5.1.1 only; every other row above stays frozen at `c389f96bf3`): +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, 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`. 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`). +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 :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`).