@deepseek-ai/dsh-session 0.1.6-alpha.2 → 0.1.7-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ import { KNOWN_SESSION_EVENT_TYPES, MESSAGE_PROJECTION_EVENT_TYPES } from "./kno
12
12
  /** Runtime counterpart of the message-producing event union. */
13
13
  const SURFACE_EVENT_TYPES = new Set([
14
14
  'system/message',
15
+ 'developer/message',
15
16
  'user/message',
16
17
  'assistant/message',
17
18
  'tool/result',
@@ -19,7 +20,7 @@ const SURFACE_EVENT_TYPES = new Set([
19
20
  /**
20
21
  * Whether an event type can join the model-visible surface.
21
22
  * @param type - event type to test.
22
- * @returns true for one of the four message-producing event types.
23
+ * @returns true for one of the message-producing event types.
23
24
  */
24
25
  export function isSurfaceEligibleType(type) {
25
26
  return SURFACE_EVENT_TYPES.has(type);
@@ -62,7 +63,7 @@ export function isReplacementSurfaceEvent(event) {
62
63
  /**
63
64
  * Project a single event into the LLM message it derives to, or null when it
64
65
  * produces none — a non-surface event (attempt, boundary, log-only record) or an
65
- * empty-content assistant/message (which exists only to host usage). A caller
66
+ * empty-content system, developer, or assistant message. A caller
66
67
  * reconstructing model input supplies the same prefix's `projectedMessages`
67
68
  * from {@link foldSurface}; without that map this function reads original
68
69
  * event content. Session instance methods apply the live projection. Messages
@@ -90,12 +91,11 @@ export function deriveEventMessage(event, projectedMessages) {
90
91
  case 'user/message': {
91
92
  return event.data;
92
93
  }
93
- // An empty-content message projects to no wire message. For
94
- // system/message the node records "no system prompt" while keeping its
95
- // surface position; for assistant/message the event exists only to host a
96
- // max-tokens step's usage and must not inject a content-less assistant
97
- // turn into the provider transcript.
94
+ // Empty system and developer nodes retain their surface positions without
95
+ // adding wire messages. An empty assistant event hosts a max-tokens step's
96
+ // usage and must not inject a content-less turn into the provider transcript.
98
97
  case 'system/message':
98
+ case 'developer/message':
99
99
  case 'assistant/message': {
100
100
  if (event.data.message.content.length === 0)
101
101
  return null;
@@ -115,14 +115,45 @@ function isRecord(value) {
115
115
  return typeof value === 'object' && value !== null && !Array.isArray(value);
116
116
  }
117
117
  /**
118
- * Reject noncanonical request-header fields and contradictory tool failure metadata.
118
+ * Reject noncanonical request-header fields, developer roles/content, and contradictory tool failure metadata.
119
119
  * This does not validate complete event payloads or embedded provider streams.
120
120
  * @param event - event whose locally related payload fields are inspected.
121
121
  * @param subject - event location to include in validation errors.
122
- * @throws when request data/header is not an object, optional header fields are empty, or tool failure metadata contradicts its message.
122
+ * @throws when request-header fields, developer roles/content, or tool failure metadata are invalid.
123
123
  */
124
124
  export function validateSessionEventData(event, subject) {
125
125
  const data = event.data;
126
+ if (SURFACE_EVENT_TYPES.has(event.type) && isRecord(data)) {
127
+ const message = event.type === 'user/message' ? data : data['message'];
128
+ if (isRecord(message)) {
129
+ if ((event.type === 'developer/message') !== (message['role'] === 'developer')) {
130
+ throw new Error(`${subject} developer/message and developer role must occur together`);
131
+ }
132
+ if (message['role'] !== 'developer' && Array.isArray(message['content'])
133
+ && message['content'].some((block) => isRecord(block)
134
+ && (block['type'] === 'tool-addition' || block['type'] === 'tool-removal'))) {
135
+ throw new Error(`${subject} tool-change blocks require developer role`);
136
+ }
137
+ if (event.type === 'developer/message' && Array.isArray(message['content'])) {
138
+ let hasAdditions = false;
139
+ for (const block of message['content']) {
140
+ if (!isRecord(block) || (block['type'] !== 'tool-addition' && block['type'] !== 'tool-removal'))
141
+ continue;
142
+ if (typeof block['toolName'] !== 'string' || block['toolName'].length === 0) {
143
+ throw new Error(`${subject} ${block['type']} requires a nonempty toolName`);
144
+ }
145
+ if (block['type'] === 'tool-addition') {
146
+ hasAdditions = true;
147
+ if (Object.hasOwn(block, 'tool'))
148
+ throw new Error(`${subject} tool-addition must omit inline tool definitions`);
149
+ }
150
+ }
151
+ if (hasAdditions ? !isEventSeq(data['headerSeq']) : Object.hasOwn(data, 'headerSeq')) {
152
+ throw new Error(`${subject} requires headerSeq exactly when tool additions are present`);
153
+ }
154
+ }
155
+ }
156
+ }
126
157
  if (event.type === 'request/header') {
127
158
  if (!isRecord(data))
128
159
  throw new Error(`${subject} data must be an object`);
@@ -145,10 +176,8 @@ export function validateSessionEventData(event, subject) {
145
176
  if (data['error'] === undefined)
146
177
  return;
147
178
  const message = data['message'];
148
- const content = isRecord(message) ? message['content'] : undefined;
149
- const block = Array.isArray(content) ? content[0] : undefined;
150
- if (!isRecord(block) || block['isError'] !== true) {
151
- throw new Error(`${subject} error requires message content[0].isError === true`);
179
+ if (!isRecord(message) || message['isError'] !== true) {
180
+ throw new Error(`${subject} error requires message.isError === true`);
152
181
  }
153
182
  }
154
183
  }
@@ -238,6 +267,34 @@ function assertSourceEventReferences(event, shadowedSeqs) {
238
267
  throw new Error(`surface replace: sourceEventSeqs must include every shadowed surface node; missing ${missing.join(', ')}`);
239
268
  }
240
269
  }
270
+ /** Resolve tool additions against their immutable historical request header. */
271
+ function assertDeveloperHeader(event, events, baseSeq) {
272
+ if (event.type !== 'developer/message')
273
+ return;
274
+ validateSessionEventData(event, `developer/message at seq ${event.seq}`);
275
+ if (event.data.headerSeq === undefined)
276
+ return;
277
+ const headerSeq = event.data.headerSeq;
278
+ const headerEvent = events[headerSeq - baseSeq];
279
+ if (headerSeq >= event.seq || headerEvent?.type !== 'request/header') {
280
+ throw new Error('developer/message headerSeq must reference an earlier request/header');
281
+ }
282
+ for (const block of event.data.message.content) {
283
+ if (block.type !== 'tool-addition')
284
+ continue;
285
+ const definitions = headerEvent.data.header.tools?.filter(tool => tool.name === block.toolName) ?? [];
286
+ if (definitions.length !== 1) {
287
+ throw new Error(`developer/message tool-addition "${block.toolName}" must name exactly one tool in headerSeq ${headerSeq}`);
288
+ }
289
+ const definition = definitions[0];
290
+ if (typeof definition.description !== 'string' || !isRecord(definition.parameters)) {
291
+ throw new Error(`developer/message tool-addition "${block.toolName}" requires a complete tool definition in headerSeq ${headerSeq}`);
292
+ }
293
+ if (Object.hasOwn(definition, 'deferLoading') && definition.deferLoading !== true) {
294
+ throw new Error('developer/message referenced tool deferLoading must be true when present');
295
+ }
296
+ }
297
+ }
241
298
  /**
242
299
  * Validate one event's surface metadata without checking membership in a log or surface.
243
300
  * @param event - event whose marker and source sequence values are inspected.
@@ -309,15 +366,13 @@ function assertToolResultRewrite(event, shadowedSeqs, events, baseSeq) {
309
366
  }
310
367
  const originalRest = { ...original.data };
311
368
  const replacementRest = { ...event.data };
312
- const originalResult = original.data.message.content[0];
313
- const replacementResult = event.data.message.content[0];
314
369
  originalRest['message'] = {
315
370
  ...original.data.message,
316
- content: [{ ...originalResult, content: null }],
371
+ content: null,
317
372
  };
318
373
  replacementRest['message'] = {
319
374
  ...event.data.message,
320
- content: [{ ...replacementResult, content: null }],
375
+ content: null,
321
376
  };
322
377
  if (!isDeepEqualJson(originalRest, replacementRest)) {
323
378
  throw new Error('tool/result surface replacement may change only content');
@@ -346,6 +401,7 @@ function planSurfaceEvent(state, event, expectedSeq, events, baseSeq, projection
346
401
  throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`);
347
402
  }
348
403
  const surfaceOp = validateSurfaceMetadata(event);
404
+ assertDeveloperHeader(event, events, baseSeq);
349
405
  const projection = projections.find(item => item.type === event.type);
350
406
  if (projection !== undefined) {
351
407
  return { kind: 'project', projection, messages: projection.project(event, {
@@ -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, SystemMessage, SystemPromptUpdate, TokenUsage, ToolResultMessage, ToolSchema, UserMessage } from '@deepseek-ai/dsh-llm';
2
+ import type { AssistantMessage, DeveloperMessage, 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 = 3;
54
+ export declare const SESSION_FORMAT_VERSION = 4;
55
55
  /**
56
56
  * Immutable validated storage metadata, kept outside the conversation event log.
57
57
  */
@@ -103,8 +103,8 @@ export interface CreateSessionOptions {
103
103
  readonly seed?: readonly SessionEvent[];
104
104
  /**
105
105
  * Exact fork-inherited prefix length when `meta.isSeeded` is true. The
106
- * constructor seed is exactly this inherited prefix; the constructor
107
- * appends the child-owned tagged marker at the cut.
106
+ * constructor appends the child-owned tagged marker at the cut unless
107
+ * the seed already includes it followed by child-owned fork closers.
108
108
  */
109
109
  readonly inheritedEventCount?: SessionLogOffset;
110
110
  /**
@@ -196,6 +196,15 @@ export interface TurnEndReasonMap {
196
196
  interrupted: {
197
197
  kind: 'interrupted';
198
198
  };
199
+ /**
200
+ * Fork-seed construction closed a turn that was still open at the fork
201
+ * boundary in the source session. Only fork seeds carry this marker — the
202
+ * loop never emits it — and the source events before the boundary remain
203
+ * intact in the child.
204
+ */
205
+ forked: {
206
+ kind: 'forked';
207
+ };
199
208
  }
200
209
  /** The union over {@link TurnEndReasonMap} — why a turn ended; plugins extend it by merging variants into the map. */
201
210
  export type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap];
@@ -212,6 +221,10 @@ export interface EpochHeader {
212
221
  adapterDefaults?: LlmCallConfigAdapterDefaults;
213
222
  /** Assembled tool schemas; absent for a tool-less request. */
214
223
  tools?: ToolSchema[];
224
+ /** Retired request text; system prompts belong to system/message events.
225
+ * @persistenceReserved
226
+ */
227
+ system?: never;
215
228
  }
216
229
  /** Registration-bound metadata for one resolved model route. */
217
230
  export interface RequestContext {
@@ -279,6 +292,14 @@ export interface SessionEventMap {
279
292
  * project their `content` verbatim; `source` tells them apart.
280
293
  */
281
294
  'user/message': UserMessage;
295
+ /** An incremental agent session change admitted at the named turn and step. */
296
+ 'developer/message': {
297
+ turn: number;
298
+ step: number;
299
+ message: DeveloperMessage;
300
+ /** Earlier request/header defining every tool addition; required exactly when additions are present. */
301
+ headerSeq?: SessionSeq;
302
+ };
282
303
  /**
283
304
  * The rendered system prompt on the model-visible surface. The loop appends
284
305
  * the first one as surface node 0 before the step's first `user/message`.
@@ -356,7 +377,7 @@ export interface SessionEventMap {
356
377
  message: ToolResultMessage;
357
378
  /**
358
379
  * Optional failure identity and raw user-facing reason, outside model content;
359
- * allowed only when the tool-result block has `isError: true`.
380
+ * allowed only when the message has `isError: true`.
360
381
  */
361
382
  error?: {
362
383
  name: string;
@@ -383,19 +404,21 @@ export interface SessionEventMap {
383
404
  */
384
405
  'request/context': RequestContext;
385
406
  /**
386
- * Marks the end of a constructor seed. Events before it have smaller seq
387
- * values and came from the seed (resume, fork, or replay); this lifecycle
388
- * produced none of them. This log-only event is the durable projection of
389
- * {@link Session.firstLiveSeq}.
407
+ * Separates inherited or restored history from later lifecycle-owned work.
408
+ * This log-only marker need not be at {@link Session.firstLiveSeq}: a fork
409
+ * seed can already contain its tagged marker and child-owned synthetic
410
+ * closers before construction.
390
411
  *
391
412
  * A fresh fork child owns one `{ inherited: true }` marker at its exact
392
413
  * inherited-prefix cut, even when that prefix ends in an ancestor marker.
393
- * The last tagged marker is the current Session's cut; untagged markers keep
394
- * ordinary restore and replay lifecycle boundaries.
414
+ * `buildForkSeed` appends that marker before any synthetic closers; the
415
+ * `Session` constructor supplies it when given only the inherited prefix.
416
+ * The last tagged marker is the current Session's cut; untagged markers
417
+ * keep ordinary restore and replay lifecycle boundaries.
395
418
  *
396
- * `Session`'s constructor is the only legitimate writer. The invariant
397
- * companion deliberately constrains nothing here, so a plugin appending one
398
- * would silently classify every live bracket before it as seed history.
419
+ * Only the `Session` constructor and `buildForkSeed` may create this marker.
420
+ * The invariant companion deliberately constrains nothing here, so a plugin
421
+ * appending one would silently classify every live bracket before it as seed history.
399
422
  *
400
423
  * An owner of a standalone open/close bracket (`compaction/start` …
401
424
  * `compaction/end`) reads it because seed history and live work are otherwise
@@ -416,7 +439,7 @@ export type SessionEventType = keyof SessionEventMap;
416
439
  * event types may carry {@link SurfaceOp}; system, user, and tool events may also cite
417
440
  * earlier sources through {@link SessionEvent.sourceEventSeqs}.
418
441
  */
419
- export type SurfaceEventType = 'system/message' | 'user/message' | 'assistant/message' | 'tool/result';
442
+ export type SurfaceEventType = 'system/message' | 'developer/message' | 'user/message' | 'assistant/message' | 'tool/result';
420
443
  /** A message-producing event carrying its required surface operation. */
421
444
  export type SurfaceEvent = SessionEvent<SurfaceEventType>;
422
445
  /**
@@ -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 = 3;
54
+ export const SESSION_FORMAT_VERSION = 4;
55
55
  //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-session",
3
3
  "description": "Event-sourced session store for the DeepSeek Harness",
4
- "version": "0.1.6-alpha.2",
4
+ "version": "0.1.7-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -26,6 +26,10 @@
26
26
  "types": "./lib/types/types.d.ts",
27
27
  "default": "./lib/types/types.js"
28
28
  },
29
+ "./fork": {
30
+ "types": "./lib/types/fork.d.ts",
31
+ "default": "./lib/types/fork.js"
32
+ },
29
33
  "./src/*": "./src/*",
30
34
  "./package.json": "./package.json",
31
35
  "./surface": {
@@ -41,19 +45,19 @@
41
45
  ],
42
46
  "license": "MIT",
43
47
  "peerDependencies": {
44
- "@deepseek-ai/dsh-scope": "^0.1.6-alpha.2",
45
- "@deepseek-ai/cordis": "^4.0.2"
48
+ "@deepseek-ai/cordis": "~4.0.4",
49
+ "@deepseek-ai/dsh-scope": "0.1.7-alpha.2"
46
50
  },
47
51
  "devDependencies": {
48
- "@deepseek-ai/dsh-invariants": "^0.1.6-alpha.2",
49
- "@deepseek-ai/dsh-scope": "^0.1.6-alpha.2",
50
- "@deepseek-ai/dsh-typert-protocol": "^0.1.6-alpha.2",
51
- "@deepseek-ai/dsh-typert-registry": "^0.1.6-alpha.2",
52
- "@deepseek-ai/cordis": "^4.0.2"
52
+ "@deepseek-ai/dsh-invariants": "0.1.7-alpha.2",
53
+ "@deepseek-ai/dsh-scope": "0.1.7-alpha.2",
54
+ "@deepseek-ai/dsh-typert-protocol": "0.1.7-alpha.2",
55
+ "@deepseek-ai/dsh-typert-registry": "0.1.7-alpha.2",
56
+ "@deepseek-ai/cordis": "~4.0.4"
53
57
  },
54
58
  "dependencies": {
55
- "@deepseek-ai/dsh-llm": "^0.1.6-alpha.2",
56
- "@deepseek-ai/dsh-util-values": "^0.1.6-alpha.2",
57
- "@deepseek-ai/dsh-brand": "^0.1.6-alpha.2"
59
+ "@deepseek-ai/dsh-llm": "0.1.7-alpha.2",
60
+ "@deepseek-ai/dsh-brand": "0.1.7-alpha.2",
61
+ "@deepseek-ai/dsh-util-values": "0.1.7-alpha.2"
58
62
  }
59
63
  }