@deepseek-ai/dsh-session 0.1.3-alpha.2 → 0.1.5-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.
@@ -11,7 +11,7 @@ import { brandString } from '@deepseek-ai/dsh-brand';
11
11
  import { assertNever, deepFreeze, snapshotJsonValue } from '@deepseek-ai/dsh-util-values';
12
12
  import { scopeOf, scopeTarget } from '@deepseek-ai/dsh-scope';
13
13
  import { SESSION_FORMAT_VERSION, SessionLogOffset, SessionSeq } from "./types.js";
14
- import { deriveEventMessage, SurfaceManager } from "./surface.js";
14
+ import { deriveEventMessage, SurfaceManager, validateSessionEventData, validateSurfaceMetadata } from "./surface.js";
15
15
  import { foldRequestHeader } from "./request-header.js";
16
16
  export * from "./types.js";
17
17
  export { SessionPreparation } from "./preparation.js";
@@ -91,13 +91,17 @@ function snapshotSessionHeader(id, source) {
91
91
  * Use {@link snapshotSessionEvent} when exclusive ownership is not guaranteed.
92
92
  * @param event - exclusively owned event imported across a trusted boundary.
93
93
  * @returns the same event object with a validated, deeply frozen message.
94
+ * @throws when event-local surface metadata, request-header fields, or message invariants are invalid; history relations are not checked.
94
95
  */
95
96
  export function adoptSessionEvent(event) {
97
+ validateSessionEventData(event, `session event at seq ${event.seq}`);
98
+ validateSurfaceMetadata(event);
96
99
  assertMessageEventShape(event, `session event at seq ${event.seq}`);
97
100
  switch (event.type) {
98
101
  case 'user/message':
99
102
  deepFreeze(event.data);
100
103
  break;
104
+ case 'system/message':
101
105
  case 'assistant/message':
102
106
  case 'tool/result':
103
107
  deepFreeze(event.data.message);
@@ -118,6 +122,9 @@ export function snapshotSessionEvent(event) {
118
122
  }
119
123
  /** Validate the fixed event envelope after one-pass JSON materialization. */
120
124
  function assertSessionEventEnvelope(value, index) {
125
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
126
+ throw new Error(`seed event at index ${index} has an invalid event envelope`);
127
+ }
121
128
  const event = value;
122
129
  for (const key in event) {
123
130
  switch (key) {
@@ -143,8 +150,10 @@ function assertSessionEventEnvelope(value, index) {
143
150
  || (event['ignorable'] !== undefined && event['ignorable'] !== true)) {
144
151
  throw new Error(`seed event at index ${index} has an invalid event envelope`);
145
152
  }
153
+ validateSessionEventData(event, `seed ${type} at index ${index}`);
146
154
  switch (type) {
147
155
  case 'request/header':
156
+ case 'system/message':
148
157
  case 'user/message':
149
158
  case 'assistant/attempt':
150
159
  case 'assistant/message':
@@ -160,11 +169,8 @@ function assertCurrentLlmShape(event, index) {
160
169
  ? data
161
170
  : undefined;
162
171
  if (event['type'] === 'request/header') {
163
- const header = record?.['header'];
164
- const headerRecord = typeof header === 'object' && header !== null && !Array.isArray(header)
165
- ? header
166
- : undefined;
167
- const config = headerRecord?.['config'];
172
+ const headerRecord = record?.['header'];
173
+ const config = headerRecord['config'];
168
174
  if (!hasProviderModel(config))
169
175
  throw new Error(`seed request/header at index ${index} lacks provider/model`);
170
176
  const configRecord = config;
@@ -173,7 +179,7 @@ function assertCurrentLlmShape(event, index) {
173
179
  && (typeof reasoningEffort !== 'string' || reasoningEffort.length === 0)) {
174
180
  throw new Error(`seed request/header at index ${index} has an invalid reasoningEffort`);
175
181
  }
176
- assertAdapterDefaults(headerRecord?.['adapterDefaults'], configRecord, index);
182
+ assertAdapterDefaults(headerRecord['adapterDefaults'], configRecord, index);
177
183
  const reason = record?.['reason'];
178
184
  if (reason !== 'initial' && reason !== 'resume' && reason !== 'change' && reason !== 'series') {
179
185
  throw new Error(`seed request/header at index ${index} has an invalid reason`);
@@ -187,8 +193,7 @@ function assertCurrentLlmShape(event, index) {
187
193
  assertAssistantSettlementShape(record, type, index);
188
194
  return;
189
195
  }
190
- if (type !== 'user/message' && type !== 'assistant/message'
191
- && type !== 'tool/result')
196
+ if (!isMessageEventType(type))
192
197
  return;
193
198
  assertMessageEventShape(event, `seed ${type} at index ${index}`);
194
199
  if (type === 'assistant/message') {
@@ -221,11 +226,21 @@ function assertAdapterDefaults(value, config, index) {
221
226
  throw new Error(`seed request/header at index ${index} has invalid adapterDefaults`);
222
227
  }
223
228
  }
229
+ /** The four surface event types whose payload carries an identified message. */
230
+ function isMessageEventType(type) {
231
+ return type === 'system/message' || type === 'user/message'
232
+ || type === 'assistant/message' || type === 'tool/result';
233
+ }
234
+ const MESSAGE_ROLE_BY_TYPE = {
235
+ 'system/message': 'system',
236
+ 'user/message': 'user',
237
+ 'assistant/message': 'assistant',
238
+ 'tool/result': 'user',
239
+ };
224
240
  /** Validate only the event-specific invariants needed to safely replay a message. */
225
241
  function assertMessageEventShape(event, subject) {
226
242
  const type = event['type'];
227
- if (type !== 'user/message' && type !== 'assistant/message'
228
- && type !== 'tool/result')
243
+ if (!isMessageEventType(type))
229
244
  return;
230
245
  const data = event['data'];
231
246
  const record = typeof data === 'object' && data !== null
@@ -238,7 +253,7 @@ function assertMessageEventShape(event, subject) {
238
253
  throw new Error(`${subject} lacks an identified message`);
239
254
  }
240
255
  const messageRecord = message;
241
- const expectedRole = type === 'assistant/message' ? 'assistant' : 'user';
256
+ const expectedRole = MESSAGE_ROLE_BY_TYPE[type];
242
257
  if (messageRecord['role'] !== expectedRole) {
243
258
  throw new Error(`${subject} message must have role "${expectedRole}"`);
244
259
  }
@@ -252,6 +267,13 @@ function assertMessageEventShape(event, subject) {
252
267
  throw new Error(`${subject} message has invalid content`);
253
268
  }
254
269
  const sourceRecord = source;
270
+ if (type === 'system/message') {
271
+ if (sourceRecord['kind'] !== 'plugin' || typeof sourceRecord['plugin'] !== 'string'
272
+ || sourceRecord['plugin'] === '') {
273
+ throw new Error(`${subject} message must have plugin source`);
274
+ }
275
+ return;
276
+ }
255
277
  if (type === 'assistant/message') {
256
278
  if (sourceRecord['kind'] !== 'model' || !hasProviderModel(sourceRecord)) {
257
279
  throw new Error(`${subject} message must have model source`);
@@ -519,6 +541,7 @@ export class Session {
519
541
  * (BigInt, function, symbol, undefined, negative zero, non-finite number,
520
542
  * circular reference, sparse array, or an exotic object such as
521
543
  * Map/Set/Date/class instance), or when the candidate violates the
544
+ * request-header empty-field or tool-error consistency rules, or the
522
545
  * canonical surface contract (marker shape and eligibility, unique
523
546
  * earlier source-event references, positional replacement validity, and complete
524
547
  * shadowed-node coverage). One iterative pass reads, validates, and
@@ -554,6 +577,7 @@ export class Session {
554
577
  data: dataSnapshot,
555
578
  ...surfaceMetadataSnapshot,
556
579
  });
580
+ validateSessionEventData(event, `session event "${type}" at seq ${event.seq}`);
557
581
  this.surfaceManager.validateNext(event);
558
582
  if (entry !== undefined)
559
583
  entry.appending = true;
@@ -103,6 +103,10 @@ function validateEvent(trace, event, fail) {
103
103
  pendingCalls = { kind: 'delete', callId };
104
104
  break;
105
105
  }
106
+ case 'system/message': {
107
+ requireOpenStep(trace, 'system/message', event.data.turn, event.data.step, fail);
108
+ break;
109
+ }
106
110
  case 'user/message':
107
111
  break;
108
112
  case 'session/end-seed':
@@ -55,6 +55,7 @@ export const KNOWN_SESSION_EVENT_TYPES = new Set([
55
55
  'step/start',
56
56
  'subagent/descriptor',
57
57
  'subagent/model-selection-policy',
58
+ 'system/message',
58
59
  'team/member',
59
60
  'team/message/delivered',
60
61
  'team/message/queued',
@@ -65,8 +66,8 @@ export const KNOWN_SESSION_EVENT_TYPES = new Set([
65
66
  'tool-workflow/run-end',
66
67
  'tool-workflow/run-start',
67
68
  'tool/call',
68
- 'tool/code-dispatch',
69
- 'tool/code-dispatch-start',
69
+ 'tool/ptc-dispatch',
70
+ 'tool/ptc-dispatch-start',
70
71
  'tool/result',
71
72
  'turn/end',
72
73
  'turn/start',
@@ -8,9 +8,9 @@
8
8
  */
9
9
  import type { EpochHeader, SessionEvent } from './types.ts';
10
10
  /**
11
- * Normalize a header to canonical form: an empty system prompt and empty tool
12
- * list become absent fields, matching how requests are built. Logging, folding,
13
- * and comparison use this one representation.
11
+ * Normalize a header to canonical form: an empty tool list becomes an absent
12
+ * field, matching how requests are built. Logging, folding, and comparison use
13
+ * this one representation.
14
14
  * @param header - the header to normalize (not mutated).
15
15
  * @returns the canonical header.
16
16
  */
@@ -19,7 +19,7 @@ export declare function canonicalHeader(header: EpochHeader): EpochHeader;
19
19
  * Field-wise equality over canonical headers. Tool schemas compare in order.
20
20
  * @param a - one canonical header.
21
21
  * @param b - the other.
22
- * @returns whether config, system, and tools all match.
22
+ * @returns whether config, adapter defaults, and tools all match.
23
23
  */
24
24
  export declare function headerEquals(a: EpochHeader, b: EpochHeader): boolean;
25
25
  /**
@@ -8,9 +8,9 @@
8
8
  */
9
9
  import { callConfigEquals } from '@deepseek-ai/dsh-llm';
10
10
  /**
11
- * Normalize a header to canonical form: an empty system prompt and empty tool
12
- * list become absent fields, matching how requests are built. Logging, folding,
13
- * and comparison use this one representation.
11
+ * Normalize a header to canonical form: an empty tool list becomes an absent
12
+ * field, matching how requests are built. Logging, folding, and comparison use
13
+ * this one representation.
14
14
  * @param header - the header to normalize (not mutated).
15
15
  * @returns the canonical header.
16
16
  */
@@ -21,7 +21,6 @@ export function canonicalHeader(header) {
21
21
  ...adapterDefaults?.reasoningEffort === true || adapterDefaults?.maxTokens === true
22
22
  ? { adapterDefaults }
23
23
  : {},
24
- ...header.system !== undefined && header.system.length > 0 ? { system: header.system } : {},
25
24
  ...header.tools !== undefined && header.tools.length > 0 ? { tools: header.tools } : {},
26
25
  };
27
26
  }
@@ -33,13 +32,12 @@ function sameSchema(a, b) {
33
32
  * Field-wise equality over canonical headers. Tool schemas compare in order.
34
33
  * @param a - one canonical header.
35
34
  * @param b - the other.
36
- * @returns whether config, system, and tools all match.
35
+ * @returns whether config, adapter defaults, and tools all match.
37
36
  */
38
37
  export function headerEquals(a, b) {
39
38
  if (!callConfigEquals(a.config, b.config)
40
39
  || a.adapterDefaults?.reasoningEffort !== b.adapterDefaults?.reasoningEffort
41
- || a.adapterDefaults?.maxTokens !== b.adapterDefaults?.maxTokens
42
- || a.system !== b.system)
40
+ || a.adapterDefaults?.maxTokens !== b.adapterDefaults?.maxTokens)
43
41
  return false;
44
42
  const at = a.tools ?? [];
45
43
  const bt = b.tools ?? [];
@@ -13,7 +13,7 @@ import type { SessionEvent, SurfaceEvent, SurfaceOp } from './types.ts';
13
13
  /**
14
14
  * Whether an event type can join the model-visible surface.
15
15
  * @param type - event type to test.
16
- * @returns true for one of the three message-producing event types.
16
+ * @returns true for one of the four message-producing event types.
17
17
  */
18
18
  export declare function isSurfaceEligibleType(type: string): boolean;
19
19
  /**
@@ -62,6 +62,14 @@ export declare function isReplacementSurfaceEvent(event: SessionEvent): event is
62
62
  * @returns the derived message, or null when the event produces none.
63
63
  */
64
64
  export declare function deriveEventMessage(event: SessionEvent): Message | null;
65
+ /**
66
+ * Reject noncanonical request-header fields and contradictory tool failure metadata.
67
+ * This does not validate complete event payloads or embedded provider streams.
68
+ * @param event - event whose locally related payload fields are inspected.
69
+ * @param subject - event location to include in validation errors.
70
+ * @throws when request data/header is not an object, optional header fields are empty, or tool failure metadata contradicts its message.
71
+ */
72
+ export declare function validateSessionEventData(event: Pick<SessionEvent, 'type' | 'data'>, subject: string): void;
65
73
  /** One replacement operation observed while folding a session surface. */
66
74
  export interface SurfaceFoldReplacement {
67
75
  /** Seq of the event that replaced the prior surface range. */
@@ -87,6 +95,14 @@ export interface SessionSurface {
87
95
  /** Monotonic count of committed positional replacements. */
88
96
  readonly replaceGeneration: number;
89
97
  }
98
+ /**
99
+ * Validate one event's surface metadata without checking membership in a log or surface.
100
+ * @param event - event whose marker and source sequence values are inspected.
101
+ * Unknown ignorable records retain opaque metadata and never change the surface.
102
+ * @returns the validated operation, or undefined for a log-only or unknown ignorable event.
103
+ * @throws when metadata violates event-local eligibility, marker, or source-sequence rules.
104
+ */
105
+ export declare function validateSurfaceMetadata(event: SessionEvent): SurfaceOp | undefined;
90
106
  /**
91
107
  * Replay a complete session log through the canonical surface fold.
92
108
  * @param events - session events in contiguous seq order.
@@ -8,8 +8,10 @@
8
8
  * @module @deepseek-ai/dsh-session/surface
9
9
  */
10
10
  import { SessionLogOffset, SessionSeq } from "./types.js";
11
+ import { KNOWN_SESSION_EVENT_TYPES } from "./known-event-types.js";
11
12
  /** Runtime counterpart of the message-producing event union. */
12
13
  const SURFACE_EVENT_TYPES = new Set([
14
+ 'system/message',
13
15
  'user/message',
14
16
  'assistant/message',
15
17
  'tool/result',
@@ -17,7 +19,7 @@ const SURFACE_EVENT_TYPES = new Set([
17
19
  /**
18
20
  * Whether an event type can join the model-visible surface.
19
21
  * @param type - event type to test.
20
- * @returns true for one of the three message-producing event types.
22
+ * @returns true for one of the four message-producing event types.
21
23
  */
22
24
  export function isSurfaceEligibleType(type) {
23
25
  return SURFACE_EVENT_TYPES.has(type);
@@ -30,7 +32,8 @@ export function isSurfaceEligibleType(type) {
30
32
  export function isSurfaceEvent(event) {
31
33
  if (!SURFACE_EVENT_TYPES.has(event.type))
32
34
  return false;
33
- return event.surfaceOp !== undefined;
35
+ const candidate = event;
36
+ return candidate.surfaceOp !== undefined;
34
37
  }
35
38
  /**
36
39
  * Narrow an event to an append-origin surface event: one that entered the
@@ -85,10 +88,13 @@ export function deriveEventMessage(event) {
85
88
  case 'user/message': {
86
89
  return event.data;
87
90
  }
91
+ // An empty-content message projects to no wire message. For
92
+ // system/message the node records "no system prompt" while keeping its
93
+ // surface position; for assistant/message the event exists only to host a
94
+ // max-tokens step's usage and must not inject a content-less assistant
95
+ // turn into the provider transcript.
96
+ case 'system/message':
88
97
  case 'assistant/message': {
89
- // Skip an empty-content assistant/message: it exists only to host a
90
- // max-tokens step's usage and must not inject a content-less assistant
91
- // turn into the provider transcript.
92
98
  if (event.data.message.content.length === 0)
93
99
  return null;
94
100
  return event.data.message;
@@ -102,6 +108,48 @@ export function deriveEventMessage(event) {
102
108
  return null;
103
109
  }
104
110
  }
111
+ /** Whether a payload field is a JSON object rather than an array or scalar. */
112
+ function isRecord(value) {
113
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
114
+ }
115
+ /**
116
+ * Reject noncanonical request-header fields and contradictory tool failure metadata.
117
+ * This does not validate complete event payloads or embedded provider streams.
118
+ * @param event - event whose locally related payload fields are inspected.
119
+ * @param subject - event location to include in validation errors.
120
+ * @throws when request data/header is not an object, optional header fields are empty, or tool failure metadata contradicts its message.
121
+ */
122
+ export function validateSessionEventData(event, subject) {
123
+ const data = event.data;
124
+ if (event.type === 'request/header') {
125
+ if (!isRecord(data))
126
+ throw new Error(`${subject} data must be an object`);
127
+ const header = data['header'];
128
+ if (!isRecord(header))
129
+ throw new Error(`${subject} header must be an object`);
130
+ if (Object.hasOwn(header, 'system'))
131
+ throw new Error(`${subject} must omit header.system; use system/message`);
132
+ if (Array.isArray(header['tools']) && header['tools'].length === 0) {
133
+ throw new Error(`${subject} must omit empty tools`);
134
+ }
135
+ const defaults = header['adapterDefaults'];
136
+ if (isRecord(defaults) && Object.keys(defaults).length === 0) {
137
+ throw new Error(`${subject} must omit empty adapterDefaults`);
138
+ }
139
+ }
140
+ else if (event.type === 'tool/result') {
141
+ if (!isRecord(data))
142
+ throw new Error(`${subject} data must be an object`);
143
+ if (data['error'] === undefined)
144
+ return;
145
+ const message = data['message'];
146
+ const content = isRecord(message) ? message['content'] : undefined;
147
+ const block = Array.isArray(content) ? content[0] : undefined;
148
+ if (!isRecord(block) || block['isError'] !== true) {
149
+ throw new Error(`${subject} error requires message content[0].isError === true`);
150
+ }
151
+ }
152
+ }
105
153
  /** Create an empty surface fold state. */
106
154
  function createFoldState() {
107
155
  return { nodes: [], replaceGeneration: 0 };
@@ -118,16 +166,19 @@ function isReplaceOp(value) {
118
166
  const op = value;
119
167
  return Object.keys(op).length === 3
120
168
  && Object.hasOwn(op, 'op')
121
- && Object.hasOwn(op, 'start')
122
- && Object.hasOwn(op, 'end')
169
+ && Object.hasOwn(op, 'startSeq')
170
+ && Object.hasOwn(op, 'endSeq')
123
171
  && op['op'] === 'replace'
124
- && isEventSeq(op['start'])
125
- && isEventSeq(op['end']);
172
+ && isEventSeq(op['startSeq'])
173
+ && isEventSeq(op['endSeq']);
126
174
  }
127
175
  /** Validate event-local surface eligibility and return its operation. */
128
176
  function surfaceOpOf(event) {
129
177
  const raw = event;
130
178
  if (!isSurfaceEligibleType(event.type)) {
179
+ // Unknown ignorable records retain opaque metadata without affecting history.
180
+ if (!KNOWN_SESSION_EVENT_TYPES.has(event.type) && event.ignorable === true)
181
+ return;
131
182
  if (raw.surfaceOp !== undefined) {
132
183
  throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry surfaceOp`);
133
184
  }
@@ -185,18 +236,35 @@ function assertProvenance(event, shadowedSeqs) {
185
236
  throw new Error(`surface replace: sourceEventSeqs must include every shadowed surface node; missing ${missing.join(', ')}`);
186
237
  }
187
238
  }
239
+ /**
240
+ * Validate one event's surface metadata without checking membership in a log or surface.
241
+ * @param event - event whose marker and source sequence values are inspected.
242
+ * Unknown ignorable records retain opaque metadata and never change the surface.
243
+ * @returns the validated operation, or undefined for a log-only or unknown ignorable event.
244
+ * @throws when metadata violates event-local eligibility, marker, or source-sequence rules.
245
+ */
246
+ export function validateSurfaceMetadata(event) {
247
+ const op = surfaceOpOf(event);
248
+ if (op !== undefined && op !== 'append'
249
+ && (op.startSeq >= event.seq || op.endSeq >= event.seq)) {
250
+ throw new Error(`surface replace at seq ${event.seq}: startSeq and endSeq must reference earlier events`);
251
+ }
252
+ if (op !== undefined)
253
+ assertProvenance(event, []);
254
+ return op;
255
+ }
188
256
  /** Locate one replacement range without mutating the current fold state. */
189
257
  function replacementRange(state, op) {
190
- const startIdx = state.nodes.indexOf(op.start);
258
+ const startIdx = state.nodes.indexOf(op.startSeq);
191
259
  if (startIdx === -1) {
192
- throw new Error(`surface replace: start seq ${op.start} not found in surface`);
260
+ throw new Error(`surface replace: start seq ${op.startSeq} not found in surface`);
193
261
  }
194
- const endIdx = state.nodes.indexOf(op.end);
262
+ const endIdx = state.nodes.indexOf(op.endSeq);
195
263
  if (endIdx === -1) {
196
- throw new Error(`surface replace: end seq ${op.end} not found in surface`);
264
+ throw new Error(`surface replace: end seq ${op.endSeq} not found in surface`);
197
265
  }
198
266
  if (startIdx > endIdx) {
199
- throw new Error(`surface replace: start seq ${op.start} (index ${startIdx}) is after end seq ${op.end} (index ${endIdx})`);
267
+ throw new Error(`surface replace: start seq ${op.startSeq} (index ${startIdx}) is after end seq ${op.endSeq} (index ${endIdx})`);
200
268
  }
201
269
  return {
202
270
  startIdx,
@@ -254,26 +322,42 @@ function assertToolResultRewrite(event, shadowedSeqs, events, baseSeq) {
254
322
  }
255
323
  }
256
324
  }
325
+ /**
326
+ * Protect the system prompt at surface node 0. A replacement covering node 0
327
+ * while that node is a `system/message` must itself be a `system/message` over
328
+ * exactly that node; later system nodes carry no protection and a compaction
329
+ * range may shadow them.
330
+ */
331
+ function assertSystemHeadRewrite(event, state, startIdx, shadowedSeqs, events, baseSeq) {
332
+ if (startIdx !== 0)
333
+ return;
334
+ const head = events[state.nodes[0] - baseSeq];
335
+ if (head?.type !== 'system/message')
336
+ return;
337
+ if (event.type !== 'system/message' || shadowedSeqs.length !== 1) {
338
+ throw new Error('surface replace: node 0 holds the system prompt and may be rewritten only by a system/message over exactly that node');
339
+ }
340
+ }
257
341
  /** Validate one event at its replay boundary and prepare its atomic fold transition. */
258
342
  function planSurfaceEvent(state, event, expectedSeq, events, baseSeq) {
259
343
  if (event.seq !== expectedSeq) {
260
344
  throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`);
261
345
  }
262
- const surfaceOp = surfaceOpOf(event);
346
+ const surfaceOp = validateSurfaceMetadata(event);
263
347
  if (surfaceOp === undefined)
264
348
  return;
265
349
  if (surfaceOp === 'append') {
266
- assertProvenance(event, []);
267
350
  return { kind: 'append', seq: event.seq };
268
351
  }
269
352
  const range = replacementRange(state, surfaceOp);
270
353
  assertProvenance(event, range.shadowedSeqs);
271
354
  assertToolResultRewrite(event, range.shadowedSeqs, events, baseSeq);
355
+ assertSystemHeadRewrite(event, state, range.startIdx, range.shadowedSeqs, events, baseSeq);
272
356
  return {
273
357
  kind: 'replace',
274
358
  seq: event.seq,
275
- start: surfaceOp.start,
276
- end: surfaceOp.end,
359
+ start: surfaceOp.startSeq,
360
+ end: surfaceOp.endSeq,
277
361
  ...range,
278
362
  };
279
363
  }
@@ -1,5 +1,5 @@
1
1
  import { type Branded, type BrandedNumber } from '@deepseek-ai/dsh-brand';
2
- import type { AssistantMessage, AssistantStreamRecord, ToolCallId, LlmCallConfig, LlmCallConfigAdapterDefaults, LlmFailure, TokenUsage, ToolResultMessage, ToolSchema, UserMessage } from '@deepseek-ai/dsh-llm';
2
+ import type { AssistantMessage, AssistantStreamRecord, ToolCallId, LlmCallConfig, LlmCallConfigAdapterDefaults, LlmFailure, SystemMessage, SystemPromptUpdate, TokenUsage, ToolResultMessage, ToolSchema, UserMessage } from '@deepseek-ai/dsh-llm';
3
3
  import type { JsonValue } from '@deepseek-ai/dsh-util-values';
4
4
  /** Identifies one session in the store (and its persistence artifacts). */
5
5
  export type SessionId = Branded<'SessionId'>;
@@ -51,7 +51,7 @@ export type OptionalSessionSeq = SessionSeq | null;
51
51
  * immutable prior-generation, and current fast-path rules are recorded in
52
52
  * `.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md`.
53
53
  */
54
- export declare const SESSION_FORMAT_VERSION = 2;
54
+ export declare const SESSION_FORMAT_VERSION = 3;
55
55
  /**
56
56
  * Immutable validated storage metadata, kept outside the conversation event log.
57
57
  */
@@ -200,8 +200,9 @@ export interface TurnEndReasonMap {
200
200
  /** The union over {@link TurnEndReasonMap} — why a turn ended; plugins extend it by merging variants into the map. */
201
201
  export type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap];
202
202
  /**
203
- * Logged request state outside derived history: call config, system prompt, and
204
- * tools. The latest full `request/header` snapshot reconstructs it; canonical
203
+ * Logged request state outside derived history: call config and tools. The
204
+ * system prompt is derived history — surface node 0, a `system/message` event.
205
+ * The latest full `request/header` snapshot reconstructs the header; canonical
205
206
  * empty optional fields are absent.
206
207
  */
207
208
  export interface EpochHeader {
@@ -209,8 +210,6 @@ export interface EpochHeader {
209
210
  config: LlmCallConfig;
210
211
  /** Effective config fields materialized from the exact adapter rather than proposed by a caller. */
211
212
  adapterDefaults?: LlmCallConfigAdapterDefaults;
212
- /** Rendered system prompt text; absent for a system-less request. */
213
- system?: string;
214
213
  /** Assembled tool schemas; absent for a tool-less request. */
215
214
  tools?: ToolSchema[];
216
215
  }
@@ -222,6 +221,8 @@ export interface RequestContext {
222
221
  model: string;
223
222
  /** Maximum combined request and response context in tokens, when advertised. */
224
223
  contextWindow?: number;
224
+ /** `'in-history'` when the route reads the latest `system` message at any position as the effective system prompt. */
225
+ systemPromptUpdate?: SystemPromptUpdate;
225
226
  }
226
227
  /**
227
228
  * Why a `request/header` snapshot was appended: `'initial'` — the log's first
@@ -278,6 +279,23 @@ export interface SessionEventMap {
278
279
  * project their `content` verbatim; `source` tells them apart.
279
280
  */
280
281
  'user/message': UserMessage;
282
+ /**
283
+ * The rendered system prompt on the model-visible surface. The loop appends
284
+ * the first one as surface node 0 before the step's first `user/message`.
285
+ * A prepared in-history route can append nonempty changes in a continuing
286
+ * series. An incapable route or new series normalizes text to the first system
287
+ * node. Normalization empties nonempty later nodes, then rewrites the head if
288
+ * needed, through logged per-node replacements. An empty rendering always
289
+ * clears all active system nodes, leaving no older instructions model-visible.
290
+ * Empty later nodes are dormant and project to no message; an empty head with
291
+ * no active later node records "no system prompt". Restored nonempty text follows
292
+ * the same route and series rule; empty nodes never restore older text.
293
+ */
294
+ 'system/message': {
295
+ turn: number;
296
+ step: number;
297
+ message: SystemMessage;
298
+ };
281
299
  /**
282
300
  * Assembled assistant message for one step (derived history uses this).
283
301
  * Carries the step's `usage` when the adapter reported token accounting, so
@@ -334,6 +352,7 @@ export interface SessionEventMap {
334
352
  turn: number;
335
353
  step: number;
336
354
  message: ToolResultMessage;
355
+ /** Optional failure identity; allowed only when the tool-result block has `isError: true`. */
337
356
  error?: {
338
357
  name: string;
339
358
  code: string;
@@ -351,8 +370,10 @@ export interface SessionEventMap {
351
370
  startsSeries?: true;
352
371
  };
353
372
  /**
354
- * Route metadata for the next request, logged only when the route or capacity
355
- * changes. It does not participate in request reconstruction or header equality.
373
+ * Route metadata for the next request, logged only when the route, capacity,
374
+ * or system prompt update mode changes. It does not participate in request
375
+ * reconstruction or header equality. Prompt admission uses the bound prepared
376
+ * call's capability, not this snapshot from an earlier request.
356
377
  */
357
378
  'request/context': RequestContext;
358
379
  /**
@@ -386,39 +407,29 @@ export type SessionEventType = keyof SessionEventMap;
386
407
  /**
387
408
  * The subset of {@link SessionEventType} values whose events produce LLM
388
409
  * messages and are eligible to appear on the ordered surface. Only these
389
- * event types may carry {@link SurfaceOp}; user and tool events may also cite
410
+ * event types may carry {@link SurfaceOp}; system, user, and tool events may also cite
390
411
  * earlier sources through {@link SessionEvent.sourceEventSeqs}.
391
412
  */
392
- export type SurfaceEventType = 'user/message' | 'assistant/message' | 'tool/result';
393
- /**
394
- * A {@link SessionEvent} that is **on** the ordered surface — its
395
- * `surfaceOp` is guaranteed present (mandatory), narrowed from a
396
- * surface-eligible {@link SessionEvent} by checking both `type` and
397
- * `surfaceOp` at runtime.
398
- *
399
- * Use the `isSurfaceEvent` type guard (in `surface.ts`) to narrow a
400
- * `SessionEvent` to this type.
401
- */
402
- export type SurfaceEvent = SessionEvent<SurfaceEventType> & {
403
- surfaceOp: SurfaceOp;
404
- };
413
+ export type SurfaceEventType = 'system/message' | 'user/message' | 'assistant/message' | 'tool/result';
414
+ /** A message-producing event carrying its required surface operation. */
415
+ export type SurfaceEvent = SessionEvent<SurfaceEventType>;
405
416
  /**
406
417
  * How a session event entered the ordered surface. Only valid on
407
418
  * {@link SurfaceEventType} events.
408
419
  *
409
420
  * - `'append'`: added to the tail — normal path for user/assistant/tool
410
421
  * messages.
411
- * - `{ op: 'replace', start, end }`: replaces surface nodes from `start`
412
- * (inclusive) through `end` (inclusive) with this node. Both must exist as
413
- * surface nodes in the current surface. `start === end` replaces a single
422
+ * - `{ op: 'replace', startSeq, endSeq }`: replaces surface nodes from `startSeq`
423
+ * (inclusive) through `endSeq` (inclusive) with this node. Both must exist as
424
+ * surface nodes in the current surface. `startSeq === endSeq` replaces a single
414
425
  * node. The node's {@link SessionEvent.sourceEventSeqs} must include every
415
426
  * shadowed surface node. Used by compaction; any surface-replacing producer
416
427
  * may use it.
417
428
  */
418
429
  export type SurfaceOp = 'append' | {
419
430
  op: 'replace';
420
- start: SessionSeq;
421
- end: SessionSeq;
431
+ startSeq: SessionSeq;
432
+ endSeq: SessionSeq;
422
433
  };
423
434
  /**
424
435
  * Surface placement and cited source-event seqs for {@link Session.append}. Required on
@@ -427,7 +438,7 @@ export type SurfaceOp = 'append' | {
427
438
  export type SurfaceIntent<T extends SurfaceEventType = SurfaceEventType> = {
428
439
  surfaceOp: SurfaceOp;
429
440
  } & (T extends 'assistant/message' ? {
430
- /** V2 Assistant messages embed their provider stream instead of citing source events. */
441
+ /** Assistant messages embed their provider stream instead of citing source events. */
431
442
  sourceEventSeqs?: never;
432
443
  } : {
433
444
  /** Complete non-empty set of known earlier source-event seqs. */
@@ -440,7 +451,7 @@ export type SurfaceIntent<T extends SurfaceEventType = SurfaceEventType> = {
440
451
  * unions), so `switch (event.type)` narrows `event.data` without casts.
441
452
  *
442
453
  * The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
443
- * they only exist on {@link SurfaceEventType} variants (`user/message`,
454
+ * they only exist on {@link SurfaceEventType} variants (`system/message`, `user/message`,
444
455
  * `assistant/message`, `tool/result`).
445
456
  * Non-surface events (boundary markers, attempts, errors) never carry
446
457
  * surface metadata — the compiler enforces this at `Session.append()`
@@ -465,16 +476,10 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
465
476
  * inconvenience) rather than silently resuming a gutted session.
466
477
  */
467
478
  ignorable?: true;
468
- } & (K extends SurfaceEventType ? {
469
- /**
470
- * Seq numbers of earlier events that this event cites as sources, such as
471
- * the surface nodes shadowed by a compaction replacement. A v2
472
- * `assistant/message` embeds its provider stream and cannot carry this field.
473
- */
474
- sourceEventSeqs?: SessionSeq[];
475
- /** How this event entered the surface; absent for non-surface events. */
476
- surfaceOp?: SurfaceOp;
477
- } : object);
479
+ } & (K extends SurfaceEventType ? SurfaceIntent<K> : {
480
+ surfaceOp?: never;
481
+ sourceEventSeqs?: never;
482
+ });
478
483
  }[T];
479
484
  declare module '@deepseek-ai/dsh-typert-protocol' {
480
485
  interface RemoteErrorDetailsMap {
@@ -51,5 +51,5 @@ export function SessionLogOffset(value) {
51
51
  * immutable prior-generation, and current fast-path rules are recorded in
52
52
  * `.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md`.
53
53
  */
54
- export const SESSION_FORMAT_VERSION = 2;
54
+ export const SESSION_FORMAT_VERSION = 3;
55
55
  //# sourceMappingURL=types.js.map