@deepseek-ai/dsh-llm 0.1.0-rc.6 → 0.1.0-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/llm/llm/README.md
5
- README.md: 2cae9a05a58295b06d25382729a3304dbdc9a6fa
6
- README.zh.md: 110cc128f1c19bef1741ae59d8106139c4a72649
5
+ README.md: fb6bd84240b41dd730d45b3eb34c35827dc4c991
6
+ README.zh.md: 5c22767a7c654972cbf614505382fa4755d7d318
package/README.md CHANGED
@@ -53,7 +53,7 @@ Exact-model metadata is a separate correctness query, not a catalog decoration o
53
53
 
54
54
  Message content is an array of typed blocks: `text`, `reasoning`, `tool-call`, `tool-result`. The union is derived from the merge-extensible `ContentBlockMap`, so plugins can add block types via declaration merging. Assistant messages use a model source carrying the provider and model that produced them plus optional adapter-private replay state. Before dispatch, `LlmRuntime` retains that state only when the historical provider route and target provider route are currently owned by the exact same adapter instance; the adapter then decides whether it can restore or convert the state across models/providers. The core block set is limited to blocks every shipping path honors — multimodal content (images, audio, …) has no core block type; a feature that needs one adds it via the map together with the adapter/UI/compaction support that honors it.
55
55
 
56
- Streaming is a raw chunk protocol (`block-start`, `text-delta`, `reasoning-delta`, `tool-call-delta`, `block-end`, `usage`, `finish`). Every adapter outcome reaches consumers as one terminal `finish`; operational failure uses its `error` or `aborted` reason rather than throwing across the stream API. `BlockAssembler` is the single shared implementation that assembles chunks into blocks/messages.
56
+ Streaming is a raw chunk protocol (`block-start`, `text-delta`, `reasoning-delta`, `tool-call-delta`, `block-end`, `usage`, `finish`). Every adapter outcome reaches consumers as one terminal `finish`; operational failure uses its `error` or `aborted` reason rather than throwing across the stream API. `BlockAssembler` is the single shared implementation that assembles chunks into blocks/messages. A successful `finish` may carry a `ReplayEnvelope` — opaque response-level replay metadata plus optional per-block entries aligned with the emitted block sequence. Assembly makes one keep/drop decision for content and metadata together: a `max-tokens` finish drops tool calls that may have been truncated, and the envelope loses the entry at each dropped position, so stored metadata always describes stored content.
57
57
 
58
58
  ### Call configuration (`call-config.ts`)
59
59
 
package/README.zh.md CHANGED
@@ -53,7 +53,7 @@
53
53
 
54
54
  消息内容是类型化内容块数组:`text`、`reasoning`、`tool-call`、`tool-result`。联合从可合并扩展的 `ContentBlockMap` 派生,因此插件可以通过 declaration merging 添加块类型。assistant 消息使用模型来源,其中携带生成该消息的提供方和模型,以及可选的适配器私有回放状态。dispatch 前,`LlmRuntime` 只在历史提供方路由与目标提供方路由当前由完全相同的适配器实例拥有时才保留该状态;随后由适配器判定能否在模型/提供方间恢复或转换该状态。核心块集只包含每条已发布路径都支持的块。多模态内容(图像、音频等)没有核心块类型;需要它的功能会通过 map 添加,并一并添加相应的适配器/UI/压缩(compaction)支持。
55
55
 
56
- 流式输出是原始分片协议(`block-start`、`text-delta`、`reasoning-delta`、`tool-call-delta`、`block-end`、`usage`、`finish`)。每个适配器结果都以一个终止 `finish` 到达消费方;运行故障使用 `error` 或 `aborted` 作为结束原因,而不会跨流 API 抛出。`BlockAssembler` 是将分片组装为块/消息的唯一共享实现。
56
+ 流式输出是原始分片协议(`block-start`、`text-delta`、`reasoning-delta`、`tool-call-delta`、`block-end`、`usage`、`finish`)。每个适配器结果都以一个终止 `finish` 到达消费方;运行故障使用 `error` 或 `aborted` 作为结束原因,而不会跨流 API 抛出。`BlockAssembler` 是将分片组装为块/消息的唯一共享实现。成功的 `finish` 可以携带 `ReplayEnvelope`——不透明的响应级回放元数据,加上与发射块序列对齐的可选逐块条目。组装对内容与元数据只做一次保留/丢弃决定:`max-tokens` 结束会丢弃可能被截断的工具调用,数据在每个被丢弃的位置同步失去对应条目,因此存储的元数据始终描述存储的内容。
57
57
 
