@deepseek-ai/dsh-session 0.0.1-rc.1
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/LICENSE +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +143 -0
- package/README.zh.md +143 -0
- package/lib/index.js +1841 -0
- package/lib/invariant.js +168 -0
- package/lib/types/chunk-rows.d.ts +92 -0
- package/lib/types/chunk-rows.js +301 -0
- package/lib/types/index.d.ts +424 -0
- package/lib/types/index.js +1015 -0
- package/lib/types/invariant.d.ts +18 -0
- package/lib/types/invariant.js +199 -0
- package/lib/types/json.d.ts +36 -0
- package/lib/types/json.js +174 -0
- package/lib/types/preparation.d.ts +33 -0
- package/lib/types/preparation.js +37 -0
- package/lib/types/repair.d.ts +38 -0
- package/lib/types/repair.js +144 -0
- package/lib/types/request-header.d.ts +35 -0
- package/lib/types/request-header.js +65 -0
- package/lib/types/surface.d.ts +123 -0
- package/lib/types/surface.js +377 -0
- package/lib/types/types.d.ts +426 -0
- package/lib/types/types.js +18 -0
- package/package.json +60 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface layer on top of the session event log: an ordered view of events
|
|
3
|
+
* that produce LLM messages. The append-only log remains the source of truth.
|
|
4
|
+
*
|
|
5
|
+
* Browser-safe: web clients consume this subpath export, so it must stay free
|
|
6
|
+
* of `node:` imports (they break the vite bundle).
|
|
7
|
+
*
|
|
8
|
+
* @module @deepseek-ai/dsh-session/surface
|
|
9
|
+
*/
|
|
10
|
+
/** Runtime counterpart of the message-producing event union. */
|
|
11
|
+
const SURFACE_EVENT_TYPES = new Set([
|
|
12
|
+
'user/message',
|
|
13
|
+
'assistant/message',
|
|
14
|
+
'tool/result',
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* Whether an event type can join the model-visible surface.
|
|
18
|
+
* @param type - event type to test.
|
|
19
|
+
* @returns true for one of the three message-producing event types.
|
|
20
|
+
*/
|
|
21
|
+
export function isSurfaceEligibleType(type) {
|
|
22
|
+
return SURFACE_EVENT_TYPES.has(type);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Narrow an event to a surface-eligible event carrying its required marker.
|
|
26
|
+
* @param event - event to test.
|
|
27
|
+
* @returns true when both the type and marker identify a surface event.
|
|
28
|
+
*/
|
|
29
|
+
export function isSurfaceEvent(event) {
|
|
30
|
+
if (!SURFACE_EVENT_TYPES.has(event.type))
|
|
31
|
+
return false;
|
|
32
|
+
return event.surfaceOp !== undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Narrow an event to an append-origin surface event: one that entered the
|
|
36
|
+
* surface at its own log position and was never itself a replacement copy.
|
|
37
|
+
*
|
|
38
|
+
* The model-visible surface deliberately shadows replaced ranges, so it is the
|
|
39
|
+
* wrong source for a human transcript — a landed replacement would erase
|
|
40
|
+
* conversation the user already saw. Append-origin events are that transcript's
|
|
41
|
+
* durable source material; replacement copies stay model-only.
|
|
42
|
+
* @param event - event to test.
|
|
43
|
+
* @returns true when the event appended to the surface tail.
|
|
44
|
+
*/
|
|
45
|
+
export function isAppendSurfaceEvent(event) {
|
|
46
|
+
return isSurfaceEvent(event) && event.surfaceOp === 'append';
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Narrow an event to a surface replacement: a node that shadowed an existing
|
|
50
|
+
* surface range instead of appending to the tail. The counterpart of
|
|
51
|
+
* {@link isAppendSurfaceEvent} over the two {@link SurfaceOp} variants.
|
|
52
|
+
* @param event - event to test.
|
|
53
|
+
* @returns true when the event replaced a surface range.
|
|
54
|
+
*/
|
|
55
|
+
export function isReplacementSurfaceEvent(event) {
|
|
56
|
+
return isSurfaceEvent(event) && event.surfaceOp !== 'append';
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Project a single event into the LLM message it derives to, or null when it
|
|
60
|
+
* produces none — a non-surface event (chunk, boundary, log-only record) or an
|
|
61
|
+
* empty-content assistant/message (which exists only to host usage). This is
|
|
62
|
+
* THE per-node projection rule: `Session.deriveMessages` folds it over the
|
|
63
|
+
* live surface, external reconstructors and pure projections fold the same
|
|
64
|
+
* function over a log prefix's surface to rebuild the exact messages any
|
|
65
|
+
* request was built from. The returned message is the already frozen message
|
|
66
|
+
* nested in the event wrapper and shared by delivery, durable history, and
|
|
67
|
+
* model requests.
|
|
68
|
+
* @param event - the event to project.
|
|
69
|
+
* @returns the derived message, or null when the event produces none.
|
|
70
|
+
*/
|
|
71
|
+
export function deriveEventMessage(event) {
|
|
72
|
+
// Intentionally non-exhaustive: only message-producing events derive
|
|
73
|
+
// history; turn/step boundaries, chunks, usage, and errors are trace/replay
|
|
74
|
+
// data.
|
|
75
|
+
switch (event.type) {
|
|
76
|
+
// Ordinary prompts and injected context project in user role: the event's
|
|
77
|
+
// model-facing content stays verbatim. Do NOT re-add per-type framing
|
|
78
|
+
// (e.g. `<context>`) here: framing is caller-owned — a producer bakes it
|
|
79
|
+
// into `content`, as workspace-context does with `<system-reminder>` — or,
|
|
80
|
+
// if reintroduced, must be driven by the event `meta` map and a dedicated
|
|
81
|
+
// renderer, keeping this projection a verbatim pass-through. See the
|
|
82
|
+
// deferred design note in
|
|
83
|
+
// ../../../../.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md
|
|
84
|
+
case 'user/message': {
|
|
85
|
+
return event.data;
|
|
86
|
+
}
|
|
87
|
+
case 'assistant/message': {
|
|
88
|
+
// Skip an empty-content assistant/message: it exists only to host a
|
|
89
|
+
// max-tokens step's usage and must not inject a content-less assistant
|
|
90
|
+
// turn into the provider transcript.
|
|
91
|
+
if (event.data.message.content.length === 0)
|
|
92
|
+
return null;
|
|
93
|
+
return event.data.message;
|
|
94
|
+
}
|
|
95
|
+
case 'tool/result': {
|
|
96
|
+
return event.data.message;
|
|
97
|
+
}
|
|
98
|
+
default:
|
|
99
|
+
// A non-surface event (boundary, chunk, log-only record) projects to
|
|
100
|
+
// no message. Merge-extensible union: no assertNever here.
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Create an empty surface fold state. */
|
|
105
|
+
function createFoldState() {
|
|
106
|
+
return { nodes: [], replaceGeneration: 0 };
|
|
107
|
+
}
|
|
108
|
+
/** Whether a runtime value is a non-negative safe event sequence. */
|
|
109
|
+
function isEventSeq(value) {
|
|
110
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
111
|
+
}
|
|
112
|
+
/** Whether a runtime value is the exact positional-replacement shape. */
|
|
113
|
+
function isReplaceOp(value) {
|
|
114
|
+
const op = value;
|
|
115
|
+
return Object.keys(op).length === 3
|
|
116
|
+
&& Object.hasOwn(op, 'op')
|
|
117
|
+
&& Object.hasOwn(op, 'start')
|
|
118
|
+
&& Object.hasOwn(op, 'end')
|
|
119
|
+
&& op['op'] === 'replace'
|
|
120
|
+
&& isEventSeq(op['start'])
|
|
121
|
+
&& isEventSeq(op['end']);
|
|
122
|
+
}
|
|
123
|
+
/** Validate event-local surface eligibility and return its operation. */
|
|
124
|
+
function surfaceOpOf(event) {
|
|
125
|
+
const raw = event;
|
|
126
|
+
if (!isSurfaceEligibleType(event.type)) {
|
|
127
|
+
if (raw.surfaceOp !== undefined) {
|
|
128
|
+
throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry surfaceOp`);
|
|
129
|
+
}
|
|
130
|
+
if (raw.sourceEventSeqs !== undefined) {
|
|
131
|
+
throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry sourceEventSeqs`);
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const op = raw.surfaceOp;
|
|
136
|
+
if (op === undefined) {
|
|
137
|
+
throw new Error(`session event "${event.type}" is surface-eligible and requires a surfaceOp marker`);
|
|
138
|
+
}
|
|
139
|
+
if (op === 'append')
|
|
140
|
+
return op;
|
|
141
|
+
if (op === null || typeof op !== 'object' || Array.isArray(op)) {
|
|
142
|
+
throw new Error(`session event "${event.type}" carries an invalid surfaceOp`);
|
|
143
|
+
}
|
|
144
|
+
if (!isReplaceOp(op)) {
|
|
145
|
+
throw new Error(`session event "${event.type}" carries an invalid replace surfaceOp`);
|
|
146
|
+
}
|
|
147
|
+
return op;
|
|
148
|
+
}
|
|
149
|
+
/** Validate cited source-event seqs against prior log entries and the replacement range. */
|
|
150
|
+
function assertProvenance(event, shadowedSeqs) {
|
|
151
|
+
const raw = event.sourceEventSeqs;
|
|
152
|
+
const sources = new Set();
|
|
153
|
+
if (raw !== undefined) {
|
|
154
|
+
if (!Array.isArray(raw)) {
|
|
155
|
+
throw new Error(`sourceEventSeqs on event at seq ${event.seq} must be an array when present`);
|
|
156
|
+
}
|
|
157
|
+
if (raw.length === 0 && event.type !== 'assistant/message') {
|
|
158
|
+
throw new Error('sourceEventSeqs must not be empty except on assistant/message');
|
|
159
|
+
}
|
|
160
|
+
let nonEarlierSource;
|
|
161
|
+
for (const source of raw) {
|
|
162
|
+
if (!isEventSeq(source)) {
|
|
163
|
+
throw new Error(`session event "${event.type}" sourceEventSeqs must densely contain non-negative safe integers`);
|
|
164
|
+
}
|
|
165
|
+
sources.add(source);
|
|
166
|
+
if (nonEarlierSource === undefined && source >= event.seq)
|
|
167
|
+
nonEarlierSource = source;
|
|
168
|
+
}
|
|
169
|
+
if (sources.size !== raw.length) {
|
|
170
|
+
throw new Error('sourceEventSeqs must not contain duplicates');
|
|
171
|
+
}
|
|
172
|
+
if (nonEarlierSource !== undefined) {
|
|
173
|
+
throw new Error(`sourceEventSeqs must reference earlier events: ${nonEarlierSource} >= current seq ${event.seq}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const missing = shadowedSeqs.filter(seq => !sources.has(seq));
|
|
177
|
+
if (missing.length > 0) {
|
|
178
|
+
throw new Error(`surface replace: sourceEventSeqs must include every shadowed surface node; missing ${missing.join(', ')}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Locate one replacement range without mutating the current fold state. */
|
|
182
|
+
function replacementRange(state, op) {
|
|
183
|
+
const startIdx = state.nodes.indexOf(op.start);
|
|
184
|
+
if (startIdx === -1) {
|
|
185
|
+
throw new Error(`surface replace: start seq ${op.start} not found in surface`);
|
|
186
|
+
}
|
|
187
|
+
const endIdx = state.nodes.indexOf(op.end);
|
|
188
|
+
if (endIdx === -1) {
|
|
189
|
+
throw new Error(`surface replace: end seq ${op.end} not found in surface`);
|
|
190
|
+
}
|
|
191
|
+
if (startIdx > endIdx) {
|
|
192
|
+
throw new Error(`surface replace: start seq ${op.start} (index ${startIdx}) is after end seq ${op.end} (index ${endIdx})`);
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
startIdx,
|
|
196
|
+
endIdx,
|
|
197
|
+
shadowedSeqs: state.nodes.slice(startIdx, endIdx + 1),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Deep structural equality over the session-event JSON value domain
|
|
202
|
+
* (null/boolean/number/string, arrays, plain objects). Replaces
|
|
203
|
+
* `node:util`'s isDeepStrictEqual to keep this module browser-safe.
|
|
204
|
+
*/
|
|
205
|
+
function isDeepEqualJson(a, b) {
|
|
206
|
+
if (a === b)
|
|
207
|
+
return true;
|
|
208
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
209
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
210
|
+
return false;
|
|
211
|
+
return a.every((item, i) => isDeepEqualJson(item, b[i]));
|
|
212
|
+
}
|
|
213
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
214
|
+
return false;
|
|
215
|
+
const aKeys = Object.keys(a);
|
|
216
|
+
const bRecord = b;
|
|
217
|
+
if (aKeys.length !== Object.keys(b).length)
|
|
218
|
+
return false;
|
|
219
|
+
return aKeys.every(key => Object.hasOwn(b, key) && isDeepEqualJson(a[key], bRecord[key]));
|
|
220
|
+
}
|
|
221
|
+
/** Restrict a tool-result replacement to one current result's content. */
|
|
222
|
+
function assertToolResultRewrite(event, shadowedSeqs, events, baseSeq) {
|
|
223
|
+
if (event.type !== 'tool/result')
|
|
224
|
+
return;
|
|
225
|
+
if (shadowedSeqs.length !== 1) {
|
|
226
|
+
throw new Error('tool/result surface replacement must rewrite exactly one current node');
|
|
227
|
+
}
|
|
228
|
+
for (const originalSeq of shadowedSeqs) {
|
|
229
|
+
const original = events[originalSeq - baseSeq];
|
|
230
|
+
if (original?.type !== 'tool/result') {
|
|
231
|
+
throw new Error('tool/result surface replacement must target a current tool/result');
|
|
232
|
+
}
|
|
233
|
+
const originalRest = { ...original.data };
|
|
234
|
+
const replacementRest = { ...event.data };
|
|
235
|
+
const originalResult = original.data.message.content[0];
|
|
236
|
+
const replacementResult = event.data.message.content[0];
|
|
237
|
+
originalRest['message'] = {
|
|
238
|
+
...original.data.message,
|
|
239
|
+
content: [{ ...originalResult, content: null }],
|
|
240
|
+
};
|
|
241
|
+
replacementRest['message'] = {
|
|
242
|
+
...event.data.message,
|
|
243
|
+
content: [{ ...replacementResult, content: null }],
|
|
244
|
+
};
|
|
245
|
+
if (!isDeepEqualJson(originalRest, replacementRest)) {
|
|
246
|
+
throw new Error('tool/result surface replacement may change only content');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** Validate one event at its replay boundary and prepare its atomic fold transition. */
|
|
251
|
+
function planSurfaceEvent(state, event, expectedSeq, events, baseSeq) {
|
|
252
|
+
if (event.seq !== expectedSeq) {
|
|
253
|
+
throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`);
|
|
254
|
+
}
|
|
255
|
+
const surfaceOp = surfaceOpOf(event);
|
|
256
|
+
if (surfaceOp === undefined)
|
|
257
|
+
return;
|
|
258
|
+
if (surfaceOp === 'append') {
|
|
259
|
+
assertProvenance(event, []);
|
|
260
|
+
return { kind: 'append', seq: event.seq };
|
|
261
|
+
}
|
|
262
|
+
const range = replacementRange(state, surfaceOp);
|
|
263
|
+
assertProvenance(event, range.shadowedSeqs);
|
|
264
|
+
assertToolResultRewrite(event, range.shadowedSeqs, events, baseSeq);
|
|
265
|
+
return {
|
|
266
|
+
kind: 'replace',
|
|
267
|
+
seq: event.seq,
|
|
268
|
+
start: surfaceOp.start,
|
|
269
|
+
end: surfaceOp.end,
|
|
270
|
+
...range,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/** Apply one event and return replacement metadata only when one occurred. */
|
|
274
|
+
function applySurfaceEvent(state, event, expectedSeq, events, baseSeq) {
|
|
275
|
+
const plan = planSurfaceEvent(state, event, expectedSeq, events, baseSeq);
|
|
276
|
+
return applySurfacePlan(state, plan);
|
|
277
|
+
}
|
|
278
|
+
/** Commit one previously validated surface transition. */
|
|
279
|
+
function applySurfacePlan(state, plan) {
|
|
280
|
+
if (plan?.kind === 'append') {
|
|
281
|
+
state.nodes.push(plan.seq);
|
|
282
|
+
}
|
|
283
|
+
else if (plan?.kind === 'replace') {
|
|
284
|
+
state.nodes.splice(plan.startIdx, plan.endIdx - plan.startIdx + 1, plan.seq);
|
|
285
|
+
state.replaceGeneration += 1;
|
|
286
|
+
}
|
|
287
|
+
if (plan?.kind !== 'replace')
|
|
288
|
+
return;
|
|
289
|
+
return {
|
|
290
|
+
seq: plan.seq,
|
|
291
|
+
start: plan.start,
|
|
292
|
+
end: plan.end,
|
|
293
|
+
shadowedSeqs: plan.shadowedSeqs,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Replay a complete session log through the canonical surface fold.
|
|
298
|
+
* @param events - session events in contiguous seq order.
|
|
299
|
+
* @returns detached current sequences and replacement history.
|
|
300
|
+
* @throws when an event violates surface metadata, source-event references, range, or tool-result rewrite rules.
|
|
301
|
+
*/
|
|
302
|
+
export function foldSurface(events) {
|
|
303
|
+
const state = createFoldState();
|
|
304
|
+
const replacements = [];
|
|
305
|
+
for (const [index, event] of events.entries()) {
|
|
306
|
+
const replacement = applySurfaceEvent(state, event, index, events, 0);
|
|
307
|
+
if (replacement !== undefined)
|
|
308
|
+
replacements.push(replacement);
|
|
309
|
+
}
|
|
310
|
+
return { nodes: [...state.nodes], replacements };
|
|
311
|
+
}
|
|
312
|
+
/** Incremental ordered surface view and append-boundary validator. */
|
|
313
|
+
export class SurfaceManager {
|
|
314
|
+
log;
|
|
315
|
+
baseSeq;
|
|
316
|
+
/** Shared transition state; replacement history is not retained. */
|
|
317
|
+
_state = createFoldState();
|
|
318
|
+
/** Last processed absolute seq. */
|
|
319
|
+
_lastProcessedSeq;
|
|
320
|
+
/** Candidate already validated by `validateNext`, pending exact log admission. */
|
|
321
|
+
_pendingPlan;
|
|
322
|
+
/**
|
|
323
|
+
* @param log - Contiguous complete log or loaded event window.
|
|
324
|
+
* @param baseSeq - Absolute sequence of the window's first event.
|
|
325
|
+
*/
|
|
326
|
+
constructor(log, baseSeq = 0) {
|
|
327
|
+
this.log = log;
|
|
328
|
+
this.baseSeq = baseSeq;
|
|
329
|
+
this._lastProcessedSeq = baseSeq - 1;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Validate the next candidate without mutating the committed surface.
|
|
333
|
+
* @param event - candidate event that has not entered the log yet.
|
|
334
|
+
*/
|
|
335
|
+
validateNext(event) {
|
|
336
|
+
if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1)
|
|
337
|
+
this._processDelta();
|
|
338
|
+
const expectedSeq = this.baseSeq + this.log.length;
|
|
339
|
+
this._pendingPlan = {
|
|
340
|
+
event,
|
|
341
|
+
expectedSeq,
|
|
342
|
+
plan: planSurfaceEvent(this._state, event, expectedSeq, this.log, this.baseSeq),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/** Monotonic count of folded positional replacements. */
|
|
346
|
+
get replaceGeneration() {
|
|
347
|
+
if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1)
|
|
348
|
+
this._processDelta();
|
|
349
|
+
return this._state.replaceGeneration;
|
|
350
|
+
}
|
|
351
|
+
/** Surface event sequences in model-visible order. */
|
|
352
|
+
get nodes() {
|
|
353
|
+
if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1)
|
|
354
|
+
this._processDelta();
|
|
355
|
+
return this._state.nodes;
|
|
356
|
+
}
|
|
357
|
+
/** Fold events appended since the previous access. */
|
|
358
|
+
_processDelta() {
|
|
359
|
+
const tailSeq = this.baseSeq + this.log.length - 1;
|
|
360
|
+
for (let seq = this._lastProcessedSeq + 1; seq <= tailSeq; seq++) {
|
|
361
|
+
const index = seq - this.baseSeq;
|
|
362
|
+
// oxlint-disable-next-line typescript/no-non-null-assertion -- bounded by the loop condition
|
|
363
|
+
const event = this.log[index];
|
|
364
|
+
const pending = this._pendingPlan;
|
|
365
|
+
if (pending?.event === event && pending.expectedSeq === seq) {
|
|
366
|
+
applySurfacePlan(this._state, pending.plan);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
applySurfaceEvent(this._state, event, seq, this.log, this.baseSeq);
|
|
370
|
+
}
|
|
371
|
+
if (pending !== undefined && pending.expectedSeq <= seq)
|
|
372
|
+
this._pendingPlan = undefined;
|
|
373
|
+
this._lastProcessedSeq = seq;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
//# sourceMappingURL=surface.js.map
|