dsh-lcx-codex 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -2
- package/README.md +138 -183
- package/README_EN.md +137 -118
- package/assets/dsh-lcx-codex-banner.jpg +0 -0
- package/lib/compact-v2.js +129 -16
- package/lib/dsh-compat.js +124 -0
- package/lib/index.js +126 -92
- package/lib/native-checkpoint.js +241 -22
- package/lib/route.js +174 -12
- package/lib/token-budget.js +165 -0
- package/lib/web-search-alpha.js +148 -7
- package/lib/web-search-capability.js +5 -3
- package/package.json +4 -2
package/lib/native-checkpoint.js
CHANGED
|
@@ -1,5 +1,70 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
import { baseURLFingerprint, routeCompatible } from './route.js'
|
|
2
4
|
import { persistNativeImageReferences } from './dsh-responses.js'
|
|
5
|
+
import { estimateBudgetItem, portableBudgetError, portableTokenCeiling } from './token-budget.js'
|
|
6
|
+
|
|
7
|
+
/** @typedef {Record<string, unknown>} UnknownRecord */
|
|
8
|
+
/** @typedef {{ type: 'compaction', encrypted_content: string }} CompactionItem */
|
|
9
|
+
/** @typedef {{ role: 'developer' | 'user' | 'system', content?: string | unknown[] }} RetainedClientItem */
|
|
10
|
+
/** @typedef {{ type: string } | RetainedClientItem} NativeOutputItem */
|
|
11
|
+
/** @typedef {{ provider: string, model: string, baseURL: string, sessionId: string }} RouteIdentity */
|
|
12
|
+
/** @typedef {{ type?: string, text?: string, id?: unknown, name?: unknown, toolCallId?: unknown, content?: DshContentBlock[] }} DshContentBlock */
|
|
13
|
+
/** @typedef {{ kind?: string, plugin?: string, compactionId?: string }} DshMessageSource */
|
|
14
|
+
/** @typedef {{ role?: string, content?: DshContentBlock[], source?: DshMessageSource }} DshMessage */
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {DshMessage & {
|
|
17
|
+
* compactionId?: string,
|
|
18
|
+
* rawOutput?: unknown[],
|
|
19
|
+
* shadowedSeqs?: number[],
|
|
20
|
+
* message?: DshMessage
|
|
21
|
+
* }} SessionEventData
|
|
22
|
+
*/
|
|
23
|
+
/** @typedef {{ type: string, seq?: number, data?: SessionEventData }} SessionEvent */
|
|
24
|
+
/** @typedef {{ type: 'compaction/summary', seq?: number, data: SessionEventData & { compactionId: string, rawOutput?: unknown[], shadowedSeqs?: number[] } }} CompactionSummaryEvent */
|
|
25
|
+
/** @typedef {{ id?: string, events?: SessionEvent[], deriveEventMessage?: (event: SessionEvent) => DshMessage | null | undefined }} NativeSession */
|
|
26
|
+
/** @typedef {{ tokenBudget?: number, assistantTokenReserve?: number, assistantPerMessageTokenCap?: number }} RetentionOptions */
|
|
27
|
+
/** @typedef {{ item: unknown, index: number }} IndexedItem */
|
|
28
|
+
/** @typedef {{ index: number, item: unknown, tokens: number }} SelectedItem */
|
|
29
|
+
/** @typedef {{ selected: SelectedItem[], used: number }} Selection */
|
|
30
|
+
/** @typedef {{ items: unknown[], clientCount: number, assistantCount: number, estimatedTokens: number, clientEstimatedTokens: number, assistantEstimatedTokens: number }} RetentionPlan */
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {object} NativeCheckpointBase
|
|
33
|
+
* @property {string} compactionId
|
|
34
|
+
* @property {string} provider
|
|
35
|
+
* @property {string} model
|
|
36
|
+
* @property {string} baseURLFingerprint
|
|
37
|
+
* @property {string} sourceSessionId
|
|
38
|
+
* @property {NativeOutputItem[]} nativeOutput
|
|
39
|
+
* @property {CompactionItem} [nativeCompaction]
|
|
40
|
+
* @property {number} [retainedInputCount]
|
|
41
|
+
* @property {number} [retainedClientCount]
|
|
42
|
+
* @property {number} [retainedAssistantCount]
|
|
43
|
+
* @property {number} [retainedEstimatedTokens]
|
|
44
|
+
* @property {number} [createdAt]
|
|
45
|
+
*/
|
|
46
|
+
/** @typedef {NativeCheckpointBase & { type: 'lcx-native-compaction-v5', version: 5, retentionPolicy?: 'conversation-fidelity-v1' }} NativeCheckpointV5 */
|
|
47
|
+
/** @typedef {NativeCheckpointBase & { type: 'lcx-native-compaction-v4', version: 4 }} NativeCheckpointV4 */
|
|
48
|
+
/** @typedef {NativeCheckpointV5 | NativeCheckpointV4} NativeCheckpointBlock */
|
|
49
|
+
/**
|
|
50
|
+
* Minimal validated candidate shape. DSH rawOutput is unknown until the existing
|
|
51
|
+
* version, field, count, and compaction validation below has completed.
|
|
52
|
+
* @typedef {object} NativeCheckpointCandidate
|
|
53
|
+
* @property {typeof NATIVE_BLOCK_TYPE | typeof LEGACY_V4_BLOCK_TYPE} type
|
|
54
|
+
* @property {typeof NATIVE_BLOCK_VERSION | typeof LEGACY_V4_BLOCK_VERSION} version
|
|
55
|
+
* @property {unknown} [compactionId]
|
|
56
|
+
* @property {unknown} [nativeOutput]
|
|
57
|
+
* @property {unknown} [provider]
|
|
58
|
+
* @property {unknown} [model]
|
|
59
|
+
* @property {unknown} [baseURLFingerprint]
|
|
60
|
+
* @property {unknown} [sourceSessionId]
|
|
61
|
+
* @property {unknown} [retainedInputCount]
|
|
62
|
+
* @property {unknown} [retainedClientCount]
|
|
63
|
+
* @property {unknown} [retainedAssistantCount]
|
|
64
|
+
*/
|
|
65
|
+
/** @typedef {{ compaction: CompactionItem }} NativeCompactionResult */
|
|
66
|
+
/** @typedef {{ session: NativeSession, route: RouteIdentity, result: NativeCompactionResult, input?: unknown[], imageMap?: unknown, retentionOptions?: RetentionOptions }} CreateCheckpointOptions */
|
|
67
|
+
/** @typedef {Error & { code?: string }} LcxError */
|
|
3
68
|
|
|
4
69
|
export const NATIVE_BLOCK_TYPE = 'lcx-native-compaction-v5'
|
|
5
70
|
export const NATIVE_BLOCK_VERSION = 5
|
|
@@ -11,37 +76,89 @@ export const RETAINED_MESSAGE_TOKEN_BUDGET = 64_000
|
|
|
11
76
|
export const ASSISTANT_RETENTION_TOKEN_RESERVE = 24_000
|
|
12
77
|
export const ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP = 3_000
|
|
13
78
|
|
|
79
|
+
/**
|
|
80
|
+
* @param {unknown} value
|
|
81
|
+
* @returns {value is UnknownRecord}
|
|
82
|
+
*/
|
|
14
83
|
function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
|
|
84
|
+
/** @param {DshMessage | null | undefined} message */
|
|
15
85
|
function textOf(message) { return (message?.content ?? []).filter((b) => b?.type === 'text').map((b) => b.text).join('') }
|
|
16
|
-
|
|
86
|
+
/** @param {unknown} item */
|
|
87
|
+
function estimatedItemTokens(item) { return estimateBudgetItem(item) }
|
|
88
|
+
/**
|
|
89
|
+
* @param {unknown} item
|
|
90
|
+
* @returns {item is UnknownRecord}
|
|
91
|
+
*/
|
|
17
92
|
function isRetainedClientItem(item) { return isObject(item) && (item.type === undefined || item.type === 'message') && ['user', 'developer', 'system'].includes(String(item.role ?? '')) }
|
|
18
|
-
|
|
93
|
+
/**
|
|
94
|
+
* @param {unknown} item
|
|
95
|
+
* @returns {{ type: 'output_text', text: string }[]}
|
|
96
|
+
*/
|
|
97
|
+
function assistantTextParts(item) { if (!isObject(item) || item.type !== 'message' || item.role !== 'assistant' || !Array.isArray(item.content)) return []; return /** @type {{ type: 'output_text', text: string }[]} */ (item.content.filter((part) => /** @type {UnknownRecord | undefined} */ (part)?.type === 'output_text' && typeof /** @type {UnknownRecord} */ (part).text === 'string' && /** @type {string} */ (/** @type {UnknownRecord} */ (part).text).length > 0)) }
|
|
98
|
+
/** @param {unknown} item */
|
|
19
99
|
function isRetainedAssistantItem(item) { return assistantTextParts(item).length > 0 }
|
|
100
|
+
/** @param {unknown} item */
|
|
20
101
|
function assistantText(item) { return assistantTextParts(item).map((part) => part.text).join('') }
|
|
21
102
|
|
|
103
|
+
/**
|
|
104
|
+
* @param {unknown} item
|
|
105
|
+
* @param {number} [maxTokens]
|
|
106
|
+
* @returns {UnknownRecord | undefined}
|
|
107
|
+
*/
|
|
22
108
|
function truncateVisibleAssistantItem(item, maxTokens = ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP) {
|
|
23
109
|
const text = assistantText(item); if (!text) return undefined
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
110
|
+
/** @param {string} retained */
|
|
111
|
+
const candidate = (retained) => ({ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: retained }] })
|
|
112
|
+
const full = candidate(text)
|
|
113
|
+
if ((estimatedItemTokens(full) ?? Infinity) <= maxTokens) return full
|
|
114
|
+
const marker = '\n…[LCX retained answer truncated]…\n'
|
|
115
|
+
/** @param {number} count */
|
|
116
|
+
const shortened = (count) => {
|
|
117
|
+
const available = Math.max(0, count - marker.length)
|
|
118
|
+
const head = Math.floor(available * 0.72); const tail = available - head
|
|
119
|
+
return candidate(`${text.slice(0, head)}${marker}${text.slice(Math.max(head, text.length - tail))}`)
|
|
120
|
+
}
|
|
121
|
+
let low = 0; let high = text.length; let best
|
|
122
|
+
while (low <= high) {
|
|
123
|
+
const count = Math.floor((low + high) / 2); const next = shortened(count)
|
|
124
|
+
if ((estimatedItemTokens(next) ?? Infinity) <= maxTokens) { best = next; low = count + 1 } else high = count - 1
|
|
125
|
+
}
|
|
126
|
+
return best
|
|
27
127
|
}
|
|
28
128
|
|
|
129
|
+
/**
|
|
130
|
+
* @param {IndexedItem[]} candidates
|
|
131
|
+
* @param {number} budget
|
|
132
|
+
* @param {(value: IndexedItem) => unknown} [transform]
|
|
133
|
+
* @returns {Selection}
|
|
134
|
+
*/
|
|
29
135
|
function selectNewest(candidates, budget, transform = (value) => structuredClone(value.item)) {
|
|
136
|
+
/** @type {SelectedItem[]} */
|
|
30
137
|
const selected = []; let used = 0
|
|
31
|
-
for (let index = candidates.length - 1; index >= 0; index -= 1) { const candidate = candidates[index]; const item = transform(candidate); if (!item) continue; const tokens = estimatedItemTokens(item); if (tokens > budget || used + tokens > budget) continue; selected.push({ index: candidate.index, item, tokens }); used += tokens }
|
|
138
|
+
for (let index = candidates.length - 1; index >= 0; index -= 1) { const candidate = candidates[index]; const item = transform(candidate); if (!item) continue; const tokens = estimatedItemTokens(item); if (tokens === undefined || tokens > budget || used + tokens > budget) continue; selected.push({ index: candidate.index, item, tokens }); used += tokens }
|
|
32
139
|
return { selected, used }
|
|
33
140
|
}
|
|
34
141
|
|
|
142
|
+
/**
|
|
143
|
+
* @param {unknown[] | null | undefined} input
|
|
144
|
+
* @param {RetentionOptions} [options]
|
|
145
|
+
* @returns {unknown[]}
|
|
146
|
+
*/
|
|
35
147
|
export function retainedCompactionInput(input, options = {}) {
|
|
36
|
-
const budget = Number.isSafeInteger(options.tokenBudget) && options.tokenBudget > 0 ? options.tokenBudget : RETAINED_MESSAGE_TOKEN_BUDGET
|
|
148
|
+
const budget = Number.isSafeInteger(options.tokenBudget) && /** @type {number} */ (options.tokenBudget) > 0 ? /** @type {number} */ (options.tokenBudget) : RETAINED_MESSAGE_TOKEN_BUDGET
|
|
37
149
|
const candidates = (input ?? []).map((item, index) => ({ item, index })).filter(({ item }) => isRetainedClientItem(item))
|
|
38
150
|
return selectNewest(candidates, budget).selected.sort((a, b) => a.index - b.index).map(({ item }) => item)
|
|
39
151
|
}
|
|
40
152
|
|
|
153
|
+
/**
|
|
154
|
+
* @param {unknown[] | null | undefined} input
|
|
155
|
+
* @param {RetentionOptions} [options]
|
|
156
|
+
* @returns {RetentionPlan}
|
|
157
|
+
*/
|
|
41
158
|
export function retainedConversationPlan(input, options = {}) {
|
|
42
|
-
const totalBudget = Number.isSafeInteger(options.tokenBudget) && options.tokenBudget > 0 ? options.tokenBudget : RETAINED_MESSAGE_TOKEN_BUDGET
|
|
43
|
-
const assistantReserve = Math.min(totalBudget, Number.isSafeInteger(options.assistantTokenReserve) && options.assistantTokenReserve >= 0 ? options.assistantTokenReserve : ASSISTANT_RETENTION_TOKEN_RESERVE)
|
|
44
|
-
const perMessageCap = Number.isSafeInteger(options.assistantPerMessageTokenCap) && options.assistantPerMessageTokenCap > 0 ? options.assistantPerMessageTokenCap : ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP
|
|
159
|
+
const totalBudget = Number.isSafeInteger(options.tokenBudget) && /** @type {number} */ (options.tokenBudget) > 0 ? /** @type {number} */ (options.tokenBudget) : RETAINED_MESSAGE_TOKEN_BUDGET
|
|
160
|
+
const assistantReserve = Math.min(totalBudget, Number.isSafeInteger(options.assistantTokenReserve) && /** @type {number} */ (options.assistantTokenReserve) >= 0 ? /** @type {number} */ (options.assistantTokenReserve) : ASSISTANT_RETENTION_TOKEN_RESERVE)
|
|
161
|
+
const perMessageCap = Number.isSafeInteger(options.assistantPerMessageTokenCap) && /** @type {number} */ (options.assistantPerMessageTokenCap) > 0 ? /** @type {number} */ (options.assistantPerMessageTokenCap) : ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP
|
|
45
162
|
const indexed = (input ?? []).map((item, index) => ({ item, index }))
|
|
46
163
|
const assistant = selectNewest(indexed.filter(({ item }) => isRetainedAssistantItem(item)), assistantReserve, ({ item }) => truncateVisibleAssistantItem(item, perMessageCap))
|
|
47
164
|
const remaining = Math.max(0, totalBudget - assistant.used)
|
|
@@ -49,45 +166,147 @@ export function retainedConversationPlan(input, options = {}) {
|
|
|
49
166
|
const selected = [...client.selected, ...assistant.selected].sort((a, b) => a.index - b.index)
|
|
50
167
|
return { items: selected.map(({ item }) => item), clientCount: client.selected.length, assistantCount: assistant.selected.length, estimatedTokens: client.used + assistant.used, clientEstimatedTokens: client.used, assistantEstimatedTokens: assistant.used }
|
|
51
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* @param {unknown[] | null | undefined} input
|
|
171
|
+
* @param {RetentionOptions} [options]
|
|
172
|
+
*/
|
|
52
173
|
export function retainedConversationInput(input, options = {}) { return retainedConversationPlan(input, options).items }
|
|
174
|
+
/** @param {unknown[] | null | undefined} items */
|
|
53
175
|
export function hasRetainedCompactionInput(items) { return (items ?? []).some((item) => isRetainedClientItem(item) || isRetainedAssistantItem(item)) }
|
|
176
|
+
/** @param {DshMessage | null | undefined} message */
|
|
54
177
|
export function compactCheckpointId(message) { const source = message?.source; return source?.kind === 'plugin' && source?.plugin === 'compact' && typeof source.compactionId === 'string' && source.compactionId ? source.compactionId : undefined }
|
|
178
|
+
/** @param {DshMessage | null | undefined} message */
|
|
55
179
|
export function legacyCheckpointId(message) { return textOf(message).match(LEGACY_V3_PATTERN)?.[1]?.toLowerCase() }
|
|
56
|
-
|
|
180
|
+
/** @param {NativeSession | null | undefined} session */
|
|
181
|
+
export function activeCompactionId(session) { if (!session?.events) return undefined; /** @type {Set<string>} */ const ended = new Set(); for (let index = session.events.length - 1; index >= 0; index -= 1) { const event = session.events[index]; if (event.type === 'compaction/end' && event.data?.compactionId) ended.add(event.data.compactionId); if (event.type === 'compaction/start' && event.data?.compactionId && !ended.has(event.data.compactionId)) return event.data.compactionId } return undefined }
|
|
57
182
|
|
|
183
|
+
/**
|
|
184
|
+
* @param {CreateCheckpointOptions} options
|
|
185
|
+
* @returns {NativeCheckpointV5}
|
|
186
|
+
*/
|
|
58
187
|
export function createNativeCheckpointBlock({ session, route, result, input = [], imageMap, retentionOptions = {} }) {
|
|
59
188
|
const compactionId = activeCompactionId(session)
|
|
60
|
-
if (!compactionId) {
|
|
189
|
+
if (!compactionId) {
|
|
190
|
+
/** @type {LcxError} */
|
|
191
|
+
const error = new Error('Native compaction could not correlate the active DSH compaction transaction')
|
|
192
|
+
error.code = 'LCX_COMPACTION_ID_UNAVAILABLE'
|
|
193
|
+
throw error
|
|
194
|
+
}
|
|
61
195
|
const retention = retainedConversationPlan(input, retentionOptions)
|
|
196
|
+
/** @type {NativeOutputItem[]} */
|
|
62
197
|
const nativeOutput = persistNativeImageReferences([...retention.items, structuredClone(result.compaction)], imageMap)
|
|
63
198
|
return { type: NATIVE_BLOCK_TYPE, version: NATIVE_BLOCK_VERSION, retentionPolicy: 'conversation-fidelity-v1', compactionId, provider: route.provider, model: route.model, baseURLFingerprint: baseURLFingerprint(route.baseURL), sourceSessionId: route.sessionId, nativeOutput, nativeCompaction: structuredClone(result.compaction), retainedInputCount: retention.items.length, retainedClientCount: retention.clientCount, retainedAssistantCount: retention.assistantCount, retainedEstimatedTokens: retention.estimatedTokens, createdAt: Date.now() }
|
|
64
199
|
}
|
|
65
200
|
|
|
201
|
+
/**
|
|
202
|
+
* @param {NativeCheckpointBlock} block
|
|
203
|
+
* @param {unknown} usage
|
|
204
|
+
* @returns {UnknownRecord[]}
|
|
205
|
+
*/
|
|
66
206
|
export function nativeCheckpointChunks(block, usage) {
|
|
67
207
|
const text = 'LCX Native V2 checkpoint saved in the DSH session log.'
|
|
68
208
|
return [{ type: 'block-start', index: 0, blockType: 'text' }, { type: 'text-delta', index: 0, text }, { type: 'block-end', index: 0, block: { type: 'text', text } }, { type: 'block-start', index: 1, blockType: NATIVE_BLOCK_TYPE }, { type: 'block-end', index: 1, block: structuredClone(block) }, ...(usage ? [{ type: 'usage', usage: structuredClone(usage) }] : []), { type: 'finish', reason: { kind: 'stop' } }]
|
|
69
209
|
}
|
|
70
210
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
211
|
+
/**
|
|
212
|
+
* @param {NativeSession | null | undefined} session
|
|
213
|
+
* @param {string | null | undefined} compactionId
|
|
214
|
+
* @returns {CompactionSummaryEvent | undefined}
|
|
215
|
+
*/
|
|
216
|
+
export function compactionSummaryEvent(session, compactionId) { if (!session?.events || !compactionId) return undefined; for (let index = session.events.length - 1; index >= 0; index -= 1) { const event = session.events[index]; if (event.type === 'compaction/summary' && event.data?.compactionId === compactionId) return /** @type {CompactionSummaryEvent} */ (event) } return undefined }
|
|
217
|
+
/**
|
|
218
|
+
* rawOutput elements remain untrusted until stateFromSummaryEvent validates them.
|
|
219
|
+
* @param {CompactionSummaryEvent | null | undefined} event
|
|
220
|
+
* @returns {NativeCheckpointCandidate | undefined}
|
|
221
|
+
*/
|
|
222
|
+
function nativeBlockFromSummary(event) { const raw = event?.data?.rawOutput ?? []; return /** @type {NativeCheckpointCandidate | undefined} */ (raw.find((value) => /** @type {UnknownRecord | undefined} */ (value)?.type === NATIVE_BLOCK_TYPE && /** @type {UnknownRecord | undefined} */ (value)?.version === NATIVE_BLOCK_VERSION) ?? raw.find((value) => /** @type {UnknownRecord | undefined} */ (value)?.type === LEGACY_V4_BLOCK_TYPE && /** @type {UnknownRecord | undefined} */ (value)?.version === LEGACY_V4_BLOCK_VERSION)) }
|
|
223
|
+
/** @param {unknown} value */
|
|
224
|
+
function validateCount(value) { return value === undefined || (Number.isSafeInteger(value) && /** @type {number} */ (value) >= 0) }
|
|
225
|
+
/**
|
|
226
|
+
* @param {CompactionSummaryEvent | null | undefined} event
|
|
227
|
+
* @returns {NativeCheckpointBlock | undefined}
|
|
228
|
+
*/
|
|
74
229
|
export function stateFromSummaryEvent(event) {
|
|
75
230
|
if (event?.type !== 'compaction/summary') return undefined
|
|
76
231
|
const block = nativeBlockFromSummary(event); if (!isObject(block)) return undefined
|
|
77
232
|
if (block.compactionId !== event.data.compactionId || !Array.isArray(block.nativeOutput) || block.nativeOutput.length === 0) return undefined
|
|
78
233
|
if (typeof block.provider !== 'string' || typeof block.model !== 'string' || typeof block.baseURLFingerprint !== 'string' || typeof block.sourceSessionId !== 'string') return undefined
|
|
79
234
|
if (!validateCount(block.retainedInputCount) || !validateCount(block.retainedClientCount) || !validateCount(block.retainedAssistantCount)) return undefined
|
|
80
|
-
const compactions = block.nativeOutput.filter((item) => item?.type === 'compaction'); if (compactions.length !== 1 || typeof compactions[0]?.encrypted_content !== 'string') return undefined
|
|
81
|
-
const compactionIndex = block.nativeOutput.findIndex((item) => item?.type === 'compaction'); const prefix = compactionIndex >= 0 ? block.nativeOutput.slice(0, compactionIndex) : []
|
|
235
|
+
const compactions = block.nativeOutput.filter((item) => /** @type {UnknownRecord | undefined} */ (item)?.type === 'compaction'); if (compactions.length !== 1 || typeof /** @type {UnknownRecord | undefined} */ (compactions[0])?.encrypted_content !== 'string') return undefined
|
|
236
|
+
const compactionIndex = block.nativeOutput.findIndex((item) => /** @type {UnknownRecord | undefined} */ (item)?.type === 'compaction'); const prefix = compactionIndex >= 0 ? block.nativeOutput.slice(0, compactionIndex) : []
|
|
82
237
|
if (block.version === NATIVE_BLOCK_VERSION) { const clients = prefix.filter(isRetainedClientItem).length; const assistants = prefix.filter(isRetainedAssistantItem).length; const total = clients + assistants; if (block.retainedInputCount !== undefined && block.retainedInputCount !== total) return undefined; if (block.retainedClientCount !== undefined && block.retainedClientCount !== clients) return undefined; if (block.retainedAssistantCount !== undefined && block.retainedAssistantCount !== assistants) return undefined } else { const clients = prefix.filter(isRetainedClientItem).length; if (block.retainedInputCount !== undefined && block.retainedInputCount !== clients) return undefined }
|
|
83
|
-
return structuredClone(block)
|
|
238
|
+
return /** @type {NativeCheckpointBlock} */ (structuredClone(block))
|
|
84
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* @param {NativeSession} session
|
|
242
|
+
* @param {DshMessage} message
|
|
243
|
+
*/
|
|
85
244
|
export function checkpointStateForMessage(session, message) { const id = compactCheckpointId(message); return id ? stateFromSummaryEvent(compactionSummaryEvent(session, id)) : undefined }
|
|
245
|
+
/**
|
|
246
|
+
* @param {NativeCheckpointBlock} state
|
|
247
|
+
* @param {RouteIdentity} route
|
|
248
|
+
* @param {unknown} ctx
|
|
249
|
+
*/
|
|
86
250
|
export function stateRouteCompatible(state, route, ctx) { return routeCompatible(state, route, ctx) }
|
|
87
|
-
|
|
88
|
-
|
|
251
|
+
/**
|
|
252
|
+
* @param {NativeSession | null | undefined} session
|
|
253
|
+
* @param {number} seq
|
|
254
|
+
* @returns {DshMessage | undefined}
|
|
255
|
+
*/
|
|
256
|
+
function eventMessage(session, seq) { const events = session?.events ?? []; const event = events[seq]?.seq === seq ? events[seq] : events.find((candidate) => candidate?.seq === seq); if (!event) return undefined; if (typeof /** @type {NativeSession} */ (session).deriveEventMessage === 'function') return /** @type {(event: SessionEvent) => DshMessage | null | undefined} */ (/** @type {NativeSession} */ (session).deriveEventMessage)(event) ?? undefined; if (event.type === 'user/message') return event.data; if (event.type === 'assistant/message') return event.data?.message; if (event.type === 'tool/result') return event.data?.message; return undefined }
|
|
257
|
+
/** @param {DshMessage} message */
|
|
258
|
+
function estimateChars(message) { try { const encoded = JSON.stringify(message); return typeof encoded === 'string' ? encoded.length : undefined } catch { return undefined } }
|
|
259
|
+
/**
|
|
260
|
+
* @param {NativeSession} session
|
|
261
|
+
* @param {string} compactionId
|
|
262
|
+
* @param {Set<string>} visited
|
|
263
|
+
* @param {number} depth
|
|
264
|
+
* @returns {DshMessage[]}
|
|
265
|
+
*/
|
|
89
266
|
function expandedCheckpointMessages(session, compactionId, visited, depth) { if (depth > 16 || visited.has(compactionId)) return []; visited.add(compactionId); const summary = compactionSummaryEvent(session, compactionId); if (!summary) return []; const result = []; for (const seq of summary.data?.shadowedSeqs ?? []) { const message = eventMessage(session, seq); if (!message) continue; const nested = compactCheckpointId(message); if (nested) result.push(...expandedCheckpointMessages(session, nested, visited, depth + 1)); else result.push(structuredClone(message)) } return result }
|
|
90
|
-
|
|
267
|
+
/**
|
|
268
|
+
* @param {NativeSession} session
|
|
269
|
+
* @param {string} compactionId
|
|
270
|
+
* @returns {DshMessage[]}
|
|
271
|
+
*/
|
|
272
|
+
export function shadowedMessagesForCheckpoint(session, compactionId) { return expandedCheckpointMessages(session, compactionId, /** @type {Set<string>} */ (new Set()), 0) }
|
|
273
|
+
/**
|
|
274
|
+
* @param {DshMessage[]} messages
|
|
275
|
+
* @returns {DshMessage[][]}
|
|
276
|
+
*/
|
|
91
277
|
function groupMessages(messages) { const groups = []; for (let index = 0; index < messages.length; index += 1) { const message = messages[index]; const toolCalls = message?.role === 'assistant' ? (message.content ?? []).filter((b) => b?.type === 'tool-call').map((b) => String(b.id)) : []; if (toolCalls.length === 0) { groups.push([message]); continue } const group = [message]; const pending = new Set(toolCalls); let cursor = index + 1; while (cursor < messages.length && pending.size > 0) { const next = messages[cursor]; const results = next?.role === 'user' ? (next.content ?? []).filter((b) => b?.type === 'tool-result').map((b) => String(b.toolCallId)) : []; if (results.length === 0) break; group.push(next); for (const id of results) pending.delete(id); cursor += 1 } if (pending.size === 0) index = cursor - 1; groups.push(group) } return groups }
|
|
92
|
-
|
|
278
|
+
/**
|
|
279
|
+
* @param {NativeSession} session
|
|
280
|
+
* @param {string} compactionId
|
|
281
|
+
* @param {{ maxChars?: number }} [options]
|
|
282
|
+
* @returns {DshMessage[]}
|
|
283
|
+
*/
|
|
284
|
+
export function portableMessagesForCheckpoint(session, compactionId, options = {}) {
|
|
285
|
+
const maxChars = Number.isSafeInteger(options.maxChars) && /** @type {number} */ (options.maxChars) > 0 ? /** @type {number} */ (options.maxChars) : 80_000
|
|
286
|
+
const maxTokens = portableTokenCeiling(maxChars) ?? 1
|
|
287
|
+
const expanded = shadowedMessagesForCheckpoint(session, compactionId); if (expanded.length === 0) return []
|
|
288
|
+
const groups = groupMessages(expanded); const kept = []; let chars = 0; let tokens = 0
|
|
289
|
+
for (let index = groups.length - 1; index >= 0; index -= 1) {
|
|
290
|
+
const group = groups[index]
|
|
291
|
+
let groupChars = 0; let groupTokens = 0; let budgetable = true
|
|
292
|
+
for (const message of group) {
|
|
293
|
+
const messageChars = estimateChars(message); const messageTokens = estimatedItemTokens(message)
|
|
294
|
+
if (messageChars === undefined || messageTokens === undefined) { budgetable = false; break }
|
|
295
|
+
groupChars += messageChars; groupTokens += messageTokens
|
|
296
|
+
}
|
|
297
|
+
const exceeds = !budgetable || chars + groupChars > maxChars || tokens + groupTokens > maxTokens
|
|
298
|
+
if (exceeds) {
|
|
299
|
+
if (kept.length === 0) throw portableBudgetError('Portable checkpoint newest message group exceeds the configured budget')
|
|
300
|
+
break
|
|
301
|
+
}
|
|
302
|
+
kept.unshift(...group); chars += groupChars; tokens += groupTokens
|
|
303
|
+
}
|
|
304
|
+
return kept
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* @param {DshMessage[]} messages
|
|
308
|
+
* @param {NativeSession} session
|
|
309
|
+
* @param {{ maxChars?: number }} [options]
|
|
310
|
+
* @returns {DshMessage[]}
|
|
311
|
+
*/
|
|
93
312
|
export function rewriteCheckpointsPortable(messages, session, options = {}) { const rewritten = []; let changed = false; for (const message of messages ?? []) { const id = compactCheckpointId(message); if (!id || !checkpointStateForMessage(session, message)) { rewritten.push(message); continue } const portable = portableMessagesForCheckpoint(session, id, options); if (portable.length === 0) { rewritten.push(message); continue } rewritten.push(...portable); changed = true } return changed ? rewritten : messages }
|