@deepseek-ai/dsh-session 0.0.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,426 @@
1
+ import type { Branded } from '@deepseek-ai/dsh-brand';
2
+ import type { AssistantMessage, CallId, LlmCallConfig, LlmCallConfigAdapterDefaults, LlmFailure, StreamChunk, TokenUsage, ToolResultMessage, ToolSchema, UserMessage } from '@deepseek-ai/dsh-llm';
3
+ import type { JsonValue } from './json.ts';
4
+ /** Identifies one session in the store (and its persistence artifacts). */
5
+ export type SessionId = Branded<'SessionId'>;
6
+ /**
7
+ * Brand a string as a {@link SessionId}.
8
+ * @param id - the raw session id string.
9
+ * @returns the same string, branded (a compile-time cast — no runtime cost).
10
+ */
11
+ export declare function SessionId(id: string): SessionId;
12
+ /**
13
+ * The on-disk session format version, stamped into every newly-written {@link SessionHeader}
14
+ * and enforced by every persistence backend on load. The single source of truth for the
15
+ * version — write sites and the load-time check all read it.
16
+ * While the harness is unreleased it is pinned at `0`: no compatibility is
17
+ * implied, incompatible logs are rejected, and no migration is provided. A
18
+ * monotonic version policy starts with the first tagged release.
19
+ */
20
+ export declare const SESSION_FORMAT_VERSION = 0;
21
+ /**
22
+ * Immutable validated storage metadata, kept outside the conversation event log.
23
+ */
24
+ export interface SessionHeader {
25
+ /**
26
+ * On-disk format version, stamped from {@link SESSION_FORMAT_VERSION} when the
27
+ * session is created. A persistence backend rejects any other version on load
28
+ * (no migration — see the constant).
29
+ */
30
+ readonly version: number;
31
+ /** The session's id (mirrors the {@link Session}'s id). */
32
+ readonly id: SessionId;
33
+ /** Non-negative safe-integer Unix epoch milliseconds when the session was created. */
34
+ readonly createdAt: number;
35
+ /** Absolute working directory the session was created in (if any). */
36
+ readonly cwd?: string;
37
+ /** The session this one was forked from (seed lineage), if any. */
38
+ readonly parentSession?: SessionId;
39
+ /**
40
+ * How many leading events were inherited through a seed. Persisting this
41
+ * boundary lets resume and replay distinguish parent history from child work.
42
+ */
43
+ readonly seedLength?: number;
44
+ /**
45
+ * Coarse product classification for a session created as a subagent child.
46
+ * This is presentation metadata, not proof that the child is continuable.
47
+ */
48
+ readonly origin?: 'subagent';
49
+ /**
50
+ * Delegation depth: absent (zero) for a top-level session, parent depth + 1
51
+ * for a subagent child. Persisted so a recursion budget survives restart and
52
+ * resume — a runtime-only depth would reset a resumed child to top-level.
53
+ */
54
+ readonly delegationDepth?: number;
55
+ /**
56
+ * Id of the agent preset this session's agent was composed from, when the
57
+ * deployment composes per session. Durable because the preset decides the
58
+ * session's tools and prompt: a resume that restored a different composition
59
+ * would replay history the model can no longer act on.
60
+ */
61
+ readonly agentPreset?: string;
62
+ }
63
+ /**
64
+ * Options for creating a {@link Session} via the store. `seed` replays/forks
65
+ * an existing event log; `meta` carries the caller-supplied storage fields the
66
+ * store folds into a {@link SessionHeader}.
67
+ */
68
+ export interface CreateSessionOptions {
69
+ /** Initial replay or fork history supplied at construction. */
70
+ readonly seed?: readonly SessionEvent[];
71
+ /**
72
+ * Storage metadata read once before publication. `seedLength` is explicit
73
+ * because a resumed seed contains the full stored log, not only its inherited prefix.
74
+ */
75
+ readonly meta?: {
76
+ readonly cwd?: string;
77
+ readonly parentSession?: SessionId;
78
+ readonly createdAt?: number;
79
+ readonly seedLength?: number;
80
+ readonly origin?: 'subagent';
81
+ readonly delegationDepth?: number;
82
+ readonly agentPreset?: string;
83
+ };
84
+ }
85
+ /**
86
+ * Fresh storage values transferred to {@link SessionStore.prepare} without a
87
+ * second serialization copy. Callers retain no mutable aliases.
88
+ */
89
+ export interface RestoredSessionOptions {
90
+ /** Fresh detached storage events to validate and freeze in place. */
91
+ readonly seed: SessionEvent[];
92
+ /** Fresh detached storage metadata to validate and freeze in place. */
93
+ readonly meta: SessionHeader;
94
+ /** Select the persistence ownership-transfer path. */
95
+ readonly seedSource: 'persistence';
96
+ }
97
+ /** Inputs accepted while constructing an unpublished Session. */
98
+ export type PrepareSessionOptions = (CreateSessionOptions & {
99
+ readonly seedSource?: undefined;
100
+ }) | RestoredSessionOptions;
101
+ /** Why an active agent driver was cancelled. */
102
+ export type AgentCancelCause = {
103
+ readonly kind: 'user';
104
+ } | {
105
+ readonly kind: 'parent';
106
+ } | {
107
+ readonly kind: 'hook';
108
+ readonly reason: string;
109
+ } | {
110
+ readonly kind: 'disposed';
111
+ };
112
+ /** Durable cancellation cause, including imports whose original coarse record carried no cause. */
113
+ export type TurnEndCancelCause = AgentCancelCause | {
114
+ readonly kind: 'legacy';
115
+ };
116
+ /**
117
+ * Why a turn ended. Merge-extensible sum type.
118
+ */
119
+ export interface TurnEndReasonMap {
120
+ completed: {
121
+ kind: 'completed';
122
+ };
123
+ /** A cancellation request interrupted the live turn. */
124
+ aborted: {
125
+ kind: 'aborted';
126
+ reason: TurnEndCancelCause;
127
+ };
128
+ blocked: {
129
+ kind: 'blocked';
130
+ };
131
+ /**
132
+ * The turn failed. `error` is always a structured failure: the `LlmError`
133
+ * facts verbatim, or `{ message: errorChain(error), code: 'UNKNOWN' }`
134
+ * flattened from any other error.
135
+ */
136
+ error: {
137
+ kind: 'error';
138
+ error: LlmFailure;
139
+ };
140
+ /** At least one step reached its output-token ceiling, even if a plugin continued the turn. */
141
+ 'max-tokens': {
142
+ kind: 'max-tokens';
143
+ };
144
+ /**
145
+ * A persistence backend closed a crash-orphaned turn on reload. The loop never
146
+ * emits this marker, and the events recorded before the crash remain intact.
147
+ */
148
+ interrupted: {
149
+ kind: 'interrupted';
150
+ };
151
+ }
152
+ /** The union over {@link TurnEndReasonMap} — why a turn ended; plugins extend it by merging variants into the map. */
153
+ export type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap];
154
+ /**
155
+ * One entry in an agent's todo list — the unit of the `todo/write`
156
+ * {@link SessionEventMap} event's whole-list snapshot.
157
+ *
158
+ * Deliberately minimal: a human-readable `content` line and a three-state
159
+ * `status`. No id, priority, or `activeForm` — the list is replaced wholesale
160
+ * on every write (last-write-wins), so entries need no stable identity. The
161
+ * three statuses describe the complete portable lifecycle needed by model and
162
+ * UI consumers.
163
+ */
164
+ export interface TodoItem {
165
+ /** What this task is — a short imperative line shown in the UI. */
166
+ content: string;
167
+ /** Lifecycle state. `in_progress` marks a task being worked now; parallel work may mark several. */
168
+ status: 'pending' | 'in_progress' | 'completed';
169
+ }
170
+ /**
171
+ * Logged request state outside derived history: call config, system prompt, and
172
+ * tools. The latest full `request/header` snapshot reconstructs it; canonical
173
+ * empty optional fields are absent.
174
+ */
175
+ export interface EpochHeader {
176
+ /** The conversation's call configuration (provider, model, reasoning effort, and sampling scalars). */
177
+ config: LlmCallConfig;
178
+ /** Effective config fields materialized from the exact adapter rather than proposed by a caller. */
179
+ adapterDefaults?: LlmCallConfigAdapterDefaults;
180
+ /** Rendered system prompt text; absent for a system-less request. */
181
+ system?: string;
182
+ /** Assembled tool schemas; absent for a tool-less request. */
183
+ tools?: ToolSchema[];
184
+ }
185
+ /** Registration-bound metadata for one resolved model route. */
186
+ export interface RequestContext {
187
+ /** Registered provider route the metadata belongs to. */
188
+ provider: string;
189
+ /** Provider-owned model id the metadata belongs to. */
190
+ model: string;
191
+ /** Maximum combined request and response context in tokens, when advertised. */
192
+ contextWindow?: number;
193
+ }
194
+ /**
195
+ * Why a `request/header` snapshot was appended: `'initial'` — the log's first
196
+ * header (a new conversation); `'resume'` — a loop instance's first request
197
+ * over a log that already has header events (process restart, fork seed);
198
+ * `'change'` — a later request used a different header.
199
+ */
200
+ export type RequestHeaderReason = 'initial' | 'resume' | 'change';
201
+ /**
202
+ * The merge-extensible, append-only source of truth for an agent interaction.
203
+ * Message history is derived from this log. Every event is lossless JSON and
204
+ * sequence numbers stay contiguous, including raw chunks, so persistence can
205
+ * store the canonical log verbatim.
206
+ */
207
+ export interface SessionEventMap {
208
+ /**
209
+ * Opens turn `turn` before the loop claims queued input or runs pre-step.
210
+ * Rejection, empty input, cancellation, or failure may close it with no
211
+ * step; otherwise the following identified `user/message` event or batch
212
+ * records the messages entering the step.
213
+ */
214
+ 'turn/start': {
215
+ turn: number;
216
+ };
217
+ /**
218
+ * Closes turn `turn` with the {@link TurnEndReason} that ended it. A turn
219
+ * with no entered step has no `step/start` or `step/end`. The loop does not await a
220
+ * flush at turn boundaries: `dsh-session-checkpoint-policy` owns the
221
+ * per-request durability checkpoint, and consumers that read storage after
222
+ * `whenIdle()` flush themselves. Success commits the turn; rejection is
223
+ * reported live and does not prevent later work.
224
+ */
225
+ 'turn/end': {
226
+ turn: number;
227
+ reason: TurnEndReason;
228
+ };
229
+ /** Opens step `step` of turn `turn` — one model call plus the tool executions it requested. */
230
+ 'step/start': {
231
+ turn: number;
232
+ step: number;
233
+ };
234
+ /** Closes step `step` of turn `turn`. */
235
+ 'step/end': {
236
+ turn: number;
237
+ step: number;
238
+ };
239
+ /**
240
+ * A user-role message on the model-visible surface: a direct human prompt
241
+ * (the queued message claimed for this turn), a synthetic `agent.inject()`
242
+ * context (file-change notices, subdir AGENTS.md, skill content, cron
243
+ * notifications, …), or an entered goal continuation round. All three
244
+ * project their `content` verbatim; `source` tells them apart.
245
+ */
246
+ 'user/message': UserMessage;
247
+ /** Raw stream chunk — token-level replay fidelity. */
248
+ 'assistant/chunk': {
249
+ turn: number;
250
+ step: number;
251
+ chunk: StreamChunk;
252
+ };
253
+ /**
254
+ * Assembled assistant message for one step (derived history uses this).
255
+ * Carries the step's `usage` when the adapter reported token accounting, so
256
+ * the model output and its accounting travel together (there is no separate
257
+ * usage record). `usage` is absent when the adapter reported none.
258
+ */
259
+ 'assistant/message': {
260
+ turn: number;
261
+ step: number;
262
+ message: AssistantMessage;
263
+ usage?: TokenUsage;
264
+ };
265
+ /**
266
+ * The model requested one tool invocation: `name` with the raw `arguments`
267
+ * JSON string exactly as the model produced it (unparsed). `callId` pairs the
268
+ * call with its `tool/result`.
269
+ */
270
+ 'tool/call': {
271
+ turn: number;
272
+ step: number;
273
+ callId: CallId;
274
+ name: string;
275
+ arguments: string;
276
+ };
277
+ /**
278
+ * A completed tool call's model-facing result, optional internal failure
279
+ * identity, and optional tool-private `meta` presentation payload. `meta` is
280
+ * opaque to the core (the producing tool owns its shape and reads it back in
281
+ * `presentResult`) but MUST be JSON-serializable: `Session.append`
282
+ * runtime-validates all event data with `isJsonValue`, so a non-serializable
283
+ * `meta` is rejected at the source, and the durable log reproduces the
284
+ * identical card on replay. Absent
285
+ * unless the tool attaches one (e.g. `dsh-tool-fs` carries its result-time
286
+ * contextual diff here).
287
+ */
288
+ 'tool/result': {
289
+ turn: number;
290
+ step: number;
291
+ message: ToolResultMessage;
292
+ error?: {
293
+ name: string;
294
+ code: string;
295
+ };
296
+ meta?: JsonValue;
297
+ };
298
+ /** Whole-list snapshot; latest write wins on replay. Log-only UI state; never derived history. */
299
+ 'todo/write': {
300
+ todos: TodoItem[];
301
+ };
302
+ /**
303
+ * Full header for the next request, appended inside its step before dispatch.
304
+ * It is log-only; the latest snapshot reconstructs the request header.
305
+ */
306
+ 'request/header': {
307
+ header: EpochHeader;
308
+ reason: RequestHeaderReason;
309
+ };
310
+ /**
311
+ * Route metadata for the next request, logged only when the route or capacity
312
+ * changes. It does not participate in request reconstruction or header equality.
313
+ */
314
+ 'request/context': RequestContext;
315
+ /**
316
+ * Marks the end of a constructor seed. Events before it have smaller seq
317
+ * values and came from the seed (resume, fork, or replay); this lifecycle
318
+ * produced none of them. This log-only event is the durable projection of
319
+ * {@link Session.firstLiveSeq}. Its payload is empty — position and `time`
320
+ * carry the meaning.
321
+ *
322
+ * Locate the LAST one in stored history. A seed already ending in one is not
323
+ * re-marked, so reopening an untouched session does not grow its log per
324
+ * pickup and the event need not be at the current `firstLiveSeq`.
325
+ *
326
+ * `Session`'s constructor is the only legitimate writer. The invariant
327
+ * companion deliberately constrains nothing here, so a plugin appending one
328
+ * would silently classify every live bracket before it as seed history.
329
+ *
330
+ * An owner of a standalone open/close bracket (`compact/start` …
331
+ * `compact/end`) reads it because seed history and live work are otherwise
332
+ * byte-identical: an unmatched opening marker before this event belongs to
333
+ * an ended lifecycle, whatever ended it. NOT a liveness signal about other
334
+ * writers — a concurrently live session holds its own boundary elsewhere,
335
+ * so tolerating concurrent writers needs a signal beyond the log.
336
+ */
337
+ 'session/end-seed': Record<string, never>;
338
+ }
339
+ /** The appendable event-type keys of {@link SessionEventMap}, plugin-merged extensions included. */
340
+ export type SessionEventType = keyof SessionEventMap;
341
+ /**
342
+ * The subset of {@link SessionEventType} values whose events produce LLM
343
+ * messages and are eligible to appear on the ordered surface. Only these
344
+ * event types may carry {@link SurfaceOp} and {@link SessionEvent.sourceEventSeqs}.
345
+ */
346
+ export type SurfaceEventType = 'user/message' | 'assistant/message' | 'tool/result';
347
+ /**
348
+ * A {@link SessionEvent} that is **on** the ordered surface — its
349
+ * `surfaceOp` is guaranteed present (mandatory), narrowed from a
350
+ * surface-eligible {@link SessionEvent} by checking both `type` and
351
+ * `surfaceOp` at runtime.
352
+ *
353
+ * Use the `isSurfaceEvent` type guard (in `surface.ts`) to narrow a
354
+ * `SessionEvent` to this type.
355
+ */
356
+ export type SurfaceEvent = SessionEvent<SurfaceEventType> & {
357
+ surfaceOp: SurfaceOp;
358
+ };
359
+ /**
360
+ * How a session event entered the ordered surface. Only valid on
361
+ * {@link SurfaceEventType} events.
362
+ *
363
+ * - `'append'`: added to the tail — normal path for user/assistant/tool
364
+ * messages.
365
+ * - `{ op: 'replace', start, end }`: replaces surface nodes from `start`
366
+ * (inclusive) through `end` (inclusive) with this node. Both must exist as
367
+ * surface nodes in the current surface. `start === end` replaces a single
368
+ * node. The node's {@link SessionEvent.sourceEventSeqs} must include every
369
+ * shadowed surface node. Used by compaction; any surface-replacing producer
370
+ * may use it.
371
+ */
372
+ export type SurfaceOp = 'append' | {
373
+ op: 'replace';
374
+ start: number;
375
+ end: number;
376
+ };
377
+ /**
378
+ * Surface placement and cited source-event seqs for {@link Session.append}. Required on
379
+ * message-producing events and forbidden on log-only events.
380
+ */
381
+ export interface SurfaceIntent {
382
+ surfaceOp: SurfaceOp;
383
+ /**
384
+ * Complete set of known source-event seqs. `assistant/message` may use a
385
+ * present empty array for a known empty provider stream; when the field is
386
+ * absent, the event does not record which earlier events produced the message.
387
+ * Other surface events require a non-empty set when this field is present.
388
+ */
389
+ sourceEventSeqs?: number[];
390
+ }
391
+ /**
392
+ * One immutable entry in the session log.
393
+ *
394
+ * A proper discriminated union over `type` (not independent `type`/`data`
395
+ * unions), so `switch (event.type)` narrows `event.data` without casts.
396
+ *
397
+ * The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
398
+ * they only exist on {@link SurfaceEventType} variants (`user/message`,
399
+ * `assistant/message`, `tool/result`).
400
+ * Non-surface events (boundary markers, chunks, usage, errors) never carry
401
+ * surface metadata — the compiler enforces this at `Session.append()`
402
+ * call sites.
403
+ */
404
+ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
405
+ [K in SessionEventType]: {
406
+ type: K;
407
+ /** Monotonic sequence number within the session. */
408
+ seq: number;
409
+ /** Unix epoch milliseconds. */
410
+ time: number;
411
+ data: SessionEventMap[K];
412
+ } & (K extends SurfaceEventType ? {
413
+ /**
414
+ * Seq numbers of earlier events that this event cites as sources
415
+ * (e.g. the `assistant/chunk` seqs that built an `assistant/message`,
416
+ * or the surface nodes shadowed by a compaction replace node). An
417
+ * `assistant/message` may carry a present empty array for a known empty
418
+ * provider stream; when the field is absent, the event does not record which
419
+ * earlier events produced the message.
420
+ */
421
+ sourceEventSeqs?: number[];
422
+ /** How this event entered the surface; absent for non-surface events. */
423
+ surfaceOp?: SurfaceOp;
424
+ } : object);
425
+ }[T];
426
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Brand a string as a {@link SessionId}.
3
+ * @param id - the raw session id string.
4
+ * @returns the same string, branded (a compile-time cast — no runtime cost).
5
+ */
6
+ export function SessionId(id) {
7
+ return id;
8
+ }
9
+ /**
10
+ * The on-disk session format version, stamped into every newly-written {@link SessionHeader}
11
+ * and enforced by every persistence backend on load. The single source of truth for the
12
+ * version — write sites and the load-time check all read it.
13
+ * While the harness is unreleased it is pinned at `0`: no compatibility is
14
+ * implied, incompatible logs are rejected, and no migration is provided. A
15
+ * monotonic version policy starts with the first tagged release.
16
+ */
17
+ export const SESSION_FORMAT_VERSION = 0;
18
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-session",
3
+ "description": "Event-sourced session store for the DeepSeek Harness",
4
+ "version": "0.0.1-rc.1",
5
+ "publishConfig": {
6
+ "access": "restricted"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/core/session"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./types": {
26
+ "types": "./lib/types/types.d.ts",
27
+ "default": "./lib/types/types.js"
28
+ },
29
+ "./src/*": "./src/*",
30
+ "./package.json": "./package.json",
31
+ "./surface": {
32
+ "types": "./lib/types/surface.d.ts",
33
+ "default": "./lib/types/surface.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "lib/index.js",
38
+ "lib/invariant.js",
39
+ "lib/types/**/*.js",
40
+ "lib/types/**/*.d.ts"
41
+ ],
42
+ "license": "BSD-3-Clause",
43
+ "peerDependencies": {
44
+ "@deepseek-ai/dsh-brand": "^0.0.1-rc.1",
45
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
46
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
47
+ "@deepseek-ai/dsh-scope": "^0.0.1-rc.1",
48
+ "@deepseek-ai/dsh-type-meta": "^0.0.1-rc.1",
49
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
50
+ },
51
+ "devDependencies": {
52
+ "@deepseek-ai/dsh-brand": "^0.0.1-rc.1",
53
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
54
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
55
+ "@deepseek-ai/dsh-type-meta": "^0.0.1-rc.1",
56
+ "@deepseek-ai/dsh-typert-registry": "^0.0.1-rc.1",
57
+ "@deepseek-ai/dsh-scope": "^0.0.1-rc.1",
58
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
59
+ }
60
+ }