@deepseek-ai/dsh-session 0.1.2-alpha.5 → 0.1.3-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +6 -7
- package/README.zh.md +6 -7
- package/lib/index.js +73 -376
- package/lib/invariant.js +2 -2
- package/lib/types/index.d.ts +23 -20
- package/lib/types/index.js +73 -63
- package/lib/types/invariant.js +2 -2
- package/lib/types/known-event-types.js +3 -1
- package/lib/types/surface.d.ts +1 -1
- package/lib/types/surface.js +8 -5
- package/lib/types/types.d.ts +67 -56
- package/lib/types/types.js +9 -10
- package/package.json +9 -13
- package/lib/types/chunk-rows.d.ts +0 -106
- package/lib/types/chunk-rows.js +0 -328
package/lib/types/chunk-rows.js
DELETED
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lossless row packing for `assistant/chunk` delta runs. Providers stream
|
|
3
|
-
* token-sized deltas, so a log stores hundreds of near-identical event lines
|
|
4
|
-
* whose JSON envelopes dwarf their payloads (~56× measured on a real DeepSeek
|
|
5
|
-
* session). This module packs each run of consecutive same-block delta chunks
|
|
6
|
-
* into ONE storage row — `text-chunks`, `reasoning-chunks`, or
|
|
7
|
-
* `tool-call-chunks` — and expands rows back to the exact original events.
|
|
8
|
-
*
|
|
9
|
-
* Packed rows are an encoding vocabulary, NOT session events: they never enter
|
|
10
|
-
* `Session.snapshotEvents()`, have no `SessionEventMap` entry, and use bare (slash-less)
|
|
11
|
-
* type tags so a reader cannot confuse them with the event taxonomy
|
|
12
|
-
* (precedent: the JSONL header line's `session` tag). Persistence and bounded
|
|
13
|
-
* history transport both use the codec. The encoder whitelists exact shapes —
|
|
14
|
-
* anything it does not fully recognize stays verbatim, so unknown fields or
|
|
15
|
-
* future chunk variants lose compression, never data. The decoder validates
|
|
16
|
-
* before expanding and fails loud on a malformed row-tagged value instead of
|
|
17
|
-
* silently dropping a whole run.
|
|
18
|
-
*
|
|
19
|
-
* @module @deepseek-ai/dsh-session/chunk-rows
|
|
20
|
-
*/
|
|
21
|
-
import { brandString } from '@deepseek-ai/dsh-brand';
|
|
22
|
-
import { SessionSeq } from "./types.js";
|
|
23
|
-
/**
|
|
24
|
-
* Test whether an encoded record is a packed chunk row rather than a Session event.
|
|
25
|
-
* @param record - one persistence or bounded-history encoding record.
|
|
26
|
-
* @returns Whether the record is a packed chunk row.
|
|
27
|
-
*/
|
|
28
|
-
export function isChunkRow(record) {
|
|
29
|
-
return record.type === 'text-chunks'
|
|
30
|
-
|| record.type === 'reasoning-chunks'
|
|
31
|
-
|| record.type === 'tool-call-chunks';
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Number of logical Session events represented by one packed row.
|
|
35
|
-
* @param row - validated or encoder-produced packed row.
|
|
36
|
-
* @returns Count of consecutive chunk events in the row.
|
|
37
|
-
*/
|
|
38
|
-
export function chunkRowLength(row) {
|
|
39
|
-
return row.type === 'tool-call-chunks' ? row.data.args.length : row.data.texts.length;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Minimum members before a run packs. Below it a row's envelope rivals the
|
|
43
|
-
* event lines it replaces. A format constant, not a tunable: both layouts
|
|
44
|
-
* decode identically, so changing it never invalidates stored logs.
|
|
45
|
-
*/
|
|
46
|
-
const MIN_RUN = 3;
|
|
47
|
-
function isRecord(value) {
|
|
48
|
-
return typeof value === 'object' && value !== null;
|
|
49
|
-
}
|
|
50
|
-
/** Exact-key check: `value` has every key in `keys` and nothing else. */
|
|
51
|
-
function hasExactKeys(value, keys) {
|
|
52
|
-
return Object.keys(value).length === keys.length && keys.every(k => Object.hasOwn(value, k));
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Classify an event for packing: its delta kind when the ENTIRE shape
|
|
56
|
-
* (envelope, data, chunk — exact keys, primitive types, integer seq/time) is
|
|
57
|
-
* whitelisted, else `undefined` (store verbatim). Inputs come from live typed
|
|
58
|
-
* appends AND parsed fixture files, so the checks are structural, not
|
|
59
|
-
* type-trusted. Integer times keep gap encoding exact: a fractional time would
|
|
60
|
-
* reconstruct through float subtraction/addition, which need not round-trip.
|
|
61
|
-
*/
|
|
62
|
-
function classify(event) {
|
|
63
|
-
if (event.type !== 'assistant/chunk')
|
|
64
|
-
return undefined;
|
|
65
|
-
if (!hasExactKeys(event, ['type', 'seq', 'time', 'data']))
|
|
66
|
-
return undefined;
|
|
67
|
-
if (!Number.isSafeInteger(event.seq) || event.seq < 0 || Object.is(event.seq, -0)
|
|
68
|
-
|| !Number.isSafeInteger(event.time))
|
|
69
|
-
return undefined;
|
|
70
|
-
const data = event.data;
|
|
71
|
-
if (!isRecord(data) || !hasExactKeys(data, ['turn', 'step', 'chunk']))
|
|
72
|
-
return undefined;
|
|
73
|
-
if (typeof data.turn !== 'number' || typeof data.step !== 'number')
|
|
74
|
-
return undefined;
|
|
75
|
-
const chunk = data.chunk;
|
|
76
|
-
if (!isRecord(chunk) || typeof chunk.index !== 'number')
|
|
77
|
-
return undefined;
|
|
78
|
-
switch (chunk.type) {
|
|
79
|
-
case 'text-delta':
|
|
80
|
-
case 'reasoning-delta':
|
|
81
|
-
return hasExactKeys(chunk, ['type', 'index', 'text']) && typeof chunk.text === 'string'
|
|
82
|
-
? chunk.type
|
|
83
|
-
: undefined;
|
|
84
|
-
case 'tool-call-delta': {
|
|
85
|
-
const shapeOk = hasExactKeys(chunk, ['type', 'index', 'id', 'argumentsDelta'])
|
|
86
|
-
|| (hasExactKeys(chunk, ['type', 'index', 'id', 'name', 'argumentsDelta']) && typeof chunk.name === 'string');
|
|
87
|
-
return shapeOk && typeof chunk.id === 'string' && typeof chunk.argumentsDelta === 'string'
|
|
88
|
-
? chunk.type
|
|
89
|
-
: undefined;
|
|
90
|
-
}
|
|
91
|
-
// Whitelist fall-through over parsed data: block-start/end, usage, finish,
|
|
92
|
-
// and any future chunk variant stay one event per line.
|
|
93
|
-
default:
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
/** The tool-call fields of a whitelisted delta chunk (only after {@link classify} returned `'tool-call-delta'`). */
|
|
98
|
-
function toolCallOf(event) {
|
|
99
|
-
return event.data.chunk;
|
|
100
|
-
}
|
|
101
|
-
/** The block index of a whitelisted delta chunk (not every {@link StreamChunk} variant carries one). */
|
|
102
|
-
function indexOf(event) {
|
|
103
|
-
return event.data.chunk.index;
|
|
104
|
-
}
|
|
105
|
-
/** Whether `next` extends a run ending in `prev` (same kind already checked by the caller). */
|
|
106
|
-
function continues(prev, next, kind) {
|
|
107
|
-
if (next.seq !== prev.seq + 1)
|
|
108
|
-
return false;
|
|
109
|
-
// Two safe-integer times can sit further apart than a double subtracts
|
|
110
|
-
// exactly (2^53-1 and its negation differ by ~2^54); a rounded gap would
|
|
111
|
-
// decode to a different timestamp. The check is exact in both directions: a
|
|
112
|
-
// true gap within safe range subtracts without rounding and passes, while a
|
|
113
|
-
// true gap beyond it rounds to a value that is itself beyond and fails.
|
|
114
|
-
if (!Number.isSafeInteger(next.time - prev.time))
|
|
115
|
-
return false;
|
|
116
|
-
if (next.data.turn !== prev.data.turn || next.data.step !== prev.data.step)
|
|
117
|
-
return false;
|
|
118
|
-
if (indexOf(next) !== indexOf(prev))
|
|
119
|
-
return false;
|
|
120
|
-
if (kind !== 'tool-call-delta')
|
|
121
|
-
return true;
|
|
122
|
-
const a = toolCallOf(prev);
|
|
123
|
-
const b = toolCallOf(next);
|
|
124
|
-
// `name` must match in presence AND value — a mixed run is not representable.
|
|
125
|
-
return a.id === b.id && Object.hasOwn(a, 'name') === Object.hasOwn(b, 'name') && a.name === b.name;
|
|
126
|
-
}
|
|
127
|
-
/** Build the row for a completed run (`run.length >= MIN_RUN`, uniform per {@link continues}). */
|
|
128
|
-
function buildRow(kind, run) {
|
|
129
|
-
const first = run[0];
|
|
130
|
-
const base = {
|
|
131
|
-
turn: first.data.turn,
|
|
132
|
-
step: first.data.step,
|
|
133
|
-
index: indexOf(first),
|
|
134
|
-
dt: run.slice(1).map((event, i) => event.time - run[i].time),
|
|
135
|
-
};
|
|
136
|
-
const envelope = { seq0: first.seq, time0: first.time };
|
|
137
|
-
if (kind === 'tool-call-delta') {
|
|
138
|
-
const call = toolCallOf(first);
|
|
139
|
-
return {
|
|
140
|
-
type: 'tool-call-chunks',
|
|
141
|
-
...envelope,
|
|
142
|
-
data: {
|
|
143
|
-
...base,
|
|
144
|
-
id: brandString(call.id),
|
|
145
|
-
...Object.hasOwn(call, 'name') ? { name: call.name } : {},
|
|
146
|
-
args: run.map(event => event.data.chunk.argumentsDelta),
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
const data = { ...base, texts: run.map(event => event.data.chunk.text) };
|
|
151
|
-
return kind === 'text-delta'
|
|
152
|
-
? { type: 'text-chunks', ...envelope, data }
|
|
153
|
-
: { type: 'reasoning-chunks', ...envelope, data };
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Pack an event batch for storage: each run of at least {@link MIN_RUN}
|
|
157
|
-
* consecutive whitelisted same-kind, same-block delta chunk events becomes one
|
|
158
|
-
* {@link ChunkRow}; every other event passes through verbatim, in order.
|
|
159
|
-
* Pure and stateless — safe over any array, including a batch whose runs were
|
|
160
|
-
* split by flush boundaries (the split runs simply pack per batch).
|
|
161
|
-
*
|
|
162
|
-
* @param events - the batch to encode, in log order.
|
|
163
|
-
* @returns the storage records to write, one JSONL line each.
|
|
164
|
-
*/
|
|
165
|
-
export function packChunkRuns(events) {
|
|
166
|
-
const out = [];
|
|
167
|
-
let kind;
|
|
168
|
-
let run = [];
|
|
169
|
-
const flush = () => {
|
|
170
|
-
if (kind !== undefined && run.length >= MIN_RUN)
|
|
171
|
-
out.push(buildRow(kind, run));
|
|
172
|
-
else
|
|
173
|
-
out.push(...run);
|
|
174
|
-
kind = undefined;
|
|
175
|
-
run = [];
|
|
176
|
-
};
|
|
177
|
-
for (const event of events) {
|
|
178
|
-
const k = classify(event);
|
|
179
|
-
if (k === undefined) {
|
|
180
|
-
flush();
|
|
181
|
-
out.push(event);
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
const delta = event;
|
|
185
|
-
const last = run[run.length - 1];
|
|
186
|
-
if (k === kind && last !== undefined && continues(last, delta, k)) {
|
|
187
|
-
run.push(delta);
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
flush();
|
|
191
|
-
kind = k;
|
|
192
|
-
run = [delta];
|
|
193
|
-
}
|
|
194
|
-
flush();
|
|
195
|
-
return out;
|
|
196
|
-
}
|
|
197
|
-
/** Throw the uniform malformed-row diagnostic. */
|
|
198
|
-
function malformed(tag, why) {
|
|
199
|
-
throw new Error(`malformed ${tag} storage row: ${why}`);
|
|
200
|
-
}
|
|
201
|
-
/** Validate the shared run-data fields and the payload/dt arity; returns the member payload. */
|
|
202
|
-
function validateRunData(tag, data, payloadKey) {
|
|
203
|
-
if (typeof data.turn !== 'number' || typeof data.step !== 'number' || typeof data.index !== 'number') {
|
|
204
|
-
malformed(tag, 'turn/step/index must be numbers');
|
|
205
|
-
}
|
|
206
|
-
const payload = data[payloadKey];
|
|
207
|
-
if (!Array.isArray(payload) || payload.length === 0 || payload.some(entry => typeof entry !== 'string')) {
|
|
208
|
-
malformed(tag, `${payloadKey} must be a non-empty string array`);
|
|
209
|
-
}
|
|
210
|
-
const dt = data.dt;
|
|
211
|
-
if (!Array.isArray(dt) || dt.some(gap => !Number.isSafeInteger(gap))) {
|
|
212
|
-
malformed(tag, 'dt must be an array of safe integers');
|
|
213
|
-
}
|
|
214
|
-
if (dt.length !== payload.length - 1) {
|
|
215
|
-
malformed(tag, `dt length ${dt.length} does not match ${payload.length} members`);
|
|
216
|
-
}
|
|
217
|
-
return payload;
|
|
218
|
-
}
|
|
219
|
-
/** Validate a row-tagged parsed value's envelope and data, throwing on any malformation. */
|
|
220
|
-
function validateRow(value, tag) {
|
|
221
|
-
if (!hasExactKeys(value, ['type', 'seq0', 'time0', 'data'])) {
|
|
222
|
-
malformed(tag, 'envelope must be exactly {type, seq0, time0, data}');
|
|
223
|
-
}
|
|
224
|
-
if (!Number.isSafeInteger(value.seq0) || value.seq0 < 0 || Object.is(value.seq0, -0)) {
|
|
225
|
-
malformed(tag, 'seq0 must be a non-negative safe integer');
|
|
226
|
-
}
|
|
227
|
-
if (!Number.isSafeInteger(value.time0)) {
|
|
228
|
-
malformed(tag, 'time0 must be a safe integer');
|
|
229
|
-
}
|
|
230
|
-
const data = value.data;
|
|
231
|
-
if (!isRecord(data))
|
|
232
|
-
malformed(tag, 'data must be an object');
|
|
233
|
-
let payload;
|
|
234
|
-
if (tag === 'tool-call-chunks') {
|
|
235
|
-
const withName = hasExactKeys(data, ['turn', 'step', 'index', 'id', 'name', 'dt', 'args']);
|
|
236
|
-
if (!withName && !hasExactKeys(data, ['turn', 'step', 'index', 'id', 'dt', 'args'])) {
|
|
237
|
-
malformed(tag, 'data must be exactly {turn, step, index, id, name?, dt, args}');
|
|
238
|
-
}
|
|
239
|
-
if (typeof data.id !== 'string' || (withName && typeof data.name !== 'string')) {
|
|
240
|
-
malformed(tag, 'id (and name when present) must be strings');
|
|
241
|
-
}
|
|
242
|
-
payload = validateRunData(tag, data, 'args');
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
if (!hasExactKeys(data, ['turn', 'step', 'index', 'dt', 'texts'])) {
|
|
246
|
-
malformed(tag, 'data must be exactly {turn, step, index, dt, texts}');
|
|
247
|
-
}
|
|
248
|
-
payload = validateRunData(tag, data, 'texts');
|
|
249
|
-
}
|
|
250
|
-
// Reconstruction bounds. The encoder only packs runs whose member seqs and
|
|
251
|
-
// times are all safe integers, so a running value that leaves safe range is
|
|
252
|
-
// outside any encoder's image: float arithmetic would round it to a
|
|
253
|
-
// different number than exact arithmetic, a silent corruption. Within safe
|
|
254
|
-
// range every step is exact, so the first departure is always caught.
|
|
255
|
-
if (payload.length - 1 > Number.MAX_SAFE_INTEGER - value.seq0) {
|
|
256
|
-
malformed(tag, 'member seqs must stay safe integers');
|
|
257
|
-
}
|
|
258
|
-
let time = value.time0;
|
|
259
|
-
for (const gap of data.dt) {
|
|
260
|
-
time += gap;
|
|
261
|
-
if (!Number.isSafeInteger(time))
|
|
262
|
-
malformed(tag, 'member times must stay safe integers');
|
|
263
|
-
}
|
|
264
|
-
SessionSeq(value.seq0);
|
|
265
|
-
return value;
|
|
266
|
-
}
|
|
267
|
-
/** Expand a validated row back into its exact original events, in order. */
|
|
268
|
-
function expandRow(row) {
|
|
269
|
-
const members = row.type === 'tool-call-chunks' ? row.data.args : row.data.texts;
|
|
270
|
-
const events = [];
|
|
271
|
-
let time = row.time0;
|
|
272
|
-
for (let k = 0; k < members.length; k++) {
|
|
273
|
-
if (k > 0)
|
|
274
|
-
time += row.data.dt[k - 1];
|
|
275
|
-
let chunk;
|
|
276
|
-
switch (row.type) {
|
|
277
|
-
case 'text-chunks':
|
|
278
|
-
chunk = { type: 'text-delta', index: row.data.index, text: members[k] };
|
|
279
|
-
break;
|
|
280
|
-
case 'reasoning-chunks':
|
|
281
|
-
chunk = { type: 'reasoning-delta', index: row.data.index, text: members[k] };
|
|
282
|
-
break;
|
|
283
|
-
case 'tool-call-chunks':
|
|
284
|
-
chunk = {
|
|
285
|
-
type: 'tool-call-delta',
|
|
286
|
-
index: row.data.index,
|
|
287
|
-
id: row.data.id,
|
|
288
|
-
...Object.hasOwn(row.data, 'name') ? { name: row.data.name } : {},
|
|
289
|
-
argumentsDelta: members[k],
|
|
290
|
-
};
|
|
291
|
-
break;
|
|
292
|
-
/* v8 ignore next 4 -- validateRow only returns the three row tags */
|
|
293
|
-
default: {
|
|
294
|
-
const unreachable = row;
|
|
295
|
-
throw new Error(`chunk-rows received unsupported row ${String(unreachable)}`);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
events.push({
|
|
299
|
-
type: 'assistant/chunk',
|
|
300
|
-
seq: SessionSeq(row.seq0 + k),
|
|
301
|
-
time,
|
|
302
|
-
data: { turn: row.data.turn, step: row.data.step, chunk },
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
return events;
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Decode one parsed JSONL line value into the session event(s) it stores.
|
|
309
|
-
* Chunk-row-tagged values validate and expand (a malformed row throws — it is
|
|
310
|
-
* corrupt storage, and treating it as an event would silently drop a whole
|
|
311
|
-
* run); every other value passes through as a single event after admitting a
|
|
312
|
-
* numeric `seq` through the Session-sequence constructor.
|
|
313
|
-
*
|
|
314
|
-
* @param value - one line's `JSON.parse` result.
|
|
315
|
-
* @returns the stored events, in log order.
|
|
316
|
-
*/
|
|
317
|
-
export function decodeStorageRecord(value) {
|
|
318
|
-
if (!isRecord(value))
|
|
319
|
-
return [value];
|
|
320
|
-
const tag = value.type;
|
|
321
|
-
if (tag !== 'text-chunks' && tag !== 'reasoning-chunks' && tag !== 'tool-call-chunks') {
|
|
322
|
-
if (typeof value.seq === 'number')
|
|
323
|
-
SessionSeq(value.seq);
|
|
324
|
-
return [value];
|
|
325
|
-
}
|
|
326
|
-
return expandRow(validateRow(value, tag));
|
|
327
|
-
}
|
|
328
|
-
//# sourceMappingURL=chunk-rows.js.map
|