dsh-lcx-codex 0.4.1 → 0.4.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/ARCHITECTURE.md +24 -0
- package/CHANGELOG.md +15 -0
- package/README.md +177 -125
- package/README_EN.md +180 -116
- package/cordis.patch.yml +1 -0
- package/lib/client.js +15 -18
- package/lib/compact-v2.js +21 -37
- package/lib/dsh-responses.js +13 -6
- package/lib/index.js +69 -43
- package/lib/native-checkpoint.js +18 -7
- package/lib/responses-replay.js +63 -276
- package/lib/responses-request.js +145 -0
- package/lib/responses-stream.js +539 -0
- package/lib/route.js +58 -43
- package/lib/transport.js +10 -4
- package/lib/web-search-alpha.js +5 -2
- package/package.json +4 -4
package/lib/dsh-responses.js
CHANGED
|
@@ -98,6 +98,7 @@ export function readDshPiReplayState(value) {
|
|
|
98
98
|
if (!['text', 'reasoning', 'tool-call'].includes(block.type)) throw invalidReplay(`block ${index} has an unknown type`)
|
|
99
99
|
for (const signature of ['textSignature', 'thinkingSignature', 'thoughtSignature']) if (block[signature] !== undefined && typeof block[signature] !== 'string') throw invalidReplay(`block ${index} ${signature} must be a string`)
|
|
100
100
|
if (block.redacted !== undefined && typeof block.redacted !== 'boolean') throw invalidReplay(`block ${index} redacted must be boolean`)
|
|
101
|
+
if (block.namespace !== undefined && typeof block.namespace !== 'string') throw invalidReplay(`block ${index} namespace must be a string`)
|
|
101
102
|
}
|
|
102
103
|
return { response, blocks: value.blocks }
|
|
103
104
|
}
|
|
@@ -125,7 +126,7 @@ function replayedAssistant(message, source) {
|
|
|
125
126
|
if (replayBlockType(block?.type) !== replay?.type) throw invalidReplay(`block ${index} does not match assistant content`)
|
|
126
127
|
if (block.type === 'text') return { type: 'text', text: String(block.text ?? ''), ...(replay.textSignature === undefined ? {} : { textSignature: replay.textSignature }) }
|
|
127
128
|
if (block.type === 'reasoning') return { type: 'thinking', thinking: String(block.text ?? ''), ...(replay.thinkingSignature === undefined ? {} : { thinkingSignature: replay.thinkingSignature }), ...(replay.redacted === undefined ? {} : { redacted: replay.redacted }) }
|
|
128
|
-
return { type: 'toolCall', id: String(block.id), name: String(block.name), arguments: parseArguments(block.arguments), ...(replay.thoughtSignature === undefined ? {} : { thoughtSignature: replay.thoughtSignature }) }
|
|
129
|
+
return { type: 'toolCall', id: String(block.id), name: String(block.name), arguments: parseArguments(block.arguments), ...(replay.thoughtSignature === undefined ? {} : { thoughtSignature: replay.thoughtSignature }), ...(replay.namespace === undefined ? {} : { namespace: replay.namespace }) }
|
|
129
130
|
})
|
|
130
131
|
return { role: 'assistant', content, api: state.response.api, provider: state.response.provider, model: state.response.model, ...(state.response.responseModel === undefined ? {} : { responseModel: state.response.responseModel }), ...(state.response.responseId === undefined ? {} : { responseId: state.response.responseId }), usage: emptyUsage(), stopReason: state.response.stopReason, timestamp: 0 }
|
|
131
132
|
}
|
|
@@ -173,7 +174,7 @@ function builtinResponsesModel(provider, modelId) {
|
|
|
173
174
|
catch { return undefined }
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
function
|
|
177
|
+
export function resolvePiResponsesModel(options) {
|
|
177
178
|
const route = options.route ?? {}
|
|
178
179
|
const explicit = options.model && typeof options.model === 'object' ? options.model : undefined
|
|
179
180
|
const provider = String(explicit?.provider ?? route.provider ?? 'dsh-lcx-codex')
|
|
@@ -217,19 +218,25 @@ export async function serializeDshMessages(messages, ctx, options = {}) {
|
|
|
217
218
|
requestImageMaxBytes: Number.isSafeInteger(options.requestImageMaxBytes) && options.requestImageMaxBytes > 0 ? options.requestImageMaxBytes : DEFAULT_REQUEST_IMAGE_MAX_BYTES,
|
|
218
219
|
}
|
|
219
220
|
const projected = offloadRequestImagesWithPolicy(messages ?? [], { representation: 'base64', maxBytes: normalized.maxRequestImageBytes, byteQuantum: 1, byteLength: ref => Math.min(ref.bytes, normalized.requestImageMaxBytes) })
|
|
220
|
-
const model =
|
|
221
|
+
const model = resolvePiResponsesModel(normalized)
|
|
221
222
|
const context = { systemPrompt: normalized.systemPrompt, messages: await dshToPiMessages(projected, ctx, normalized, imageMap), tools: options.tools ?? [] }
|
|
222
223
|
const supportsStrictMode = model.compat?.supportsStrictMode ?? false
|
|
223
224
|
const supportsOpenAIGrammarTools = model.compat?.supportsOpenAIGrammarTools ?? false
|
|
225
|
+
const supportsAdditionalTools = model.compat?.supportsAdditionalTools ?? false
|
|
224
226
|
const supportsToolSearch = model.compat?.supportsToolSearch ?? false
|
|
227
|
+
const deferredToolsMode = supportsAdditionalTools ? 'additional-tools' : supportsToolSearch ? 'tool-search' : undefined
|
|
225
228
|
const grammarToolInputProperties = createGrammarToolInputProperties(context.tools, supportsOpenAIGrammarTools)
|
|
226
|
-
const placement = splitDeferredTools(context,
|
|
229
|
+
const placement = splitDeferredTools(context, deferredToolsMode !== undefined)
|
|
227
230
|
const toolOptions = { supportsStrictMode, supportsOpenAIGrammarTools }
|
|
228
231
|
const input = convertResponsesMessages(model, context, new Set(['openai', 'openai-codex', 'opencode']), {
|
|
229
|
-
includeSystemPrompt: normalized.includeSystemPrompt,
|
|
232
|
+
includeSystemPrompt: normalized.includeSystemPrompt,
|
|
233
|
+
grammarToolInputProperties,
|
|
234
|
+
deferredTools: placement.deferred,
|
|
235
|
+
deferredToolsMode,
|
|
236
|
+
toolOptions,
|
|
230
237
|
})
|
|
231
238
|
const tools = options.tools === undefined ? undefined : convertResponsesTools(placement.immediate, toolOptions)
|
|
232
|
-
return { input, imageMap, tools }
|
|
239
|
+
return { input, imageMap, tools, model, grammarToolInputProperties, deferredToolsMode }
|
|
233
240
|
}
|
|
234
241
|
|
|
235
242
|
export function responsesTools(tools) {
|
package/lib/index.js
CHANGED
|
@@ -49,8 +49,9 @@ import {
|
|
|
49
49
|
resolveModelImageSupport,
|
|
50
50
|
serializeDshMessages,
|
|
51
51
|
} from './dsh-responses.js'
|
|
52
|
-
import { requestNativeCompaction } from './compact-v2.js'
|
|
53
|
-
import {
|
|
52
|
+
import { mergeFeatureHeader, requestNativeCompaction } from './compact-v2.js'
|
|
53
|
+
import { buildResponsesBody } from './responses-request.js'
|
|
54
|
+
import { managedFailureChunk, streamResponsesRequest } from './responses-stream.js'
|
|
54
55
|
import {
|
|
55
56
|
checkpointStateForMessage,
|
|
56
57
|
compactCheckpointId,
|
|
@@ -94,7 +95,6 @@ const SETTINGS_NS = settingsNamespace('lcx-codex')
|
|
|
94
95
|
const ADVANCED_HOSTED_TOOL = 'websearch_gpt_advanced'
|
|
95
96
|
const ALPHA_TOOL = 'websearch_alpha'
|
|
96
97
|
const COMPACTION_DIRECTIVE = 'You are now acting as a compaction engine'
|
|
97
|
-
const bypassReplayOptions = new WeakSet()
|
|
98
98
|
const hostedSearchRouteContext = new AsyncLocalStorage()
|
|
99
99
|
|
|
100
100
|
function dshHome() { return process.env.DSH_HOME ?? join(homedir(), '.dsh') }
|
|
@@ -107,6 +107,7 @@ export const Config = z.object({
|
|
|
107
107
|
baseURL: z.string().default('https://api.lcxbot.com/v1'),
|
|
108
108
|
apiKeyEnv: z.string().default('LCX_API_KEY'),
|
|
109
109
|
model: z.string().default('gpt-5.6-sol'),
|
|
110
|
+
supportsExplicitPromptCacheMode: z.boolean().default(false),
|
|
110
111
|
legacyCheckpointPath: z.string().default(''),
|
|
111
112
|
checkpointPath: z.string().default(''),
|
|
112
113
|
alphaCapabilityPath: z.string().default(''),
|
|
@@ -155,6 +156,7 @@ function normalizeConfig(input = {}) {
|
|
|
155
156
|
baseURL: String(input.baseURL || 'https://api.lcxbot.com/v1').replace(/\/+$/u, ''),
|
|
156
157
|
apiKeyEnv: input.apiKeyEnv || 'LCX_API_KEY',
|
|
157
158
|
model: input.model || 'gpt-5.6-sol',
|
|
159
|
+
supportsExplicitPromptCacheMode: input.supportsExplicitPromptCacheMode === true,
|
|
158
160
|
headers: input.headers && typeof input.headers === 'object' ? { ...input.headers } : {},
|
|
159
161
|
legacyCheckpointPath: legacy,
|
|
160
162
|
alphaCapabilityPath: input.alphaCapabilityPath || defaultAlphaCapabilityPath(),
|
|
@@ -370,15 +372,16 @@ function mergeMap(target, source) { for (const [key, value] of source ?? []) tar
|
|
|
370
372
|
async function serializeNativeAware(messages, route, routeConfig, ctx, options = {}) {
|
|
371
373
|
const session = sessionFor(ctx, route.sessionId)
|
|
372
374
|
const imageSupport = await resolveModelImageSupport(ctx, route, options.signal)
|
|
373
|
-
const input = []; const imageMap = new Map(); let nativeTools
|
|
375
|
+
const input = []; const imageMap = new Map(); let nativeTools; let nativeModel; let grammarToolInputProperties
|
|
374
376
|
let normal = []
|
|
375
377
|
const serializeOptions = (imageMapOverride, extra = {}) => requestImageOptions(routeConfig, imageSupport, options.signal, imageMapOverride, { route, tools: options.tools, responsesCompat: routeConfig.responsesCompat, ...extra })
|
|
376
378
|
const prelude = await serializeDshMessages([], ctx, serializeOptions(undefined, { systemPrompt: options.system, includeSystemPrompt: true }))
|
|
377
|
-
|
|
379
|
+
const ephemeralPreludeItemCount = prelude.input.length
|
|
380
|
+
input.push(...prelude.input); nativeTools = prelude.tools; nativeModel = prelude.model; grammarToolInputProperties = prelude.grammarToolInputProperties
|
|
378
381
|
const flush = async () => {
|
|
379
382
|
if (!normal.length) return
|
|
380
383
|
const serialized = await serializeDshMessages(normal, ctx, serializeOptions())
|
|
381
|
-
nativeTools = serialized.tools ?? nativeTools
|
|
384
|
+
nativeTools = serialized.tools ?? nativeTools; nativeModel = serialized.model ?? nativeModel; grammarToolInputProperties = serialized.grammarToolInputProperties ?? grammarToolInputProperties
|
|
382
385
|
input.push(...serialized.input); mergeMap(imageMap, serialized.imageMap); normal = []
|
|
383
386
|
}
|
|
384
387
|
for (const message of messages ?? []) {
|
|
@@ -439,7 +442,8 @@ async function serializeNativeAware(messages, route, routeConfig, ctx, options =
|
|
|
439
442
|
normal.push(message)
|
|
440
443
|
}
|
|
441
444
|
await flush()
|
|
442
|
-
|
|
445
|
+
if (!nativeModel) throw Object.assign(new Error('LCX could not resolve the Pi Responses model descriptor'), { code: 'LCX_RESPONSES_MODEL_UNAVAILABLE' })
|
|
446
|
+
return { input, ephemeralPreludeItemCount, imageMap, imageSupport, tools: nativeTools, model: nativeModel, grammarToolInputProperties }
|
|
443
447
|
}
|
|
444
448
|
|
|
445
449
|
function fallbackEligible(error, signal) {
|
|
@@ -461,10 +465,12 @@ async function* remoteCompactionStream(options, routeConfig, state, ctx, next, r
|
|
|
461
465
|
const result = await requestNativeCompaction({
|
|
462
466
|
baseURL: routeConfig.baseURL,
|
|
463
467
|
model: route.model,
|
|
468
|
+
modelDescriptor: prepared.model,
|
|
464
469
|
input: prepared.input,
|
|
465
470
|
tools: prepared.tools ?? options.tools,
|
|
466
471
|
promptCacheKey: promptCacheKey(route, routeConfig),
|
|
467
472
|
promptCacheRetention: promptCacheRetention(routeConfig),
|
|
473
|
+
cacheRetention: routeConfig.cacheRetention,
|
|
468
474
|
reasoningEffort: generation.reasoningEffort,
|
|
469
475
|
temperature: generation.temperature,
|
|
470
476
|
maxTokens: generation.maxTokens,
|
|
@@ -477,7 +483,7 @@ async function* remoteCompactionStream(options, routeConfig, state, ctx, next, r
|
|
|
477
483
|
})
|
|
478
484
|
const session = sessionFor(ctx, route.sessionId)
|
|
479
485
|
const block = createNativeCheckpointBlock({
|
|
480
|
-
session, route, result, input: prepared.input, imageMap: prepared.imageMap,
|
|
486
|
+
session, route, result, input: prepared.input, ephemeralPreludeItemCount: prepared.ephemeralPreludeItemCount, imageMap: prepared.imageMap,
|
|
481
487
|
retentionOptions: {
|
|
482
488
|
tokenBudget: routeConfig.nativeRetentionTokenBudget,
|
|
483
489
|
assistantTokenReserve: routeConfig.assistantRetentionTokenReserve,
|
|
@@ -522,6 +528,11 @@ function pressurePolicy(state, config) {
|
|
|
522
528
|
return { auto, emergency: Math.min(99, emergency) }
|
|
523
529
|
}
|
|
524
530
|
|
|
531
|
+
export function compactionPressureBand(totalTokens, contextWindow, policy) {
|
|
532
|
+
const ratioPercent = totalTokens / contextWindow * 100
|
|
533
|
+
return { ratioPercent, band: ratioPercent < policy.auto ? 'below' : ratioPercent < policy.emergency ? 'native' : 'emergency' }
|
|
534
|
+
}
|
|
535
|
+
|
|
525
536
|
function adjustedCompactionConfig(config, target, thresholdRatio) {
|
|
526
537
|
if (!config || typeof config !== 'object') return config
|
|
527
538
|
const modelPolicies = Array.isArray(config.modelPolicies)
|
|
@@ -549,7 +560,7 @@ function patchCompactionPressureService(compactionValue, state, getConfig, ctx,
|
|
|
549
560
|
return original.call(this, agentArg, trigger, activeSignal)
|
|
550
561
|
}
|
|
551
562
|
return record.mutex.run(activeSignal, async () => {
|
|
552
|
-
if (trigger !== 'pressure' || !state.enabled || !state.
|
|
563
|
+
if (trigger !== 'pressure' || !state.enabled || !state.autoCompaction) return callOriginal()
|
|
553
564
|
const config = getConfig()
|
|
554
565
|
const target = routedTargetForAgent(agentArg, config)
|
|
555
566
|
if (!target || !resolveResponsesRouteConfig(ctx, target, config)) return callOriginal()
|
|
@@ -561,11 +572,12 @@ function patchCompactionPressureService(compactionValue, state, getConfig, ctx,
|
|
|
561
572
|
if (!Number.isFinite(contextWindow) || contextWindow <= 0) return callOriginal()
|
|
562
573
|
const totalTokens = Number(tokenMeter.measure(agentArg.session)?.totalTokens ?? 0)
|
|
563
574
|
const { auto, emergency } = pressurePolicy(state, config)
|
|
564
|
-
const
|
|
565
|
-
|
|
575
|
+
const pressure = compactionPressureBand(totalTokens, contextWindow, { auto, emergency })
|
|
576
|
+
const ratioPercent = pressure.ratioPercent
|
|
577
|
+
if (pressure.band === 'below') return null
|
|
566
578
|
const prunerState = toolResultPrunerState(resolveAgentService(ctx, agentArg, 'toolResultPruner') ?? resolveContextService(this?.ctx, 'toolResultPruner'))
|
|
567
579
|
const configState = compactionConfigState(this)
|
|
568
|
-
const nativeFirst =
|
|
580
|
+
const nativeFirst = pressure.band === 'native'
|
|
569
581
|
const noOpPrune = () => ({ pruned: [], charsRemoved: 0 })
|
|
570
582
|
const prunerPatch = nativeFirst ? patchToolResultPruner(prunerState, noOpPrune) : undefined
|
|
571
583
|
const configPatch = patchCompactionConfig(configState, originalConfig => adjustedCompactionConfig(originalConfig, target, auto / 100))
|
|
@@ -604,29 +616,51 @@ function visibleWebSearchTimeout(state, getConfig) {
|
|
|
604
616
|
function messagesContainNativeCheckpoint(messages, session) { return Boolean(session && (messages ?? []).some((message) => checkpointStateForMessage(session, message))) }
|
|
605
617
|
function messagesContainLegacyCheckpoint(messages) { return (messages ?? []).some((message) => Boolean(legacyCheckpointId(message))) }
|
|
606
618
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
if (!llm?.stream) throw Object.assign(new Error('LCX portable replay requires ctx.llm.stream'), { code: 'LCX_CHECKPOINT_REPLAY_UNAVAILABLE' })
|
|
610
|
-
const rewritten = { ...options, messages }
|
|
611
|
-
bypassReplayOptions.add(rewritten)
|
|
612
|
-
const stream = await llm.stream(rewritten)
|
|
613
|
-
for await (const chunk of stream) yield chunk
|
|
619
|
+
function inputHasNativeState(input) {
|
|
620
|
+
return (input ?? []).some((item) => item?.type === 'compaction')
|
|
614
621
|
}
|
|
615
622
|
|
|
616
|
-
async function*
|
|
623
|
+
async function* managedResponsesStream(options, routeConfig, ctx) {
|
|
617
624
|
const route = currentRoute(options, routeConfig)
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
+
try {
|
|
626
|
+
if (options.stop !== undefined) throw Object.assign(new Error('LCX Responses does not support GenerateOptions.stop'), { code: 'LCX_RESPONSES_UNSUPPORTED_OPTION' })
|
|
627
|
+
const prepared = await serializeNativeAware(options.messages, route, routeConfig, ctx, { signal: options.signal, system: options.system, tools: options.tools })
|
|
628
|
+
const cacheSessionId = promptCacheSessionId(route, routeConfig)
|
|
629
|
+
const headers = await authenticatedHeaders(ctx, routeConfig, cacheSessionId, cacheSessionId === undefined ? null : undefined)
|
|
630
|
+
const body = buildResponsesBody({
|
|
631
|
+
model: prepared.model,
|
|
632
|
+
input: prepared.input,
|
|
633
|
+
tools: prepared.tools ?? options.tools,
|
|
634
|
+
sessionId: cacheSessionId,
|
|
635
|
+
promptCacheKey: promptCacheKey(route, routeConfig),
|
|
636
|
+
promptCacheRetention: promptCacheRetention(routeConfig),
|
|
637
|
+
cacheRetention: routeConfig.cacheRetention,
|
|
638
|
+
reasoningEffort: options.reasoningEffort,
|
|
639
|
+
temperature: options.temperature,
|
|
640
|
+
maxTokens: options.maxTokens,
|
|
641
|
+
})
|
|
642
|
+
const nativeReplay = inputHasNativeState(prepared.input)
|
|
643
|
+
if (nativeReplay) { body.tool_choice = 'auto'; body.parallel_tool_calls = true }
|
|
644
|
+
yield* streamResponsesRequest({
|
|
645
|
+
baseURL: routeConfig.baseURL,
|
|
646
|
+
provider: route.provider,
|
|
647
|
+
model: route.model,
|
|
648
|
+
piModel: prepared.model,
|
|
649
|
+
body,
|
|
650
|
+
grammarToolInputProperties: prepared.grammarToolInputProperties,
|
|
651
|
+
headers: nativeReplay ? mergeFeatureHeader(headers) : headers,
|
|
652
|
+
signal: options.signal,
|
|
653
|
+
timeoutMs: routeConfig.timeoutMs,
|
|
654
|
+
maxAttempts: 1,
|
|
655
|
+
maxResponseBytes: routeConfig.maxResponseBytes,
|
|
656
|
+
})
|
|
657
|
+
} catch (error) {
|
|
658
|
+
yield managedFailureChunk(error, options.signal)
|
|
625
659
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
yield
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function* unavailableManagedRouteStream(options) {
|
|
663
|
+
yield managedFailureChunk(Object.assign(new Error('LCX is enabled but the selected DSH route cannot be resolved as an authenticated OpenAI Responses wire route'), { code: 'LCX_RESPONSES_ROUTE_UNAVAILABLE' }), options.signal)
|
|
630
664
|
}
|
|
631
665
|
|
|
632
666
|
function installInjected(ctx, configInput = {}) {
|
|
@@ -733,22 +767,14 @@ function installInjected(ctx, configInput = {}) {
|
|
|
733
767
|
}, { global: true })
|
|
734
768
|
|
|
735
769
|
ctx.on('llm/stream', (options, next) => {
|
|
736
|
-
if (
|
|
737
|
-
if (!state.enabled || !state.remoteCompaction || options.purpose === 'session-title') return next()
|
|
770
|
+
if (!state.enabled || options.purpose === 'session-title') return next()
|
|
738
771
|
const routeConfig = resolveResponsesRouteConfig(ctx, options, runtimeConfig)
|
|
739
772
|
if (options.purpose === 'compaction') {
|
|
740
|
-
if (!routeConfig) return
|
|
773
|
+
if (!routeConfig) return unavailableManagedRouteStream(options)
|
|
741
774
|
return remoteCompactionStream(options, routeConfig, state, ctx, next, requestHeaders)
|
|
742
775
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const hasLegacy = messagesContainLegacyCheckpoint(options.messages)
|
|
746
|
-
if (!hasNative && !hasLegacy) return next()
|
|
747
|
-
if (!routeConfig) {
|
|
748
|
-
if (hasNative && session) return recursiveLlmStream(ctx, options, rewriteCheckpointsPortable(options.messages, session, { maxChars: runtimeConfig.portableReplayMaxChars }))
|
|
749
|
-
return next()
|
|
750
|
-
}
|
|
751
|
-
return replayCheckpointStream(options, routeConfig, ctx)
|
|
776
|
+
if (!routeConfig) return unavailableManagedRouteStream(options)
|
|
777
|
+
return managedResponsesStream(options, routeConfig, ctx)
|
|
752
778
|
})
|
|
753
779
|
|
|
754
780
|
ctx.effect?.(() => async () => {
|
package/lib/native-checkpoint.js
CHANGED
|
@@ -63,7 +63,7 @@ import { estimateBudgetItem, portableBudgetError, portableTokenCeiling } from '.
|
|
|
63
63
|
* @property {unknown} [retainedAssistantCount]
|
|
64
64
|
*/
|
|
65
65
|
/** @typedef {{ compaction: CompactionItem }} NativeCompactionResult */
|
|
66
|
-
/** @typedef {{ session: NativeSession, route: RouteIdentity, result: NativeCompactionResult, input?: unknown[], imageMap?: unknown, retentionOptions?: RetentionOptions }} CreateCheckpointOptions */
|
|
66
|
+
/** @typedef {{ session: NativeSession, route: RouteIdentity, result: NativeCompactionResult, input?: unknown[], ephemeralPreludeItemCount?: number, imageMap?: unknown, retentionOptions?: RetentionOptions }} CreateCheckpointOptions */
|
|
67
67
|
/** @typedef {Error & { code?: string }} LcxError */
|
|
68
68
|
|
|
69
69
|
export const NATIVE_BLOCK_TYPE = 'lcx-native-compaction-v5'
|
|
@@ -99,6 +99,8 @@ function assistantTextParts(item) { if (!isObject(item) || item.type !== 'messag
|
|
|
99
99
|
function isRetainedAssistantItem(item) { return assistantTextParts(item).length > 0 }
|
|
100
100
|
/** @param {unknown} item */
|
|
101
101
|
function assistantText(item) { return assistantTextParts(item).map((part) => part.text).join('') }
|
|
102
|
+
/** @param {unknown} item */
|
|
103
|
+
function retainedAssistantPhase(item) { const phase = isObject(item) ? item.phase : undefined; return phase === 'commentary' || phase === 'final_answer' ? phase : undefined }
|
|
102
104
|
|
|
103
105
|
/**
|
|
104
106
|
* @param {unknown} item
|
|
@@ -107,16 +109,22 @@ function assistantText(item) { return assistantTextParts(item).map((part) => par
|
|
|
107
109
|
*/
|
|
108
110
|
function truncateVisibleAssistantItem(item, maxTokens = ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP) {
|
|
109
111
|
const text = assistantText(item); if (!text) return undefined
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
112
|
+
const phase = retainedAssistantPhase(item)
|
|
113
|
+
const providerId = isObject(item) && typeof item.id === 'string' && item.id ? item.id : undefined
|
|
114
|
+
/** @param {string} retained @param {boolean} preserveProviderId */
|
|
115
|
+
const candidate = (retained, preserveProviderId) => ({
|
|
116
|
+
type: 'message', role: 'assistant', content: [{ type: 'output_text', text: retained }],
|
|
117
|
+
...(phase ? { phase } : {}),
|
|
118
|
+
...(preserveProviderId && providerId ? { id: providerId } : {}),
|
|
119
|
+
})
|
|
120
|
+
const full = candidate(text, true)
|
|
113
121
|
if ((estimatedItemTokens(full) ?? Infinity) <= maxTokens) return full
|
|
114
122
|
const marker = '\n…[LCX retained answer truncated]…\n'
|
|
115
123
|
/** @param {number} count */
|
|
116
124
|
const shortened = (count) => {
|
|
117
125
|
const available = Math.max(0, count - marker.length)
|
|
118
126
|
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))}
|
|
127
|
+
return candidate(`${text.slice(0, head)}${marker}${text.slice(Math.max(head, text.length - tail))}`, false)
|
|
120
128
|
}
|
|
121
129
|
let low = 0; let high = text.length; let best
|
|
122
130
|
while (low <= high) {
|
|
@@ -184,7 +192,7 @@ export function activeCompactionId(session) { if (!session?.events) return undef
|
|
|
184
192
|
* @param {CreateCheckpointOptions} options
|
|
185
193
|
* @returns {NativeCheckpointV5}
|
|
186
194
|
*/
|
|
187
|
-
export function createNativeCheckpointBlock({ session, route, result, input = [], imageMap, retentionOptions = {} }) {
|
|
195
|
+
export function createNativeCheckpointBlock({ session, route, result, input = [], ephemeralPreludeItemCount = 0, imageMap, retentionOptions = {} }) {
|
|
188
196
|
const compactionId = activeCompactionId(session)
|
|
189
197
|
if (!compactionId) {
|
|
190
198
|
/** @type {LcxError} */
|
|
@@ -192,7 +200,10 @@ export function createNativeCheckpointBlock({ session, route, result, input = []
|
|
|
192
200
|
error.code = 'LCX_COMPACTION_ID_UNAVAILABLE'
|
|
193
201
|
throw error
|
|
194
202
|
}
|
|
195
|
-
|
|
203
|
+
if (!Number.isSafeInteger(ephemeralPreludeItemCount) || ephemeralPreludeItemCount < 0 || ephemeralPreludeItemCount > input.length) {
|
|
204
|
+
throw Object.assign(new Error('Native compaction prelude provenance is invalid'), { code: 'LCX_COMPACTION_PRELUDE_PROVENANCE_INVALID' })
|
|
205
|
+
}
|
|
206
|
+
const retention = retainedConversationPlan(input.slice(ephemeralPreludeItemCount), retentionOptions)
|
|
196
207
|
/** @type {NativeOutputItem[]} */
|
|
197
208
|
const nativeOutput = persistNativeImageReferences([...retention.items, structuredClone(result.compaction)], imageMap)
|
|
198
209
|
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() }
|