58
58
  ### 调用配置(`call-config.ts`)
59
59
 
package/lib/index.js CHANGED
@@ -675,7 +675,7 @@ var BlockAssembler = class {
675
675
  order = [];
676
676
  _usage;
677
677
  _finish;
678
- _replayState = void 0;
678
+ _replayState;
679
679
  /**
680
680
  * Feed one chunk into the assembly state.
681
681
  * @param chunk - the next raw chunk, in stream order.
@@ -763,14 +763,39 @@ var BlockAssembler = class {
763
763
  return partial;
764
764
  }
765
765
  /**
766
+ * The one shared keep/drop decision over all seen blocks: max-token
767
+ * truncation drops tool calls that cannot be executed safely. Emitted blocks
768
+ * and replay metadata both derive from this result, so they cannot disagree.
769
+ */
770
+ assembled() {
771
+ const all = this.order.map((index) => this.assemble(this.mustGet(index), index));
772
+ const kept = this.finish.kind === "max-tokens" ? all.map((block) => block.type !== "tool-call") : void 0;
773
+ const blocks = kept === void 0 ? all : all.filter((_, position) => kept[position]);
774
+ const envelope = this._replayState;
775
+ if (envelope?.blocks === void 0) return {
776
+ blocks,
777
+ replay: envelope
778
+ };
779
+ if (envelope.blocks.length !== all.length) return {
780
+ blocks,
781
+ replay: void 0
782
+ };
783
+ return {
784
+ blocks,
785
+ replay: kept === void 0 || blocks.length === all.length ? envelope : {
786
+ response: envelope.response,
787
+ blocks: envelope.blocks.filter((_, position) => kept[position])
788
+ }
789
+ };
790
+ }
791
+ /**
766
792
  * Assemble all blocks seen so far, in stream order.
767
793
  * @returns one block per seen index, except that max-token truncation drops
768
794
  * tool calls that cannot be executed safely; an open block assembles from
769
795
  * its accumulated deltas (an unknown block type never closed by `block-end` throws).
770
796
  */
771
797
  blocks() {
772
- const blocks = this.order.map((index) => this.assemble(this.mustGet(index), index));
773
- return this.finish.kind === "max-tokens" ? blocks.filter((block) => block.type !== "tool-call") : blocks;
798
+ return this.assembled().blocks;
774
799
  }
775
800
  /** Usage from the `usage` chunk; undefined until one arrives. */
776
801
  get usage() {
@@ -780,9 +805,13 @@ var BlockAssembler = class {
780
805
  get finish() {
781
806
  return this._finish ?? { kind: "stop" };
782
807
  }
783
- /** Adapter-private replay state from the terminal finish chunk, if any. */
808
+ /**
809
+ * Replay metadata from the terminal finish chunk, if any, with per-block
810
+ * entries pruned in step with {@link blocks}. Undefined when the envelope's
811
+ * entries do not align with the emitted blocks.
812
+ */
784
813
  get replayState() {
785
- return this._replayState;
814
+ return this.assembled().replay;
786
815
  }
787
816
  /**
788
817
  * The assembled assistant message.
@@ -6,7 +6,7 @@
6
6
  * @module @deepseek-ai/dsh-llm/assembler
7
7
  */
8
8
  import type { Message, MessageSource } from './message.ts';
9
- import type { ContentBlock, FinishReason, StreamChunk, TokenUsage } from './types.ts';
9
+ import type { ContentBlock, FinishReason, ReplayEnvelope, StreamChunk, TokenUsage } from './types.ts';
10
10
  /**
11
11
  * Incrementally assembles raw {@link StreamChunk}s into complete
12
12
  * {@link ContentBlock}s and a final assistant {@link Message}.
@@ -33,6 +33,12 @@ export declare class BlockAssembler {
33
33
  private assemble;
34
34
  /** Invariant accessor: every index in `order` has a partial. */
35
35
  private mustGet;
36
+ /**
37
+ * The one shared keep/drop decision over all seen blocks: max-token
38
+ * truncation drops tool calls that cannot be executed safely. Emitted blocks
39
+ * and replay metadata both derive from this result, so they cannot disagree.
40
+ */
41
+ private assembled;
36
42
  /**
37
43
  * Assemble all blocks seen so far, in stream order.
38
44
  * @returns one block per seen index, except that max-token truncation drops
@@ -44,8 +50,12 @@ export declare class BlockAssembler {
44
50
  get usage(): TokenUsage | undefined;
45
51
  /** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */
46
52
  get finish(): FinishReason;
47
- /** Adapter-private replay state from the terminal finish chunk, if any. */
48
- get replayState(): unknown;
53
+ /**
54
+ * Replay metadata from the terminal finish chunk, if any, with per-block
55
+ * entries pruned in step with {@link blocks}. Undefined when the envelope's
56
+ * entries do not align with the emitted blocks.
57
+ */
58
+ get replayState(): ReplayEnvelope | undefined;
49
59
  /**
50
60
  * The assembled assistant message.
51
61
  * @param source - producer attribution for the assembled message.
@@ -24,7 +24,7 @@ export class BlockAssembler {
24
24
  order = [];
25
25
  _usage;
26
26
  _finish;
27
- _replayState = undefined;
27
+ _replayState;
28
28
  /**
29
29
  * Feed one chunk into the assembly state.
30
30
  * @param chunk - the next raw chunk, in stream order.
@@ -112,6 +112,29 @@ export class BlockAssembler {
112
112
  throw new Error(`BlockAssembler invariant violated: no partial for index ${index}`);
113
113
  return partial;
114
114
  }
115
+ /**
116
+ * The one shared keep/drop decision over all seen blocks: max-token
117
+ * truncation drops tool calls that cannot be executed safely. Emitted blocks
118
+ * and replay metadata both derive from this result, so they cannot disagree.
119
+ */
120
+ assembled() {
121
+ const all = this.order.map(index => this.assemble(this.mustGet(index), index));
122
+ const kept = this.finish.kind === 'max-tokens'
123
+ ? all.map(block => block.type !== 'tool-call')
124
+ : undefined;
125
+ const blocks = kept === undefined ? all : all.filter((_, position) => kept[position]);
126
+ const envelope = this._replayState;
127
+ if (envelope?.blocks === undefined)
128
+ return { blocks, replay: envelope };
129
+ if (envelope.blocks.length !== all.length)
130
+ return { blocks, replay: undefined };
131
+ return {
132
+ blocks,
133
+ replay: kept === undefined || blocks.length === all.length
134
+ ? envelope
135
+ : { response: envelope.response, blocks: envelope.blocks.filter((_, position) => kept[position]) },
136
+ };
137
+ }
115
138
  /**
116
139
  * Assemble all blocks seen so far, in stream order.
117
140
  * @returns one block per seen index, except that max-token truncation drops
@@ -119,10 +142,7 @@ export class BlockAssembler {
119
142
  * its accumulated deltas (an unknown block type never closed by `block-end` throws).
120
143
  */
121
144
  blocks() {
122
- const blocks = this.order.map(index => this.assemble(this.mustGet(index), index));
123
- return this.finish.kind === 'max-tokens'
124
- ? blocks.filter(block => block.type !== 'tool-call')
125
- : blocks;
145
+ return this.assembled().blocks;
126
146
  }
127
147
  /** Usage from the `usage` chunk; undefined until one arrives. */
128
148
  get usage() {
@@ -132,9 +152,13 @@ export class BlockAssembler {
132
152
  get finish() {
133
153
  return this._finish ?? { kind: 'stop' };
134
154
  }
135
- /** Adapter-private replay state from the terminal finish chunk, if any. */
155
+ /**
156
+ * Replay metadata from the terminal finish chunk, if any, with per-block
157
+ * entries pruned in step with {@link blocks}. Undefined when the envelope's
158
+ * entries do not align with the emitted blocks.
159
+ */
136
160
  get replayState() {
137
- return this._replayState;
161
+ return this.assembled().replay;
138
162
  }
139
163
  /**
140
164
  * The assembled assistant message.
@@ -256,6 +256,26 @@ export interface LlmResolvedModelInfo extends LlmModelInfo {
256
256
  /** Adapter-owned selectable reasoning levels when exposed. */
257
257
  reasoning?: LlmModelReasoningInfo;
258
258
  }
259
+ /**
260
+ * Adapter-private lossless-JSON state for replaying a successful response,
261
+ * carried by a terminal `finish` chunk and stored on the assembled assistant
262
+ * message's model source. Both halves stay opaque to the harness; only the
263
+ * split is shared vocabulary, so assembly can keep stored metadata aligned
264
+ * with stored content without reading either half.
265
+ */
266
+ export interface ReplayEnvelope {
267
+ /** Response-level adapter-private metadata (ids, native stop reason). */
268
+ response: unknown;
269
+ /**
270
+ * Per-block adapter-private metadata, one entry per emitted block in
271
+ * first-seen stream order. When assembly drops a block it drops the entry at
272
+ * the same position; entries whose length does not match the emitted block
273
+ * count discard the whole envelope. An adapter whose metadata is independent
274
+ * of block structure omits this field and the envelope passes through
275
+ * assembly unchanged.
276
+ */
277
+ blocks?: readonly unknown[];
278
+ }
259
279
  /**
260
280
  * Raw streaming protocol emitted by adapters.
261
281
  * Block indexes correlate interleaved deltas, and `block-end` carries the
@@ -292,8 +312,8 @@ export type StreamChunk = {
292
312
  } | {
293
313
  type: 'finish';
294
314
  reason: FinishReason;
295
- /** Adapter-private lossless-JSON state for replaying a successful response. */
296
- replayState?: unknown;
315
+ /** Replay metadata for a successful response; see {@link ReplayEnvelope}. */
316
+ replayState?: ReplayEnvelope;
297
317
  };
298
318
  /**
299
319
  * JSON-schema description of a tool, as sent to the model.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-llm",
3
3
  "description": "Provider-neutral LLM service interface for the DeepSeek Harness",
4
- "version": "0.1.0-rc.6",
4
+ "version": "0.1.0-rc.7",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -45,20 +45,20 @@
45
45
  ],
46
46
  "license": "MIT",
47
47
  "peerDependencies": {
48
- "@deepseek-ai/dsh-attachment": "^0.1.0-rc.6",
49
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
50
- "@deepseek-ai/dsh-brand": "^0.1.0-rc.6",
51
- "@deepseek-ai/dsh-timeout": "^0.1.0-rc.6",
48
+ "@deepseek-ai/dsh-attachment": "^0.1.0-rc.7",
49
+ "@deepseek-ai/dsh-brand": "^0.1.0-rc.7",
50
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
51
+ "@deepseek-ai/dsh-timeout": "^0.1.0-rc.7",
52
52
  "@deepseek-ai/cordis": "^4.0.1"
53
53
  },
54
54
  "dependencies": {
55
55
  "@deepseek-ai/schemastery": "^3.18.1"
56
56
  },
57
57
  "devDependencies": {
58
- "@deepseek-ai/dsh-attachment": "^0.1.0-rc.6",
59
- "@deepseek-ai/dsh-brand": "^0.1.0-rc.6",
60
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
61
- "@deepseek-ai/cordis": "^4.0.1",
62
- "@deepseek-ai/dsh-timeout": "^0.1.0-rc.6"
58
+ "@deepseek-ai/dsh-attachment": "^0.1.0-rc.7",
59
+ "@deepseek-ai/dsh-brand": "^0.1.0-rc.7",
60
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
61
+ "@deepseek-ai/dsh-timeout": "^0.1.0-rc.7",
62
+ "@deepseek-ai/cordis": "^4.0.1"
63
63
  }
64
64
  }