@deepseek-ai/dsh-session 0.2.2-alpha.3 → 0.2.2-rc.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.
- package/README.i18n.yaml +4 -4
- package/README.md +3 -1
- package/README.zh.md +3 -1
- package/lib/index.js +151 -79
- package/lib/types/fork.d.ts.map +1 -0
- package/lib/types/fork.js.map +1 -0
- package/lib/types/index.d.ts +17 -1
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +82 -63
- package/lib/types/index.js.map +1 -0
- package/lib/types/known-event-types.d.ts.map +1 -0
- package/lib/types/known-event-types.js +1 -0
- package/lib/types/known-event-types.js.map +1 -0
- package/lib/types/preparation.d.ts.map +1 -0
- package/lib/types/preparation.js.map +1 -0
- package/lib/types/repair.d.ts.map +1 -0
- package/lib/types/repair.js.map +1 -0
- package/lib/types/request-header.d.ts.map +1 -0
- package/lib/types/request-header.js.map +1 -0
- package/lib/types/seq-ranges.d.ts.map +1 -0
- package/lib/types/seq-ranges.js.map +1 -0
- package/lib/types/surface.d.ts +6 -0
- package/lib/types/surface.d.ts.map +1 -0
- package/lib/types/surface.js +57 -11
- package/lib/types/surface.js.map +1 -0
- package/lib/types/tool-history.d.ts.map +1 -0
- package/lib/types/tool-history.js.map +1 -0
- package/lib/types/types.d.ts +4 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/types.js.map +1 -0
- package/package.json +14 -11
- package/src/fork.ts +30 -0
- package/src/index.ts +1555 -0
- package/src/known-event-types.ts +91 -0
- package/src/preparation.ts +49 -0
- package/src/repair.ts +211 -0
- package/src/request-header.ts +69 -0
- package/src/seq-ranges.ts +73 -0
- package/src/surface.ts +758 -0
- package/src/tool-history.ts +75 -0
- package/src/types.ts +574 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Stateful reconstruction of historical tool definitions for request projection. */
|
|
2
|
+
|
|
3
|
+
import type { ToolHistory, ToolSchema } from '@deepseek-ai/dsh-llm'
|
|
4
|
+
import { deepFreeze } from '@deepseek-ai/dsh-util-values'
|
|
5
|
+
import type { SessionEvent, SessionSeq } from './types.ts'
|
|
6
|
+
|
|
7
|
+
/** Folds committed headers and developer messages independently of model capability. */
|
|
8
|
+
export class ToolHistoryProjection {
|
|
9
|
+
/** Historical declarations indexed by the header sequence referenced by additions. */
|
|
10
|
+
private readonly headers = new Map<SessionSeq, readonly ToolSchema[]>()
|
|
11
|
+
/** Definitions retained in the current declaration series, including removed tools. */
|
|
12
|
+
private declared = new Map<string, ToolSchema>()
|
|
13
|
+
/** Active definitions from the latest request header. */
|
|
14
|
+
private active: readonly ToolSchema[] = []
|
|
15
|
+
/** Active names reconstructed from the baseline and recorded updates. */
|
|
16
|
+
private available = new Set<string>()
|
|
17
|
+
/** Header that starts the current declaration series; absent before the first header. */
|
|
18
|
+
private baselineSeq: SessionSeq | undefined
|
|
19
|
+
/** Immutable request snapshot of the current baseline and resolved updates. */
|
|
20
|
+
private history: ToolHistory = deepFreeze({ tools: [], updates: [] })
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Consume the next committed event in log order.
|
|
24
|
+
* @param event - a session event, including inherited events during restoration.
|
|
25
|
+
*/
|
|
26
|
+
apply(event: SessionEvent): void {
|
|
27
|
+
if (event.type === 'request/header') {
|
|
28
|
+
const tools = event.data.header.tools ?? []
|
|
29
|
+
this.headers.set(event.seq, tools)
|
|
30
|
+
// A retained name cannot acquire a different definition without losing
|
|
31
|
+
// prefix reuse; an unchanged restored name is re-offered by its recorded addition.
|
|
32
|
+
const redeclared = tools.some((tool) => {
|
|
33
|
+
const before = this.declared.get(tool.name)
|
|
34
|
+
return before !== undefined && JSON.stringify(before) !== JSON.stringify(tool)
|
|
35
|
+
})
|
|
36
|
+
if (this.baselineSeq === undefined || event.data.reason === 'series' || event.data.startsSeries || redeclared) {
|
|
37
|
+
this.baselineSeq = event.seq
|
|
38
|
+
this.declared = new Map(tools.map(tool => [tool.name, tool]))
|
|
39
|
+
this.history = deepFreeze({ tools, updates: [] })
|
|
40
|
+
this.available = new Set(tools.map(tool => tool.name))
|
|
41
|
+
}
|
|
42
|
+
this.active = tools
|
|
43
|
+
} else if (event.type === 'developer/message') {
|
|
44
|
+
const { message, headerSeq } = event.data
|
|
45
|
+
const definitions = headerSeq === undefined ? [] : this.headers.get(headerSeq)
|
|
46
|
+
const additions = message.content.flatMap((block) => {
|
|
47
|
+
if (block.type !== 'tool-addition') return []
|
|
48
|
+
const tool = definitions?.find(tool => tool.name === block.toolName)
|
|
49
|
+
if (tool === undefined) throw new Error(`tool history: missing definition for ${block.toolName}`)
|
|
50
|
+
return [tool]
|
|
51
|
+
})
|
|
52
|
+
for (const tool of additions) this.declared.set(tool.name, tool)
|
|
53
|
+
for (const block of message.content) {
|
|
54
|
+
if (block.type === 'tool-addition') this.available.add(block.toolName)
|
|
55
|
+
else if (block.type === 'tool-removal') this.available.delete(block.toolName)
|
|
56
|
+
}
|
|
57
|
+
this.history = deepFreeze({
|
|
58
|
+
tools: this.history.tools,
|
|
59
|
+
updates: [...this.history.updates, { messageId: message.id, additions }],
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read an immutable snapshot; subsequent events do not mutate it.
|
|
66
|
+
* @returns initial declarations and historically resolved additions for the current series.
|
|
67
|
+
*/
|
|
68
|
+
snapshot(): ToolHistory {
|
|
69
|
+
// Sessions written before tool-update emission have headers without matching updates.
|
|
70
|
+
if (this.active.length !== this.available.size || this.active.some(tool => !this.available.has(tool.name))) {
|
|
71
|
+
return deepFreeze({ tools: this.active, updates: [] })
|
|
72
|
+
}
|
|
73
|
+
return this.history
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import { brandNumber, brandString, type Branded, type BrandedNumber } from '@deepseek-ai/dsh-brand'
|
|
2
|
+
import type {
|
|
3
|
+
AssistantMessage,
|
|
4
|
+
DeveloperMessage,
|
|
5
|
+
AssistantStreamRecord,
|
|
6
|
+
ToolCallId,
|
|
7
|
+
LlmCallConfig,
|
|
8
|
+
LlmCallConfigAdapterDefaults,
|
|
9
|
+
LlmFailure,
|
|
10
|
+
SystemMessage,
|
|
11
|
+
SystemPromptUpdate,
|
|
12
|
+
TokenUsage,
|
|
13
|
+
ToolResultMessage,
|
|
14
|
+
ToolSchema,
|
|
15
|
+
UserMessage,
|
|
16
|
+
} from '@deepseek-ai/dsh-llm'
|
|
17
|
+
import type { TypertResource } from '@deepseek-ai/dsh-typert-protocol'
|
|
18
|
+
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
|
|
19
|
+
|
|
20
|
+
/** Identifies one session in the store (and its persistence artifacts). */
|
|
21
|
+
export type SessionId = Branded<'SessionId'>
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Brand a string as a {@link SessionId}.
|
|
25
|
+
* @param id - the raw session id string.
|
|
26
|
+
* @returns the same string with the session-id brand.
|
|
27
|
+
*/
|
|
28
|
+
export function SessionId(id: string): SessionId {
|
|
29
|
+
return brandString<SessionId>(id)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Sequence number of one existing event in a Session log. */
|
|
33
|
+
export type SessionSeq = BrandedNumber<'SessionSeq'>
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Admit a numeric value as an existing Session event position.
|
|
37
|
+
* @param value - non-negative safe integer admitted by the owning log operation.
|
|
38
|
+
* @returns the same number with the Session-sequence brand.
|
|
39
|
+
*/
|
|
40
|
+
export function SessionSeq(value: number): SessionSeq {
|
|
41
|
+
if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) {
|
|
42
|
+
throw new TypeError(`SessionSeq must be a non-negative safe integer, got ${String(value)}`)
|
|
43
|
+
}
|
|
44
|
+
return brandNumber<SessionSeq>(value)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A Session log gap, prefix length, or read offset, which may equal the event count. */
|
|
48
|
+
export type SessionLogOffset = BrandedNumber<'SessionLogOffset'>
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Admit a numeric value as a Session log offset.
|
|
52
|
+
* @param value - non-negative safe integer used as a gap or prefix length.
|
|
53
|
+
* @returns the same number with the Session-log-offset brand.
|
|
54
|
+
*/
|
|
55
|
+
export function SessionLogOffset(value: number): SessionLogOffset {
|
|
56
|
+
if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) {
|
|
57
|
+
throw new TypeError(`SessionLogOffset must be a non-negative safe integer, got ${String(value)}`)
|
|
58
|
+
}
|
|
59
|
+
return brandNumber<SessionLogOffset>(value)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Inclusive Session event watermark, or `-1` before any event exists. */
|
|
63
|
+
export type SessionSeqCursor = SessionSeq | -1
|
|
64
|
+
|
|
65
|
+
/** One existing Session event position, or explicit absence. */
|
|
66
|
+
export type OptionalSessionSeq = SessionSeq | null
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Current logical Session format version, stamped into every newly written
|
|
70
|
+
* {@link SessionHeader}. Current Session and persistence code accept only this
|
|
71
|
+
* value; header-only readers classify supported historical formats, while an
|
|
72
|
+
* event-body read composes the build-static adjacent chain and publishes only
|
|
73
|
+
* this final generation before constructing a Session.
|
|
74
|
+
*
|
|
75
|
+
* The version is a single monotonic integer with no major/minor split. Retain
|
|
76
|
+
* it when older readers resume correctly, preserve safely ignorable records,
|
|
77
|
+
* or refuse required features before execution, mutation, or tail repair.
|
|
78
|
+
* New readers must preserve historical meanings, including logs mixing old
|
|
79
|
+
* and new record representations. Bump when an existing discriminator cannot
|
|
80
|
+
* prevent unsafe interpretation; a structural difference alone is insufficient.
|
|
81
|
+
* Unknown fields and values require evidence of the released reader's actual
|
|
82
|
+
* admission behavior. The per-event {@link SessionEvent.ignorable} marker
|
|
83
|
+
* governs unknown event types, not arbitrary nested payload changes.
|
|
84
|
+
* Same-version readers retain earlier representations: the adjacent migration
|
|
85
|
+
* chain does not run when stored and current versions match. A subsequent
|
|
86
|
+
* migration must account for all supported representations of its source.
|
|
87
|
+
* Compatibility review and the retained migration obligations are recorded in
|
|
88
|
+
* `.agents/notes/implemented/process/2026-10-08-session-reader-compatibility-review.md`.
|
|
89
|
+
*/
|
|
90
|
+
export const SESSION_FORMAT_VERSION = 4
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Immutable validated storage metadata, kept outside the conversation event log.
|
|
94
|
+
*/
|
|
95
|
+
export interface SessionHeader {
|
|
96
|
+
/**
|
|
97
|
+
* Current logical format version, stamped from {@link SESSION_FORMAT_VERSION}.
|
|
98
|
+
* Historical physical headers are translated before entering this interface.
|
|
99
|
+
*/
|
|
100
|
+
readonly version: typeof SESSION_FORMAT_VERSION
|
|
101
|
+
/** The session's id (mirrors the {@link Session}'s id). */
|
|
102
|
+
readonly id: SessionId
|
|
103
|
+
/** Non-negative safe-integer Unix epoch milliseconds when the session was created. */
|
|
104
|
+
readonly createdAt: number
|
|
105
|
+
/** Absolute working directory the session was created in (if any). */
|
|
106
|
+
readonly cwd?: string
|
|
107
|
+
/**
|
|
108
|
+
* Direct creator of a subagent Session. Ordinary forks omit this field;
|
|
109
|
+
* historical ordinary forks may retain their source id here. Presence alone
|
|
110
|
+
* does not classify a Session as a subagent; use `origin` for that distinction.
|
|
111
|
+
*/
|
|
112
|
+
readonly parentSession?: SessionId
|
|
113
|
+
/**
|
|
114
|
+
* Whether this Session contains a fork-inherited event prefix. The exact prefix
|
|
115
|
+
* length is Session state rather than ordinary header metadata.
|
|
116
|
+
*/
|
|
117
|
+
readonly isSeeded: boolean
|
|
118
|
+
/**
|
|
119
|
+
* Coarse product classification for a session created as a subagent child.
|
|
120
|
+
* This is presentation metadata, not proof that the child is continuable.
|
|
121
|
+
*/
|
|
122
|
+
readonly origin?: 'subagent'
|
|
123
|
+
/**
|
|
124
|
+
* Delegation depth: absent (zero) for a top-level session, parent depth + 1
|
|
125
|
+
* for a subagent child. Persisted so a recursion budget survives restart and
|
|
126
|
+
* resume — a runtime-only depth would reset a resumed child to top-level.
|
|
127
|
+
*/
|
|
128
|
+
readonly delegationDepth?: number
|
|
129
|
+
/**
|
|
130
|
+
* Id of the agent preset this session's agent was composed from, when the
|
|
131
|
+
* deployment composes per session. Durable because the preset decides the
|
|
132
|
+
* session's tools and prompt: a resume that restored a different composition
|
|
133
|
+
* would replay history the model can no longer act on.
|
|
134
|
+
*/
|
|
135
|
+
readonly agentPreset?: string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Options for creating a {@link Session} via the store. `seed` replays/forks
|
|
140
|
+
* an existing event log; `meta` carries the caller-supplied storage fields the
|
|
141
|
+
* store folds into a {@link SessionHeader}.
|
|
142
|
+
*/
|
|
143
|
+
export interface CreateSessionOptions {
|
|
144
|
+
/** Initial replay or fork history supplied at construction. */
|
|
145
|
+
readonly seed?: readonly SessionEvent[]
|
|
146
|
+
/**
|
|
147
|
+
* Exact fork-inherited prefix length when `meta.isSeeded` is true. The
|
|
148
|
+
* constructor appends the child-owned tagged marker at the cut unless
|
|
149
|
+
* the seed already includes it followed by child-owned fork closers.
|
|
150
|
+
*/
|
|
151
|
+
readonly inheritedEventCount?: SessionLogOffset
|
|
152
|
+
/**
|
|
153
|
+
* Storage metadata read once before publication. `isSeeded` marks fork
|
|
154
|
+
* lineage; supplying replay history alone does not make it inherited.
|
|
155
|
+
*/
|
|
156
|
+
readonly meta?: {
|
|
157
|
+
readonly cwd?: string
|
|
158
|
+
readonly parentSession?: SessionId
|
|
159
|
+
readonly createdAt?: number
|
|
160
|
+
readonly isSeeded?: boolean
|
|
161
|
+
readonly origin?: 'subagent'
|
|
162
|
+
readonly delegationDepth?: number
|
|
163
|
+
readonly agentPreset?: string
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Aliasing state of an adoptable Session seed. `shared-frozen` permits deeply
|
|
169
|
+
* frozen aliases plus independently owned unfrozen values in the same seed.
|
|
170
|
+
*/
|
|
171
|
+
export type SessionSeedEventState = 'detached' | 'shared-frozen'
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Adoptable storage values transferred to {@link SessionStore.prepare}
|
|
175
|
+
* without another copy or freeze pass.
|
|
176
|
+
*/
|
|
177
|
+
export interface RestoredSessionOptions {
|
|
178
|
+
/** Events that are independently owned or already deeply frozen. */
|
|
179
|
+
readonly seed: SessionEvent[]
|
|
180
|
+
/** Independently owned storage metadata to validate and freeze in place. */
|
|
181
|
+
readonly meta: SessionHeader
|
|
182
|
+
/** Exact number of fork-inherited leading events decoded from storage. */
|
|
183
|
+
readonly inheritedEventCount: SessionLogOffset
|
|
184
|
+
/** Aliasing state carried from the operation that produced the seed. */
|
|
185
|
+
readonly eventState: SessionSeedEventState
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Inputs accepted while constructing an unpublished Session. */
|
|
189
|
+
export type PrepareSessionOptions =
|
|
190
|
+
| (CreateSessionOptions & { readonly eventState?: undefined })
|
|
191
|
+
| RestoredSessionOptions
|
|
192
|
+
|
|
193
|
+
/** Why an active agent driver was cancelled. */
|
|
194
|
+
export type AgentCancelCause =
|
|
195
|
+
| { readonly kind: 'user' }
|
|
196
|
+
| { readonly kind: 'parent' }
|
|
197
|
+
| { readonly kind: 'hook'; readonly reason: string }
|
|
198
|
+
| { readonly kind: 'disposed' }
|
|
199
|
+
|
|
200
|
+
/** Durable cancellation cause, including imports whose original coarse record carried no cause. */
|
|
201
|
+
export type TurnEndCancelCause = AgentCancelCause | { readonly kind: 'legacy' }
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Why a turn ended. Merge-extensible sum type.
|
|
205
|
+
*/
|
|
206
|
+
export interface TurnEndReasonMap {
|
|
207
|
+
completed: { kind: 'completed' }
|
|
208
|
+
/** A cancellation request interrupted the live turn. */
|
|
209
|
+
aborted: { kind: 'aborted'; reason: TurnEndCancelCause }
|
|
210
|
+
|
|
211
|
+
blocked: { kind: 'blocked' }
|
|
212
|
+
/**
|
|
213
|
+
* The turn failed. `error` is always a structured failure: the `LlmError`
|
|
214
|
+
* facts verbatim, or `{ message: errorChain(error), code }` flattened from
|
|
215
|
+
* any other error, where `code` is `TOOL_ACTIVATION_FAILED` for a failed
|
|
216
|
+
* tool condition and `UNKNOWN` otherwise.
|
|
217
|
+
*/
|
|
218
|
+
error: { kind: 'error'; error: LlmFailure }
|
|
219
|
+
/** At least one step reached its output-token ceiling, even if a plugin continued the turn. */
|
|
220
|
+
'max-tokens': { kind: 'max-tokens' }
|
|
221
|
+
/**
|
|
222
|
+
* A crash-orphaned turn was closed after the fact: agent-loop resume appends
|
|
223
|
+
* this closer for a stored log whose last turn never ended, and session-query
|
|
224
|
+
* synthesizes it on cold reads. The loop never emits this marker live, and
|
|
225
|
+
* the events recorded before the crash remain intact.
|
|
226
|
+
*/
|
|
227
|
+
interrupted: { kind: 'interrupted' }
|
|
228
|
+
/**
|
|
229
|
+
* Fork-seed construction closed a turn that was still open at the fork
|
|
230
|
+
* boundary in the source session. Only fork seeds carry this marker — the
|
|
231
|
+
* loop never emits it — and the source events before the boundary remain
|
|
232
|
+
* intact in the child.
|
|
233
|
+
*/
|
|
234
|
+
forked: { kind: 'forked' }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The union over {@link TurnEndReasonMap} — why a turn ended; plugins extend it by merging variants into the map. */
|
|
238
|
+
export type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap]
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Logged request state outside derived history: call config and tools. The
|
|
242
|
+
* system prompt is derived history — surface node 0, a `system/message` event.
|
|
243
|
+
* The latest full `request/header` snapshot reconstructs the header; canonical
|
|
244
|
+
* empty optional fields are absent.
|
|
245
|
+
*/
|
|
246
|
+
export interface EpochHeader {
|
|
247
|
+
/** The conversation's call configuration (provider, model, reasoning effort, and sampling scalars). */
|
|
248
|
+
config: LlmCallConfig
|
|
249
|
+
/** Effective config fields materialized from the exact adapter rather than proposed by a caller. */
|
|
250
|
+
adapterDefaults?: LlmCallConfigAdapterDefaults
|
|
251
|
+
/** Assembled tool schemas; absent for a tool-less request. */
|
|
252
|
+
tools?: ToolSchema[]
|
|
253
|
+
/** Retired request text; system prompts belong to system/message events.
|
|
254
|
+
* @persistenceReserved
|
|
255
|
+
*/
|
|
256
|
+
system?: never
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Registration-bound metadata for one resolved model route. */
|
|
260
|
+
export interface RequestContext {
|
|
261
|
+
/** Registered provider route the metadata belongs to. */
|
|
262
|
+
provider: string
|
|
263
|
+
/** Provider-owned model id the metadata belongs to. */
|
|
264
|
+
model: string
|
|
265
|
+
/** Maximum combined request and response context in tokens, when advertised. */
|
|
266
|
+
contextWindow?: number
|
|
267
|
+
/** `'in-history'` when the route reads the latest `system` message at any position as the effective system prompt. */
|
|
268
|
+
systemPromptUpdate?: SystemPromptUpdate
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Why a `request/header` snapshot was appended: `'initial'` — the log's first
|
|
273
|
+
* header (a new conversation); `'resume'` — a loop instance's first request
|
|
274
|
+
* over a log that already has header events (process restart, fork seed);
|
|
275
|
+
* `'change'` — a later request used a different header; `'series'` — an unchanged
|
|
276
|
+
* header began an explicitly distinct message series or followed a surface
|
|
277
|
+
* replacement. Other reasons carry `startsSeries` when a new series coincides.
|
|
278
|
+
*/
|
|
279
|
+
export type RequestHeaderReason = 'initial' | 'resume' | 'change' | 'series'
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The merge-extensible, append-only source of truth for an agent interaction.
|
|
283
|
+
* Message history is derived from this log. Every event is lossless JSON and
|
|
284
|
+
* sequence numbers stay contiguous. Assistant attempt events embed their exact
|
|
285
|
+
* compact raw streams so persistence stores one durable settlement per attempt.
|
|
286
|
+
*/
|
|
287
|
+
export interface SessionEventMap {
|
|
288
|
+
/**
|
|
289
|
+
* Opens turn `turn` before the loop claims queued input or runs pre-step.
|
|
290
|
+
* Rejection, empty input, cancellation, or failure may close it with no
|
|
291
|
+
* step; otherwise the following identified `user/message` event or batch
|
|
292
|
+
* records the messages entering the step.
|
|
293
|
+
*/
|
|
294
|
+
'turn/start': { turn: number }
|
|
295
|
+
/**
|
|
296
|
+
* Closes turn `turn` with the {@link TurnEndReason} that ended it. A turn
|
|
297
|
+
* with no entered step has no `step/start` or `step/end`. The loop does not await a
|
|
298
|
+
* flush at turn boundaries: `dsh-session-checkpoint-policy` owns the
|
|
299
|
+
* per-request durability checkpoint, and consumers that read storage after
|
|
300
|
+
* `whenIdle()` flush themselves. Success commits the turn; rejection is
|
|
301
|
+
* reported live and does not prevent later work.
|
|
302
|
+
*/
|
|
303
|
+
'turn/end': { turn: number; reason: TurnEndReason }
|
|
304
|
+
/** Opens step `step` of turn `turn` — one model call plus the tool executions it requested. */
|
|
305
|
+
'step/start': { turn: number; step: number }
|
|
306
|
+
/** Closes step `step` of turn `turn`. */
|
|
307
|
+
'step/end': { turn: number; step: number }
|
|
308
|
+
/**
|
|
309
|
+
* A user-role message on the model-visible surface: a direct human prompt
|
|
310
|
+
* (the queued message claimed for this turn), a synthetic `agent.inject()`
|
|
311
|
+
* context (file-change notices, subdir AGENTS.md, skill content, cron
|
|
312
|
+
* notifications, …), or an entered goal continuation round. All three
|
|
313
|
+
* project their `content` verbatim; `source` tells them apart.
|
|
314
|
+
*/
|
|
315
|
+
'user/message': UserMessage
|
|
316
|
+
/** An incremental agent session change admitted at the named turn and step. */
|
|
317
|
+
'developer/message': {
|
|
318
|
+
turn: number
|
|
319
|
+
step: number
|
|
320
|
+
message: DeveloperMessage
|
|
321
|
+
/** Earlier request/header defining every tool addition; required exactly when additions are present. */
|
|
322
|
+
headerSeq?: SessionSeq
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* The rendered system prompt on the model-visible surface. The loop appends
|
|
326
|
+
* the first one as surface node 0 before the step's first `user/message`.
|
|
327
|
+
* A prepared in-history route can append nonempty changes in a continuing
|
|
328
|
+
* series. An incapable route, or a new series whose effective text differs,
|
|
329
|
+
* normalizes text to the first system node; a series start whose latest
|
|
330
|
+
* surviving node already holds the rendering leaves it in place.
|
|
331
|
+
* Normalization empties nonempty later nodes, then rewrites the head if
|
|
332
|
+
* needed, through logged per-node replacements. An empty rendering always
|
|
333
|
+
* clears all active system nodes, leaving no older instructions model-visible.
|
|
334
|
+
* Empty later nodes are dormant and project to no message; an empty head with
|
|
335
|
+
* no active later node records "no system prompt". Restored nonempty text follows
|
|
336
|
+
* the same route and series rule; empty nodes never restore older text.
|
|
337
|
+
*/
|
|
338
|
+
'system/message': { turn: number; step: number; message: SystemMessage }
|
|
339
|
+
/**
|
|
340
|
+
* Assembled assistant message for one step (derived history uses this).
|
|
341
|
+
* Carries the step's `usage` when the adapter reported token accounting, so
|
|
342
|
+
* the model output and its accounting travel together (there is no separate
|
|
343
|
+
* usage record). `usage` is absent when the adapter reported none. A turn
|
|
344
|
+
* cancelled mid-stream finalizes its delivered text/reasoning prefix as this
|
|
345
|
+
* event with `interrupted: true`; undispatched tool calls are absent. The
|
|
346
|
+
* marker distinguishes that prefix without re-deriving interruption from turn
|
|
347
|
+
* boundaries. An aborted turn with no such event streamed no visible content.
|
|
348
|
+
*/
|
|
349
|
+
'assistant/message': {
|
|
350
|
+
turn: number
|
|
351
|
+
step: number
|
|
352
|
+
message: AssistantMessage
|
|
353
|
+
/** Exact timed model stream, compacted without joining delta boundaries. */
|
|
354
|
+
stream: AssistantStreamRecord[]
|
|
355
|
+
usage?: TokenUsage
|
|
356
|
+
interrupted?: true
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* One model attempt that committed no surface message. The embedded stream
|
|
360
|
+
* preserves a failed, retried, cancelled, or stream-error attempt that
|
|
361
|
+
* reached settlement without fabricating model-visible history.
|
|
362
|
+
*/
|
|
363
|
+
'assistant/attempt': { turn: number; step: number; stream: AssistantStreamRecord[] }
|
|
364
|
+
/**
|
|
365
|
+
* The model requested one tool invocation: `name` with the raw `arguments`
|
|
366
|
+
* JSON string exactly as the model produced it (unparsed). `callId` pairs the
|
|
367
|
+
* call with its `tool/result`.
|
|
368
|
+
*/
|
|
369
|
+
'tool/call': { turn: number; step: number; callId: ToolCallId; name: string; arguments: string }
|
|
370
|
+
/**
|
|
371
|
+
* A completed tool call's model-facing result, optional internal failure
|
|
372
|
+
* identity and user-facing reason, and optional tool-private `meta`
|
|
373
|
+
* presentation payload. The reason remains outside the model-facing message.
|
|
374
|
+
* `meta` is
|
|
375
|
+
* opaque to the core (the producing tool owns its shape and reads it back in
|
|
376
|
+
* `presentResult`) but MUST be JSON-serializable: `Session.append`
|
|
377
|
+
* runtime-validates all event data with `isJsonValue`, so a non-serializable
|
|
378
|
+
* `meta` is rejected at the source, and the durable log reproduces the
|
|
379
|
+
* identical card on replay. Absent
|
|
380
|
+
* unless the tool attaches one (e.g. `dsh-tool-fs` carries its result-time
|
|
381
|
+
* contextual diff here).
|
|
382
|
+
*/
|
|
383
|
+
'tool/result': {
|
|
384
|
+
turn: number
|
|
385
|
+
step: number
|
|
386
|
+
message: ToolResultMessage
|
|
387
|
+
/**
|
|
388
|
+
* Optional failure identity and raw user-facing reason, outside model content;
|
|
389
|
+
* allowed only when the message has `isError: true`.
|
|
390
|
+
*/
|
|
391
|
+
error?: { name: string; code: string; reason?: string }
|
|
392
|
+
meta?: JsonValue
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Full header for the next request, appended inside its step before dispatch.
|
|
396
|
+
* It is log-only; the latest snapshot reconstructs the request header.
|
|
397
|
+
*/
|
|
398
|
+
'request/header': {
|
|
399
|
+
header: EpochHeader
|
|
400
|
+
reason: RequestHeaderReason
|
|
401
|
+
/** This request begins a distinct model-message series, independently of the header reason. */
|
|
402
|
+
startsSeries?: true
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Route metadata for the next request, logged only when the route, capacity,
|
|
406
|
+
* or system prompt update mode changes. It does not participate in request
|
|
407
|
+
* reconstruction or header equality. Prompt admission uses the bound prepared
|
|
408
|
+
* call's capability, not this snapshot from an earlier request.
|
|
409
|
+
*/
|
|
410
|
+
'request/context': RequestContext
|
|
411
|
+
/**
|
|
412
|
+
* Separates inherited or restored history from later lifecycle-owned work.
|
|
413
|
+
* This log-only marker need not be at {@link Session.firstLiveSeq}: a fork
|
|
414
|
+
* seed can already contain its tagged marker and child-owned synthetic
|
|
415
|
+
* closers before construction.
|
|
416
|
+
*
|
|
417
|
+
* A fresh fork child owns one `{ inherited: true }` marker at its exact
|
|
418
|
+
* inherited-prefix cut, even when that prefix ends in an ancestor marker.
|
|
419
|
+
* `buildForkSeed` appends that marker before any synthetic closers; the
|
|
420
|
+
* `Session` constructor supplies it when given only the inherited prefix.
|
|
421
|
+
* The last tagged marker is the current Session's cut; untagged markers
|
|
422
|
+
* keep ordinary restore and replay lifecycle boundaries.
|
|
423
|
+
*
|
|
424
|
+
* Only the `Session` constructor and `buildForkSeed` may create this marker.
|
|
425
|
+
* Session append does not reject other writers, so a plugin appending one
|
|
426
|
+
* would silently classify every live bracket before it as seed history.
|
|
427
|
+
*
|
|
428
|
+
* An owner of a standalone open/close bracket (`compaction/start` …
|
|
429
|
+
* `compaction/end`) reads it because seed history and live work are otherwise
|
|
430
|
+
* byte-identical: an unmatched opening marker before this event belongs to
|
|
431
|
+
* an ended lifecycle, whatever ended it. NOT a liveness signal about other
|
|
432
|
+
* writers — a concurrently live session holds its own boundary elsewhere,
|
|
433
|
+
* so tolerating concurrent writers needs a signal beyond the log.
|
|
434
|
+
*/
|
|
435
|
+
'session/end-seed': { inherited?: true }
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** The appendable event-type keys of {@link SessionEventMap}, plugin-merged extensions included. */
|
|
439
|
+
export type SessionEventType = keyof SessionEventMap
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The subset of {@link SessionEventType} values whose events produce LLM
|
|
443
|
+
* messages and are eligible to appear on the ordered surface. Only these
|
|
444
|
+
* event types may carry {@link SurfaceOp}; system, user, and tool events may also cite
|
|
445
|
+
* earlier sources through {@link SessionEvent.sourceEventSeqs}.
|
|
446
|
+
*/
|
|
447
|
+
export type SurfaceEventType =
|
|
448
|
+
| 'system/message'
|
|
449
|
+
| 'developer/message'
|
|
450
|
+
| 'user/message'
|
|
451
|
+
| 'assistant/message'
|
|
452
|
+
| 'tool/result'
|
|
453
|
+
|
|
454
|
+
/** A message-producing event carrying its required surface operation. */
|
|
455
|
+
export type SurfaceEvent = SessionEvent<SurfaceEventType>
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* How a session event entered the ordered surface. Only valid on
|
|
459
|
+
* {@link SurfaceEventType} events.
|
|
460
|
+
*
|
|
461
|
+
* - `'append'`: added to the tail — normal path for user/assistant/tool
|
|
462
|
+
* messages.
|
|
463
|
+
* - `{ op: 'replace', startSeq, endSeq }`: replaces surface nodes from `startSeq`
|
|
464
|
+
* (inclusive) through `endSeq` (inclusive) with this node. Both must exist as
|
|
465
|
+
* surface nodes in the current surface. `startSeq === endSeq` replaces a single
|
|
466
|
+
* node. The node's {@link SessionEvent.sourceEventSeqs} must include every
|
|
467
|
+
* shadowed surface node. Used by compaction; any surface-replacing producer
|
|
468
|
+
* may use it.
|
|
469
|
+
*/
|
|
470
|
+
export type SurfaceOp =
|
|
471
|
+
| 'append'
|
|
472
|
+
| { op: 'replace'; startSeq: SessionSeq; endSeq: SessionSeq }
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Surface placement and cited source-event seqs for {@link Session.append}. Required on
|
|
476
|
+
* message-producing events and forbidden on log-only events.
|
|
477
|
+
*/
|
|
478
|
+
export type SurfaceIntent<T extends SurfaceEventType = SurfaceEventType> = {
|
|
479
|
+
surfaceOp: SurfaceOp
|
|
480
|
+
} & (T extends 'assistant/message' ? {
|
|
481
|
+
/** Assistant messages embed their provider stream instead of citing source events. */
|
|
482
|
+
sourceEventSeqs?: never
|
|
483
|
+
} : {
|
|
484
|
+
/** Complete non-empty set of known earlier source-event seqs. */
|
|
485
|
+
sourceEventSeqs?: SessionSeq[]
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
/** One positional event call proposed for append, without assigned sequence or time. */
|
|
489
|
+
export type SessionEventInput = {
|
|
490
|
+
[K in SessionEventType]: readonly [type: K, data: SessionEventMap[K], ...opts: K extends SurfaceEventType ? [opts: SurfaceIntent<K>] : []]
|
|
491
|
+
}[SessionEventType]
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* One immutable entry in the session log.
|
|
495
|
+
*
|
|
496
|
+
* A proper discriminated union over `type` (not independent `type`/`data`
|
|
497
|
+
* unions), so `switch (event.type)` narrows `event.data` without casts.
|
|
498
|
+
*
|
|
499
|
+
* The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
|
|
500
|
+
* they only exist on {@link SurfaceEventType} variants (`system/message`, `user/message`,
|
|
501
|
+
* `assistant/message`, `tool/result`).
|
|
502
|
+
* Non-surface events (boundary markers, attempts, errors) never carry
|
|
503
|
+
* surface metadata — the compiler enforces this at `Session.append()`
|
|
504
|
+
* call sites.
|
|
505
|
+
*/
|
|
506
|
+
export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
|
507
|
+
[K in SessionEventType]: {
|
|
508
|
+
type: K
|
|
509
|
+
/** Monotonic sequence number within the session. */
|
|
510
|
+
seq: SessionSeq
|
|
511
|
+
/** Unix epoch milliseconds. */
|
|
512
|
+
time: number
|
|
513
|
+
data: SessionEventMap[K]
|
|
514
|
+
/**
|
|
515
|
+
* Marks an event a reader may safely skip when it does not recognize
|
|
516
|
+
* `type`. Absent means required: a reader meeting an unrecognized type
|
|
517
|
+
* without this marker MUST refuse to reconstruct the session instead of
|
|
518
|
+
* silently dropping the event, because an unrecognized required event may
|
|
519
|
+
* change how the rest of the log is interpreted. A writer sets `true` only
|
|
520
|
+
* on purely informational records whose loss cannot affect reconstruction;
|
|
521
|
+
* defaulting to required means a forgotten marker over-refuses (an
|
|
522
|
+
* inconvenience) rather than silently resuming a gutted session.
|
|
523
|
+
*/
|
|
524
|
+
ignorable?: true
|
|
525
|
+
} & (K extends SurfaceEventType ? SurfaceIntent<K> : {
|
|
526
|
+
surfaceOp?: never
|
|
527
|
+
sourceEventSeqs?: never
|
|
528
|
+
})
|
|
529
|
+
}[T]
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Event type of an experimental plugin record: `plugin:` followed by
|
|
533
|
+
* slash-separated segments of lowercase letters, digits, `.`, `_`, and `-`,
|
|
534
|
+
* each starting with a letter or digit. A record type is never a
|
|
535
|
+
* {@link SessionEventMap} member.
|
|
536
|
+
*/
|
|
537
|
+
export type PluginRecordType = `plugin:${string}`
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Payloads written by experimental packages, keyed by their `plugin:` record
|
|
541
|
+
* names. Packages augment this map to type writes and enter the current plugin
|
|
542
|
+
* record catalog; these declarations do not enter {@link SessionEventMap} or
|
|
543
|
+
* released persistence schemas. Stored records still require owner validation.
|
|
544
|
+
*/
|
|
545
|
+
export interface PluginRecordMap {}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* One committed experimental plugin record, read from the log by
|
|
549
|
+
* `pluginRecordOf`. The record's owner validates `data` before use, because a
|
|
550
|
+
* restored record carries whatever JSON an earlier build of its owner wrote,
|
|
551
|
+
* or a V3 event that a format migration renamed into the `plugin:` namespace held.
|
|
552
|
+
*/
|
|
553
|
+
export interface PluginRecord {
|
|
554
|
+
/** The record type, chosen by the owning plugin. */
|
|
555
|
+
readonly type: PluginRecordType
|
|
556
|
+
/** The record's position in the Session log. */
|
|
557
|
+
readonly seq: SessionSeq
|
|
558
|
+
/** Unix epoch milliseconds at append. */
|
|
559
|
+
readonly time: number
|
|
560
|
+
/** The JSON payload as committed. */
|
|
561
|
+
readonly data: unknown
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
declare module '@deepseek-ai/dsh-typert-protocol' {
|
|
565
|
+
interface TypertResourceMap {
|
|
566
|
+
/** One Session; the Session Controller registers its default `sessionId` wire field. */
|
|
567
|
+
session: TypertResource<SessionId>
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
interface RemoteErrorDetailsMap {
|
|
571
|
+
/** The named Session does not exist; produced by every layer that resolves a SessionId. */
|
|
572
|
+
'session/not-found': { readonly sessionId: SessionId }
|
|
573
|
+
}
|
|
574
|
+
}
|