@deepseek-ai/dsh-session 0.1.6-alpha.1 → 0.1.7-alpha.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/README.i18n.yaml +2 -2
- package/README.md +11 -9
- package/README.zh.md +12 -8
- package/lib/index.js +217 -126
- package/lib/invariant.js +4 -1
- package/lib/types/fork.d.ts +18 -0
- package/lib/types/fork.js +28 -0
- package/lib/types/index.d.ts +27 -29
- package/lib/types/index.js +56 -61
- package/lib/types/invariant.js +5 -1
- package/lib/types/known-event-types.js +2 -0
- package/lib/types/repair.d.ts +42 -7
- package/lib/types/repair.js +52 -23
- package/lib/types/surface.d.ts +4 -4
- package/lib/types/surface.js +73 -17
- package/lib/types/types.d.ts +38 -15
- package/lib/types/types.js +1 -1
- package/package.json +15 -11
package/lib/index.js
CHANGED
|
@@ -53,7 +53,7 @@ function SessionLogOffset(value) {
|
|
|
53
53
|
* immutable prior-generation, and current fast-path rules are recorded in
|
|
54
54
|
* `.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md`.
|
|
55
55
|
*/
|
|
56
|
-
const SESSION_FORMAT_VERSION =
|
|
56
|
+
const SESSION_FORMAT_VERSION = 4;
|
|
57
57
|
//#endregion
|
|
58
58
|
//#region lib/types/known-event-types.js
|
|
59
59
|
/**
|
|
@@ -91,6 +91,7 @@ const KNOWN_SESSION_EVENT_TYPES = new Set([
|
|
|
91
91
|
"compaction/start",
|
|
92
92
|
"compaction/summary",
|
|
93
93
|
"deliverables/presented",
|
|
94
|
+
"developer/message",
|
|
94
95
|
"feedback/message-delete",
|
|
95
96
|
"feedback/message-put",
|
|
96
97
|
"feedback/record",
|
|
@@ -133,7 +134,8 @@ const KNOWN_SESSION_EVENT_TYPES = new Set([
|
|
|
133
134
|
"turn/end",
|
|
134
135
|
"turn/start",
|
|
135
136
|
"user/message",
|
|
136
|
-
"web/deepseek-search-llm-request"
|
|
137
|
+
"web/deepseek-search-llm-request",
|
|
138
|
+
"workspace/changes"
|
|
137
139
|
]);
|
|
138
140
|
/** Event types whose model-visible effects require an explicit pure interpreter. */
|
|
139
141
|
const MESSAGE_PROJECTION_EVENT_TYPES = new Set(["image/offload"]);
|
|
@@ -151,6 +153,7 @@ const MESSAGE_PROJECTION_EVENT_TYPES = new Set(["image/offload"]);
|
|
|
151
153
|
/** Runtime counterpart of the message-producing event union. */
|
|
152
154
|
const SURFACE_EVENT_TYPES = new Set([
|
|
153
155
|
"system/message",
|
|
156
|
+
"developer/message",
|
|
154
157
|
"user/message",
|
|
155
158
|
"assistant/message",
|
|
156
159
|
"tool/result"
|
|
@@ -158,7 +161,7 @@ const SURFACE_EVENT_TYPES = new Set([
|
|
|
158
161
|
/**
|
|
159
162
|
* Whether an event type can join the model-visible surface.
|
|
160
163
|
* @param type - event type to test.
|
|
161
|
-
* @returns true for one of the
|
|
164
|
+
* @returns true for one of the message-producing event types.
|
|
162
165
|
*/
|
|
163
166
|
function isSurfaceEligibleType(type) {
|
|
164
167
|
return SURFACE_EVENT_TYPES.has(type);
|
|
@@ -199,7 +202,7 @@ function isReplacementSurfaceEvent(event) {
|
|
|
199
202
|
/**
|
|
200
203
|
* Project a single event into the LLM message it derives to, or null when it
|
|
201
204
|
* produces none — a non-surface event (attempt, boundary, log-only record) or an
|
|
202
|
-
* empty-content
|
|
205
|
+
* empty-content system, developer, or assistant message. A caller
|
|
203
206
|
* reconstructing model input supplies the same prefix's `projectedMessages`
|
|
204
207
|
* from {@link foldSurface}; without that map this function reads original
|
|
205
208
|
* event content. Session instance methods apply the live projection. Messages
|
|
@@ -214,6 +217,7 @@ function deriveEventMessage(event, projectedMessages) {
|
|
|
214
217
|
switch (event.type) {
|
|
215
218
|
case "user/message": return event.data;
|
|
216
219
|
case "system/message":
|
|
220
|
+
case "developer/message":
|
|
217
221
|
case "assistant/message":
|
|
218
222
|
if (event.data.message.content.length === 0) return null;
|
|
219
223
|
return event.data.message;
|
|
@@ -226,14 +230,33 @@ function isRecord(value) {
|
|
|
226
230
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
227
231
|
}
|
|
228
232
|
/**
|
|
229
|
-
* Reject noncanonical request-header fields and contradictory tool failure metadata.
|
|
233
|
+
* Reject noncanonical request-header fields, developer roles/content, and contradictory tool failure metadata.
|
|
230
234
|
* This does not validate complete event payloads or embedded provider streams.
|
|
231
235
|
* @param event - event whose locally related payload fields are inspected.
|
|
232
236
|
* @param subject - event location to include in validation errors.
|
|
233
|
-
* @throws when request
|
|
237
|
+
* @throws when request-header fields, developer roles/content, or tool failure metadata are invalid.
|
|
234
238
|
*/
|
|
235
239
|
function validateSessionEventData(event, subject) {
|
|
236
240
|
const data = event.data;
|
|
241
|
+
if (SURFACE_EVENT_TYPES.has(event.type) && isRecord(data)) {
|
|
242
|
+
const message = event.type === "user/message" ? data : data["message"];
|
|
243
|
+
if (isRecord(message)) {
|
|
244
|
+
if (event.type === "developer/message" !== (message["role"] === "developer")) throw new Error(`${subject} developer/message and developer role must occur together`);
|
|
245
|
+
if (message["role"] !== "developer" && Array.isArray(message["content"]) && message["content"].some((block) => isRecord(block) && (block["type"] === "tool-addition" || block["type"] === "tool-removal"))) throw new Error(`${subject} tool-change blocks require developer role`);
|
|
246
|
+
if (event.type === "developer/message" && Array.isArray(message["content"])) {
|
|
247
|
+
let hasAdditions = false;
|
|
248
|
+
for (const block of message["content"]) {
|
|
249
|
+
if (!isRecord(block) || block["type"] !== "tool-addition" && block["type"] !== "tool-removal") continue;
|
|
250
|
+
if (typeof block["toolName"] !== "string" || block["toolName"].length === 0) throw new Error(`${subject} ${block["type"]} requires a nonempty toolName`);
|
|
251
|
+
if (block["type"] === "tool-addition") {
|
|
252
|
+
hasAdditions = true;
|
|
253
|
+
if (Object.hasOwn(block, "tool")) throw new Error(`${subject} tool-addition must omit inline tool definitions`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (hasAdditions ? !isEventSeq(data["headerSeq"]) : Object.hasOwn(data, "headerSeq")) throw new Error(`${subject} requires headerSeq exactly when tool additions are present`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
237
260
|
if (event.type === "request/header") {
|
|
238
261
|
if (!isRecord(data)) throw new Error(`${subject} data must be an object`);
|
|
239
262
|
const header = data["header"];
|
|
@@ -246,9 +269,7 @@ function validateSessionEventData(event, subject) {
|
|
|
246
269
|
if (!isRecord(data)) throw new Error(`${subject} data must be an object`);
|
|
247
270
|
if (data["error"] === void 0) return;
|
|
248
271
|
const message = data["message"];
|
|
249
|
-
|
|
250
|
-
const block = Array.isArray(content) ? content[0] : void 0;
|
|
251
|
-
if (!isRecord(block) || block["isError"] !== true) throw new Error(`${subject} error requires message content[0].isError === true`);
|
|
272
|
+
if (!isRecord(message) || message["isError"] !== true) throw new Error(`${subject} error requires message.isError === true`);
|
|
252
273
|
}
|
|
253
274
|
}
|
|
254
275
|
/** Create an empty surface fold state. */
|
|
@@ -306,6 +327,23 @@ function assertSourceEventReferences(event, shadowedSeqs) {
|
|
|
306
327
|
const missing = shadowedSeqs.filter((seq) => !sources.has(seq));
|
|
307
328
|
if (missing.length > 0) throw new Error(`surface replace: sourceEventSeqs must include every shadowed surface node; missing ${missing.join(", ")}`);
|
|
308
329
|
}
|
|
330
|
+
/** Resolve tool additions against their immutable historical request header. */
|
|
331
|
+
function assertDeveloperHeader(event, events, baseSeq) {
|
|
332
|
+
if (event.type !== "developer/message") return;
|
|
333
|
+
validateSessionEventData(event, `developer/message at seq ${event.seq}`);
|
|
334
|
+
if (event.data.headerSeq === void 0) return;
|
|
335
|
+
const headerSeq = event.data.headerSeq;
|
|
336
|
+
const headerEvent = events[headerSeq - baseSeq];
|
|
337
|
+
if (headerSeq >= event.seq || headerEvent?.type !== "request/header") throw new Error("developer/message headerSeq must reference an earlier request/header");
|
|
338
|
+
for (const block of event.data.message.content) {
|
|
339
|
+
if (block.type !== "tool-addition") continue;
|
|
340
|
+
const definitions = headerEvent.data.header.tools?.filter((tool) => tool.name === block.toolName) ?? [];
|
|
341
|
+
if (definitions.length !== 1) throw new Error(`developer/message tool-addition "${block.toolName}" must name exactly one tool in headerSeq ${headerSeq}`);
|
|
342
|
+
const definition = definitions[0];
|
|
343
|
+
if (typeof definition.description !== "string" || !isRecord(definition.parameters)) throw new Error(`developer/message tool-addition "${block.toolName}" requires a complete tool definition in headerSeq ${headerSeq}`);
|
|
344
|
+
if (Object.hasOwn(definition, "deferLoading") && definition.deferLoading !== true) throw new Error("developer/message referenced tool deferLoading must be true when present");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
309
347
|
/**
|
|
310
348
|
* Validate one event's surface metadata without checking membership in a log or surface.
|
|
311
349
|
* @param event - event whose marker and source sequence values are inspected.
|
|
@@ -358,21 +396,13 @@ function assertToolResultRewrite(event, shadowedSeqs, events, baseSeq) {
|
|
|
358
396
|
if (original?.type !== "tool/result") throw new Error("tool/result surface replacement must target a current tool/result");
|
|
359
397
|
const originalRest = { ...original.data };
|
|
360
398
|
const replacementRest = { ...event.data };
|
|
361
|
-
const originalResult = original.data.message.content[0];
|
|
362
|
-
const replacementResult = event.data.message.content[0];
|
|
363
399
|
originalRest["message"] = {
|
|
364
400
|
...original.data.message,
|
|
365
|
-
content:
|
|
366
|
-
...originalResult,
|
|
367
|
-
content: null
|
|
368
|
-
}]
|
|
401
|
+
content: null
|
|
369
402
|
};
|
|
370
403
|
replacementRest["message"] = {
|
|
371
404
|
...event.data.message,
|
|
372
|
-
content:
|
|
373
|
-
...replacementResult,
|
|
374
|
-
content: null
|
|
375
|
-
}]
|
|
405
|
+
content: null
|
|
376
406
|
};
|
|
377
407
|
if (!isDeepEqualJson(originalRest, replacementRest)) throw new Error("tool/result surface replacement may change only content");
|
|
378
408
|
}
|
|
@@ -392,6 +422,7 @@ function assertSystemHeadRewrite(event, state, startIdx, shadowedSeqs, events, b
|
|
|
392
422
|
function planSurfaceEvent(state, event, expectedSeq, events, baseSeq, projections) {
|
|
393
423
|
if (event.seq !== expectedSeq) throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`);
|
|
394
424
|
const surfaceOp = validateSurfaceMetadata(event);
|
|
425
|
+
assertDeveloperHeader(event, events, baseSeq);
|
|
395
426
|
const projection = projections.find((item) => item.type === event.type);
|
|
396
427
|
if (projection !== void 0) return {
|
|
397
428
|
kind: "project",
|
|
@@ -606,64 +637,49 @@ function foldRequestHeader(events, from) {
|
|
|
606
637
|
return state;
|
|
607
638
|
}
|
|
608
639
|
//#endregion
|
|
609
|
-
//#region lib/types/preparation.js
|
|
610
|
-
/**
|
|
611
|
-
* Ownership of one unpublished Session before registry publication.
|
|
612
|
-
* @module @deepseek-ai/dsh-session/preparation
|
|
613
|
-
*/
|
|
614
|
-
/**
|
|
615
|
-
* One exact unpublished Session and the provider state that keeps it usable.
|
|
616
|
-
* Disposal is synchronous and idempotent. Providers decide whether release
|
|
617
|
-
* returns the Session to a cache or discards it; publication may consume that
|
|
618
|
-
* state before disposal, making the callback a no-op.
|
|
619
|
-
*/
|
|
620
|
-
var SessionPreparation = class SessionPreparation {
|
|
621
|
-
options;
|
|
622
|
-
released = false;
|
|
623
|
-
/** The exact Session to use for setup and publication. */
|
|
624
|
-
session;
|
|
625
|
-
constructor(session, options) {
|
|
626
|
-
this.options = options;
|
|
627
|
-
this.session = session;
|
|
628
|
-
}
|
|
629
|
-
/**
|
|
630
|
-
* Wrap an unpublished Session in one preparation lifetime.
|
|
631
|
-
* @param session - exact unpublished Session.
|
|
632
|
-
* @param options - optional provider release behavior.
|
|
633
|
-
* @returns a preparation disposed after publication or rollback.
|
|
634
|
-
*/
|
|
635
|
-
static create(session, options) {
|
|
636
|
-
return new SessionPreparation(session, options ?? {});
|
|
637
|
-
}
|
|
638
|
-
/** Release provider state once when this preparation leaves its caller. */
|
|
639
|
-
[Symbol.dispose]() {
|
|
640
|
-
if (this.released) return;
|
|
641
|
-
this.released = true;
|
|
642
|
-
this.options.release?.();
|
|
643
|
-
}
|
|
644
|
-
};
|
|
645
|
-
//#endregion
|
|
646
640
|
//#region lib/types/repair.js
|
|
647
641
|
/**
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
642
|
+
* Synthetic closer events that balance a session log whose tail turn is open.
|
|
643
|
+
* Two producers share the mechanism: crash recovery closes an interrupted
|
|
644
|
+
* persisted log on reload, and fork-seed construction closes a prefix cut
|
|
645
|
+
* inside the source's open turn. Both preserve every fully written event and
|
|
646
|
+
* close the unfinished step and turn. Calls in already closed steps remain
|
|
647
|
+
* unchanged, including any missing results.
|
|
651
648
|
* @module @deepseek-ai/dsh-session/repair
|
|
652
649
|
*/
|
|
653
650
|
/** Recovery code for an assistant tool request that never reached a recorded call start. */
|
|
654
651
|
const TOOL_NOT_STARTED = "TOOL_NOT_STARTED";
|
|
655
652
|
/** Recovery code for a recorded tool call whose completed outcome was not durably recorded. */
|
|
656
653
|
const TOOL_OUTCOME_UNKNOWN = "TOOL_OUTCOME_UNKNOWN";
|
|
654
|
+
/** Model-visible wording of the synthetic error tool results, keyed by cause. */
|
|
655
|
+
const CLOSER_TEXT = {
|
|
656
|
+
interrupted: {
|
|
657
|
+
started: "The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.",
|
|
658
|
+
notStarted: "The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed."
|
|
659
|
+
},
|
|
660
|
+
forked: {
|
|
661
|
+
started: "The history inherited by this branch records this tool call starting but does not include its result. The parent session may have completed it after the fork point. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.",
|
|
662
|
+
notStarted: "The history inherited by this branch has no record of this tool call starting. The parent session may have executed it after the fork point. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly."
|
|
663
|
+
}
|
|
664
|
+
};
|
|
657
665
|
/**
|
|
658
666
|
* Return deterministic synthetic events that close an open tail turn. Unmatched
|
|
659
|
-
* calls receive error results
|
|
660
|
-
*
|
|
661
|
-
* last real event. A balanced or empty log returns no
|
|
667
|
+
* calls in its open step receive error results, followed by `step/end` and a
|
|
668
|
+
* `turn/end` carrying the cause's reason. Calls in closed steps remain unchanged.
|
|
669
|
+
* Sequences continue the log and timestamps reuse the last real event. A balanced or empty log returns no
|
|
670
|
+
* events.
|
|
662
671
|
*
|
|
663
|
-
*
|
|
672
|
+
* Package-internal: each cause has exactly one owner, so external callers go
|
|
673
|
+
* through {@link interruptedTurnClosers} (persistence crash recovery) or
|
|
674
|
+
* `buildForkSeed` in `./fork.ts` (fork seeds) instead of selecting a cause.
|
|
675
|
+
*
|
|
676
|
+
* @param events - the log to scan: a valid committed prefix, possibly ending
|
|
677
|
+
* inside an open turn (a crash tail or a mid-turn fork cut).
|
|
678
|
+
* @param cause - why the turn is being closed; selects the `turn/end` reason
|
|
679
|
+
* and the model-visible wording of synthetic error tool results.
|
|
664
680
|
* @returns the synthetic closer events to append after `events`, in order; empty when the log is already balanced.
|
|
665
681
|
*/
|
|
666
|
-
function
|
|
682
|
+
function openTurnClosers(events, cause) {
|
|
667
683
|
let openTurn = null;
|
|
668
684
|
let openStep = null;
|
|
669
685
|
const pendingCalls = /* @__PURE__ */ new Map();
|
|
@@ -704,23 +720,21 @@ function interruptedTurnClosers(events) {
|
|
|
704
720
|
let seq = last.seq + 1;
|
|
705
721
|
const time = last.time;
|
|
706
722
|
const closers = [];
|
|
723
|
+
const text = CLOSER_TEXT[cause.kind];
|
|
707
724
|
for (const [callId, { step, callSeq }] of pendingCalls) {
|
|
708
725
|
const started = callSeq !== void 0;
|
|
709
726
|
const message = deepFreeze({
|
|
710
|
-
id: brandString(
|
|
711
|
-
role: "
|
|
727
|
+
id: brandString(`${cause.kind}-tool-result-${callId}-${seq}`),
|
|
728
|
+
role: "tool",
|
|
729
|
+
toolCallId: callId,
|
|
730
|
+
isError: true,
|
|
712
731
|
source: {
|
|
713
732
|
kind: "tool",
|
|
714
733
|
callId
|
|
715
734
|
},
|
|
716
735
|
content: [{
|
|
717
|
-
type: "
|
|
718
|
-
|
|
719
|
-
isError: true,
|
|
720
|
-
content: [{
|
|
721
|
-
type: "text",
|
|
722
|
-
text: started ? "The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly." : "The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed."
|
|
723
|
-
}]
|
|
736
|
+
type: "text",
|
|
737
|
+
text: started ? text.started : text.notStarted
|
|
724
738
|
}]
|
|
725
739
|
});
|
|
726
740
|
closers.push({
|
|
@@ -758,11 +772,87 @@ function interruptedTurnClosers(events) {
|
|
|
758
772
|
time,
|
|
759
773
|
data: {
|
|
760
774
|
turn: openTurn,
|
|
761
|
-
reason: { kind:
|
|
775
|
+
reason: { kind: cause.kind }
|
|
762
776
|
}
|
|
763
777
|
});
|
|
764
778
|
return closers;
|
|
765
779
|
}
|
|
780
|
+
/**
|
|
781
|
+
* Crash-recovery entry point: synthetic closers that balance a persisted log
|
|
782
|
+
* whose tail turn was interrupted. Used by crash-recovery callers; fork
|
|
783
|
+
* seeds receive their `forked`-cause closers through `buildForkSeed` in
|
|
784
|
+
* `./fork.ts`, and cause selection stays internal to those two owners.
|
|
785
|
+
*
|
|
786
|
+
* @param events - the persisted log to scan, possibly ending inside an open turn.
|
|
787
|
+
* @returns the synthetic `interrupted` closer events to append after `events`; empty when the log is already balanced.
|
|
788
|
+
*/
|
|
789
|
+
function interruptedTurnClosers(events) {
|
|
790
|
+
return openTurnClosers(events, { kind: "interrupted" });
|
|
791
|
+
}
|
|
792
|
+
//#endregion
|
|
793
|
+
//#region lib/types/fork.js
|
|
794
|
+
/**
|
|
795
|
+
* Fork seed construction over an exact source-event prefix.
|
|
796
|
+
* @module @deepseek-ai/dsh-session/fork
|
|
797
|
+
*/
|
|
798
|
+
/**
|
|
799
|
+
* Copy an inclusive event prefix, mark its inherited cut, and close its open tail with forked results
|
|
800
|
+
* and step/turn endings. Closed steps and turns are preserved unchanged.
|
|
801
|
+
* The caller validates that the boundary is an existing contiguous event seq;
|
|
802
|
+
* Session construction snapshots the borrowed events before publication.
|
|
803
|
+
*
|
|
804
|
+
* @param events - source log with contiguous seqs from zero.
|
|
805
|
+
* @param boundary - inclusive source event seq the child inherits through.
|
|
806
|
+
* @returns a new array retaining the source event objects, followed by synthetic
|
|
807
|
+
* closers outside the inherited prefix counted by `inheritedEventCount`.
|
|
808
|
+
*/
|
|
809
|
+
function buildForkSeed(events, boundary) {
|
|
810
|
+
const prefix = events.slice(0, boundary + 1);
|
|
811
|
+
prefix.push({
|
|
812
|
+
type: "session/end-seed",
|
|
813
|
+
seq: SessionSeq(boundary + 1),
|
|
814
|
+
time: events[boundary].time,
|
|
815
|
+
data: { inherited: true }
|
|
816
|
+
});
|
|
817
|
+
return prefix.concat(openTurnClosers(prefix, { kind: "forked" }));
|
|
818
|
+
}
|
|
819
|
+
//#endregion
|
|
820
|
+
//#region lib/types/preparation.js
|
|
821
|
+
/**
|
|
822
|
+
* Ownership of one unpublished Session before registry publication.
|
|
823
|
+
* @module @deepseek-ai/dsh-session/preparation
|
|
824
|
+
*/
|
|
825
|
+
/**
|
|
826
|
+
* One exact unpublished Session and the provider state that keeps it usable.
|
|
827
|
+
* Disposal is synchronous and idempotent. Providers decide whether release
|
|
828
|
+
* returns the Session to a cache or discards it; publication may consume that
|
|
829
|
+
* state before disposal, making the callback a no-op.
|
|
830
|
+
*/
|
|
831
|
+
var SessionPreparation = class SessionPreparation {
|
|
832
|
+
options;
|
|
833
|
+
released = false;
|
|
834
|
+
/** The exact Session to use for setup and publication. */
|
|
835
|
+
session;
|
|
836
|
+
constructor(session, options) {
|
|
837
|
+
this.options = options;
|
|
838
|
+
this.session = session;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Wrap an unpublished Session in one preparation lifetime.
|
|
842
|
+
* @param session - exact unpublished Session.
|
|
843
|
+
* @param options - optional provider release behavior.
|
|
844
|
+
* @returns a preparation disposed after publication or rollback.
|
|
845
|
+
*/
|
|
846
|
+
static create(session, options) {
|
|
847
|
+
return new SessionPreparation(session, options ?? {});
|
|
848
|
+
}
|
|
849
|
+
/** Release provider state once when this preparation leaves its caller. */
|
|
850
|
+
[Symbol.dispose]() {
|
|
851
|
+
if (this.released) return;
|
|
852
|
+
this.released = true;
|
|
853
|
+
this.options.release?.();
|
|
854
|
+
}
|
|
855
|
+
};
|
|
766
856
|
//#endregion
|
|
767
857
|
//#region lib/types/seq-ranges.js
|
|
768
858
|
/** Lossless range encoding for JSONL `sourceEventSeqs` arrays. */
|
|
@@ -833,7 +923,7 @@ function validateSessionHeader(id, input) {
|
|
|
833
923
|
if (input === null || typeof input !== "object" || Array.isArray(input)) throw new Error("session header is not a plain JSON record");
|
|
834
924
|
const record = input;
|
|
835
925
|
if (Object.hasOwn(record, "seedLength")) throw new Error("session header has invalid field \"seedLength\"");
|
|
836
|
-
if (record.version !==
|
|
926
|
+
if (record.version !== 4) throw new Error(`session header version must be 4, got ${String(record.version)}`);
|
|
837
927
|
if (record.id !== id) throw new Error(`session header id "${String(record.id)}" does not match session id "${id}"`);
|
|
838
928
|
if (typeof record.createdAt !== "number" || !Number.isSafeInteger(record.createdAt) || record.createdAt < 0) throw new Error("session header createdAt must be a non-negative safe integer");
|
|
839
929
|
if (record.cwd !== void 0) {
|
|
@@ -858,7 +948,7 @@ function validateRestoredSessionHeader(id, input) {
|
|
|
858
948
|
/** Detach, validate, and freeze the creation metadata published by a session. */
|
|
859
949
|
function snapshotSessionHeader(id, source) {
|
|
860
950
|
const snapshot = snapshotJsonValue(source === void 0 ? {
|
|
861
|
-
version:
|
|
951
|
+
version: 4,
|
|
862
952
|
id,
|
|
863
953
|
createdAt: Date.now(),
|
|
864
954
|
isSeeded: false
|
|
@@ -883,6 +973,7 @@ function adoptSessionEvent(event) {
|
|
|
883
973
|
case "user/message":
|
|
884
974
|
deepFreeze(event.data);
|
|
885
975
|
break;
|
|
976
|
+
case "developer/message":
|
|
886
977
|
case "system/message":
|
|
887
978
|
case "assistant/message":
|
|
888
979
|
case "tool/result":
|
|
@@ -921,6 +1012,7 @@ function assertSessionEventEnvelope(value, index) {
|
|
|
921
1012
|
validateSessionEventData(event, `seed ${type} at index ${index}`);
|
|
922
1013
|
switch (type) {
|
|
923
1014
|
case "request/header":
|
|
1015
|
+
case "developer/message":
|
|
924
1016
|
case "system/message":
|
|
925
1017
|
case "user/message":
|
|
926
1018
|
case "assistant/attempt":
|
|
@@ -969,15 +1061,16 @@ function assertAdapterDefaults(value, config, index) {
|
|
|
969
1061
|
const defaults = value;
|
|
970
1062
|
if (Object.keys(defaults).some((key) => !allowedAdapterKeys.has(key)) || Object.values(defaults).some((marker) => marker !== true) || defaults["reasoningEffort"] === true && config["reasoningEffort"] === void 0 || defaults["maxTokens"] === true && config["maxTokens"] === void 0) throw new Error(`seed request/header at index ${index} has invalid adapterDefaults`);
|
|
971
1063
|
}
|
|
972
|
-
/** The
|
|
1064
|
+
/** The surface event types whose payload carries an identified message. */
|
|
973
1065
|
function isMessageEventType(type) {
|
|
974
|
-
return type === "system/message" || type === "user/message" || type === "assistant/message" || type === "tool/result";
|
|
1066
|
+
return type === "developer/message" || type === "system/message" || type === "user/message" || type === "assistant/message" || type === "tool/result";
|
|
975
1067
|
}
|
|
976
1068
|
const MESSAGE_ROLE_BY_TYPE = {
|
|
977
1069
|
"system/message": "system",
|
|
1070
|
+
"developer/message": "developer",
|
|
978
1071
|
"user/message": "user",
|
|
979
1072
|
"assistant/message": "assistant",
|
|
980
|
-
"tool/result": "
|
|
1073
|
+
"tool/result": "tool"
|
|
981
1074
|
};
|
|
982
1075
|
/** Validate only the event-specific invariants needed to safely replay a message. */
|
|
983
1076
|
function assertMessageEventShape(event, subject) {
|
|
@@ -995,7 +1088,7 @@ function assertMessageEventShape(event, subject) {
|
|
|
995
1088
|
if (!Array.isArray(messageRecord["content"])) throw new Error(`${subject} message has invalid content`);
|
|
996
1089
|
const sourceRecord = source;
|
|
997
1090
|
if (type === "system/message") {
|
|
998
|
-
if (sourceRecord["kind"] !== "
|
|
1091
|
+
if (sourceRecord["kind"] !== "system-prompt") throw new Error(`${subject} message must have system-prompt source`);
|
|
999
1092
|
return;
|
|
1000
1093
|
}
|
|
1001
1094
|
if (type === "assistant/message") {
|
|
@@ -1004,10 +1097,7 @@ function assertMessageEventShape(event, subject) {
|
|
|
1004
1097
|
}
|
|
1005
1098
|
if (type !== "tool/result") return;
|
|
1006
1099
|
if (sourceRecord["kind"] !== "tool" || typeof sourceRecord["callId"] !== "string" || sourceRecord["callId"] === "") throw new Error(`${subject} message must have tool source`);
|
|
1007
|
-
|
|
1008
|
-
const block = content[0];
|
|
1009
|
-
if (content.length !== 1 || typeof block !== "object" || block === null || block["type"] !== "tool-result" || !Array.isArray(block["content"])) throw new Error(`${subject} message must contain one tool-result block`);
|
|
1010
|
-
if (block["toolCallId"] !== sourceRecord["callId"]) throw new Error(`${subject} message has mismatched tool call ids`);
|
|
1100
|
+
if (messageRecord["toolCallId"] !== sourceRecord["callId"]) throw new Error(`${subject} message has mismatched tool call ids`);
|
|
1011
1101
|
}
|
|
1012
1102
|
/** Whether an unknown value carries the current provider/model pair. */
|
|
1013
1103
|
function hasProviderModel(value) {
|
|
@@ -1064,30 +1154,26 @@ var Session = class Session {
|
|
|
1064
1154
|
return this.header.id;
|
|
1065
1155
|
}
|
|
1066
1156
|
/**
|
|
1067
|
-
* The
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
* the constructor-input boundary for lifecycle ownership and persistence
|
|
1072
|
-
* adoption; consumers that need complete canonical history still start at
|
|
1073
|
-
* seq 0. Distinct from {@link inheritedEventCount}, the DURABLE
|
|
1074
|
-
* fork-lineage cut: a resumed session's constructor seed is its full stored
|
|
1075
|
-
* log, while the inherited count keeps the original fork value — this field is the
|
|
1076
|
-
* in-process construction fact.
|
|
1157
|
+
* The constructor seed length (0 without one), before any marker appended
|
|
1158
|
+
* during construction. Seed events never publish on `session/event`. A
|
|
1159
|
+
* marker appended before the store attaches occupies this seq without
|
|
1160
|
+
* publishing either; otherwise this seq is available for the next append.
|
|
1077
1161
|
*
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
* When this lifecycle appends the marker, it occupies this seq before the
|
|
1086
|
-
* store attaches and therefore does not publish either. Otherwise this seq
|
|
1087
|
-
* holds an ordinary published write.
|
|
1162
|
+
* This in-process offset is not persisted. A fork seed can already contain
|
|
1163
|
+
* the child's inherited marker and synthetic closers, so its child-owned
|
|
1164
|
+
* history starts at {@link inheritedEventCount}, before this offset. A
|
|
1165
|
+
* resumed Session's seed contains its full stored log, while its inherited
|
|
1166
|
+
* count keeps the durable fork cut. Consumers needing complete canonical
|
|
1167
|
+
* history start at seq 0.
|
|
1088
1168
|
*/
|
|
1089
1169
|
firstLiveSeq;
|
|
1090
1170
|
/**
|
|
1171
|
+
* First event produced for this object lifecycle. A new fork includes its
|
|
1172
|
+
* child-owned seed marker and closers; a restored Session starts after its
|
|
1173
|
+
* complete stored prefix. This in-process capture offset is not persisted.
|
|
1174
|
+
*/
|
|
1175
|
+
firstLifecycleSeq;
|
|
1176
|
+
/**
|
|
1091
1177
|
* Create a detached session by validating and snapshotting borrowed seed
|
|
1092
1178
|
* events and storage metadata.
|
|
1093
1179
|
* @param id - session identity.
|
|
@@ -1141,10 +1227,14 @@ var Session = class Session {
|
|
|
1141
1227
|
const inheritedEventCount = SessionLogOffset(suppliedInheritedEventCount ?? 0);
|
|
1142
1228
|
if (!this.header.isSeeded && inheritedEventCount !== 0) throw new Error("unseeded session inherited event count must be 0");
|
|
1143
1229
|
if (inheritedEventCount > this.log.length) throw new Error("session inherited event count exceeds its event log");
|
|
1144
|
-
|
|
1230
|
+
const seedMarker = this.log[inheritedEventCount];
|
|
1231
|
+
const markedSeed = seedMarker?.type === "session/end-seed" && seedMarker.data.inherited === true;
|
|
1232
|
+
if (mode === "snapshot" && this.header.isSeeded && inheritedEventCount !== this.log.length && !markedSeed) throw new Error("seeded session constructor seed must equal its inherited prefix or mark its inherited cut");
|
|
1233
|
+
if (markedSeed && this.log.slice(inheritedEventCount + 1).some((event) => event.type === "session/end-seed" && event.data.inherited === true)) throw new Error("session inherited event count must identify the final inherited marker");
|
|
1145
1234
|
this.inheritedEventCount = inheritedEventCount;
|
|
1146
|
-
|
|
1147
|
-
|
|
1235
|
+
this.firstLifecycleSeq = mode === "snapshot" && this.header.isSeeded ? inheritedEventCount : this.firstLiveSeq;
|
|
1236
|
+
if (seed !== void 0 && mode === "snapshot" && this.header.isSeeded && !markedSeed) this.append("session/end-seed", { inherited: true });
|
|
1237
|
+
else if (seed !== void 0 && !(mode === "snapshot" && this.header.isSeeded) && this.log.at(-1)?.type !== "session/end-seed") this.append("session/end-seed", {});
|
|
1148
1238
|
}
|
|
1149
1239
|
/** Cached immutable full snapshot of the private append-only log. */
|
|
1150
1240
|
eventsSnapshot;
|
|
@@ -1478,7 +1568,7 @@ var SessionStore = class extends Service {
|
|
|
1478
1568
|
const seed = options?.seed;
|
|
1479
1569
|
const meta = options?.meta;
|
|
1480
1570
|
const header = {
|
|
1481
|
-
version:
|
|
1571
|
+
version: 4,
|
|
1482
1572
|
id: sessionId,
|
|
1483
1573
|
createdAt: meta?.createdAt ?? Date.now(),
|
|
1484
1574
|
...meta?.cwd === void 0 ? {} : { cwd: meta.cwd },
|
|
@@ -1650,10 +1740,12 @@ var SessionStore = class extends Service {
|
|
|
1650
1740
|
return [...this.store.values()].map((entry) => entry.session);
|
|
1651
1741
|
}
|
|
1652
1742
|
/**
|
|
1653
|
-
* Create a live child session from
|
|
1743
|
+
* Create a live child session from an exact prefix of a live source.
|
|
1654
1744
|
* `boundary` is an inclusive source event seq; omitted means the source's
|
|
1655
|
-
* current last event.
|
|
1656
|
-
*
|
|
1745
|
+
* current last event. An open tail receives synthetic tool results and
|
|
1746
|
+
* step/turn closers with the forked cause. Closed steps and turns remain
|
|
1747
|
+
* unchanged, including any failed tool calls already missing results.
|
|
1748
|
+
* `inheritedEventCount` counts only copied source events, excluding these closers.
|
|
1657
1749
|
*
|
|
1658
1750
|
* @param source - Live source session object or id.
|
|
1659
1751
|
* @param boundary - Inclusive source event seq to fork through; omitted means
|
|
@@ -1666,10 +1758,12 @@ var SessionStore = class extends Service {
|
|
|
1666
1758
|
fork(source, boundary, childSessionId) {
|
|
1667
1759
|
if (childSessionId !== void 0 && this.get(childSessionId) !== void 0) throw new SessionForkError(`session "${childSessionId}" already exists`, "SESSION_ALREADY_EXISTS");
|
|
1668
1760
|
const liveSource = this._resolveForkSource(source);
|
|
1669
|
-
const
|
|
1761
|
+
const events = liveSource.snapshotEvents();
|
|
1762
|
+
const resolved = this._forkBoundary(liveSource.id, events, boundary);
|
|
1763
|
+
const seed = resolved === void 0 ? [] : buildForkSeed(events, resolved);
|
|
1670
1764
|
return this.create(childSessionId, {
|
|
1671
1765
|
seed,
|
|
1672
|
-
inheritedEventCount: SessionLogOffset(
|
|
1766
|
+
inheritedEventCount: SessionLogOffset(resolved === void 0 ? 0 : resolved + 1),
|
|
1673
1767
|
meta: {
|
|
1674
1768
|
...liveSource.header.cwd !== void 0 ? { cwd: liveSource.header.cwd } : {},
|
|
1675
1769
|
parentSession: liveSource.id,
|
|
@@ -1677,25 +1771,22 @@ var SessionStore = class extends Service {
|
|
|
1677
1771
|
}
|
|
1678
1772
|
});
|
|
1679
1773
|
}
|
|
1680
|
-
|
|
1681
|
-
const lastEvent =
|
|
1774
|
+
_forkBoundary(sessionId, events, requestedBoundary) {
|
|
1775
|
+
const lastEvent = events.at(-1);
|
|
1682
1776
|
let boundary;
|
|
1683
1777
|
if (requestedBoundary !== void 0) boundary = requestedBoundary;
|
|
1684
1778
|
else {
|
|
1685
|
-
if (lastEvent === void 0) return
|
|
1779
|
+
if (lastEvent === void 0) return void 0;
|
|
1686
1780
|
boundary = lastEvent.seq;
|
|
1687
1781
|
}
|
|
1688
|
-
if (!Number.isSafeInteger(boundary) || boundary < 0) throw new SessionForkError(`fork boundary for session "${
|
|
1689
|
-
if (boundary >=
|
|
1782
|
+
if (!Number.isSafeInteger(boundary) || boundary < 0) throw new SessionForkError(`fork boundary for session "${sessionId}" must be a non-negative safe integer, got ${String(boundary)}`, "INVALID_BOUNDARY");
|
|
1783
|
+
if (boundary >= events.length) {
|
|
1690
1784
|
const lastSeq = lastEvent?.seq;
|
|
1691
|
-
throw new SessionForkError(`fork boundary ${boundary} does not exist in session "${
|
|
1785
|
+
throw new SessionForkError(`fork boundary ${boundary} does not exist in session "${sessionId}" (last seq: ${lastSeq ?? "none"})`, "INVALID_BOUNDARY");
|
|
1692
1786
|
}
|
|
1693
|
-
const boundaryEvent =
|
|
1694
|
-
if (boundaryEvent === void 0 || boundaryEvent.seq !== boundary) throw new SessionForkError(`fork boundary ${boundary} does not match a contiguous event seq in session "${
|
|
1695
|
-
|
|
1696
|
-
const lastTurnBoundary = events.findLast((event) => event.type === "turn/start" || event.type === "turn/end");
|
|
1697
|
-
if (lastTurnBoundary?.type === "turn/start") throw new SessionForkError(`fork boundary ${boundary} in session "${session.id}" ends inside open turn ${lastTurnBoundary.data.turn}`, "OPEN_TURN");
|
|
1698
|
-
return events;
|
|
1787
|
+
const boundaryEvent = events[boundary];
|
|
1788
|
+
if (boundaryEvent === void 0 || boundaryEvent.seq !== boundary) throw new SessionForkError(`fork boundary ${boundary} does not match a contiguous event seq in session "${sessionId}"`, "INVALID_BOUNDARY");
|
|
1789
|
+
return boundary;
|
|
1699
1790
|
}
|
|
1700
1791
|
_resolveForkSource(source) {
|
|
1701
1792
|
if (typeof source === "string") {
|
|
@@ -1710,4 +1801,4 @@ var SessionStore = class extends Service {
|
|
|
1710
1801
|
}
|
|
1711
1802
|
};
|
|
1712
1803
|
//#endregion
|
|
1713
|
-
export { KNOWN_SESSION_EVENT_TYPES, SESSION_FORMAT_VERSION, Session, SessionForkError, SessionId, SessionLogOffset, SessionPreparation, SessionSeq, SessionStore, SessionStore as default, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN, adoptSessionEvent, canonicalHeader, decodeSeqRanges, deriveEventMessage, encodeSeqRanges, foldRequestHeader, foldSurface, headerEquals, interruptedTurnClosers, isAppendSurfaceEvent, isReplacementSurfaceEvent, isSurfaceEligibleType, isSurfaceEvent, snapshotSessionEvent };
|
|
1804
|
+
export { KNOWN_SESSION_EVENT_TYPES, SESSION_FORMAT_VERSION, Session, SessionForkError, SessionId, SessionLogOffset, SessionPreparation, SessionSeq, SessionStore, SessionStore as default, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN, adoptSessionEvent, buildForkSeed, canonicalHeader, decodeSeqRanges, deriveEventMessage, encodeSeqRanges, foldRequestHeader, foldSurface, headerEquals, interruptedTurnClosers, isAppendSurfaceEvent, isReplacementSurfaceEvent, isSurfaceEligibleType, isSurfaceEvent, snapshotSessionEvent };
|
package/lib/invariant.js
CHANGED
|
@@ -44,6 +44,9 @@ function validateEvent(trace, event, fail) {
|
|
|
44
44
|
if (event.data.step !== trace.nextStep) fail(`step/start expected step ${trace.nextStep} in turn ${event.data.turn}, got ${event.data.step}`);
|
|
45
45
|
openStep = event.data.step;
|
|
46
46
|
break;
|
|
47
|
+
case "developer/message":
|
|
48
|
+
requireOpenStep(trace, "developer/message", event.data.turn, event.data.step, fail);
|
|
49
|
+
break;
|
|
47
50
|
case "step/end":
|
|
48
51
|
requireOpenStep(trace, "step/end", event.data.turn, event.data.step, fail);
|
|
49
52
|
pendingCalls = { kind: "clear" };
|
|
@@ -70,7 +73,7 @@ function validateEvent(trace, event, fail) {
|
|
|
70
73
|
}
|
|
71
74
|
requireOpenStep(trace, "tool/result", event.data.turn, event.data.step, fail);
|
|
72
75
|
const callId = event.data.message.source.callId;
|
|
73
|
-
const syntheticNotStarted = event.data.message.
|
|
76
|
+
const syntheticNotStarted = event.data.message.isError === true && event.data.error?.code === "TOOL_NOT_STARTED";
|
|
74
77
|
if (!trace.pendingCalls.has(callId) && !syntheticNotStarted) fail(`tool/result for ${callId} with no prior tool/call in this step`);
|
|
75
78
|
pendingCalls = {
|
|
76
79
|
kind: "delete",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fork seed construction over an exact source-event prefix.
|
|
3
|
+
* @module @deepseek-ai/dsh-session/fork
|
|
4
|
+
*/
|
|
5
|
+
import type { SessionEvent, SessionSeq as SessionSeqType } from './types.ts';
|
|
6
|
+
/**
|
|
7
|
+
* Copy an inclusive event prefix, mark its inherited cut, and close its open tail with forked results
|
|
8
|
+
* and step/turn endings. Closed steps and turns are preserved unchanged.
|
|
9
|
+
* The caller validates that the boundary is an existing contiguous event seq;
|
|
10
|
+
* Session construction snapshots the borrowed events before publication.
|
|
11
|
+
*
|
|
12
|
+
* @param events - source log with contiguous seqs from zero.
|
|
13
|
+
* @param boundary - inclusive source event seq the child inherits through.
|
|
14
|
+
* @returns a new array retaining the source event objects, followed by synthetic
|
|
15
|
+
* closers outside the inherited prefix counted by `inheritedEventCount`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildForkSeed(events: readonly SessionEvent[], boundary: SessionSeqType): SessionEvent[];
|
|
18
|
+
//# sourceMappingURL=fork.d.ts.map
|