dsh-lcx-codex 0.4.1 → 0.4.2-pre.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/ARCHITECTURE.md +24 -0
- package/CHANGELOG.md +15 -0
- 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/ARCHITECTURE.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
# GPT Responses full-lifecycle ownership — 0.4.2-pre.1
|
|
2
|
+
|
|
3
|
+
## Product contract
|
|
4
|
+
|
|
5
|
+
`LCX OFF` leaves every LLM request on the native DSH adapter path. `LCX ON` is the ownership switch for the selected GPT Responses conversation: LCX owns the final Responses wire from the first ordinary turn through tool loops, Native V2 compaction, native replay, portable model migration, and restart/resume. Users turn LCX off before selecting a non-GPT model; DSH model switching itself remains hot and requires no restart.
|
|
6
|
+
|
|
7
|
+
## Responsibility boundary
|
|
8
|
+
|
|
9
|
+
- **DSH** remains the Agent, Session, request assembly, model/settings/credential, tool-execution, attachment, pressure-policy and compaction-transaction owner.
|
|
10
|
+
- **The rc.2 compatibility seam** projects DSH `GenerateOptions` and durable replay content into Pi's provider-neutral `Context`. It exists only because DSH 0.1.1-rc.2 does not publish `toPiContext()` / `toStreamChunks()` as stable package APIs.
|
|
11
|
+
- **Plugin Pi 0.84.3** owns canonical Responses message/tool serialization and stream semantics: reasoning, IDs, custom tools, strict/grammar tools, `additional_tools`, `tool_search`, namespace, cache semantics and event parsing.
|
|
12
|
+
- **LCX** owns the final body, HTTP/SSE wire, safe error normalization, ordinary/compact/replay orchestration, Remote V2 opaque state and checkpoint compatibility.
|
|
13
|
+
|
|
14
|
+
## Unified request path
|
|
15
|
+
|
|
16
|
+
`serializeNativeAware()` is the shared history compiler. With no checkpoint it serializes ordinary history; with a compatible checkpoint it injects retained native history plus the opaque compaction item; with an incompatible GPT model/route it reconstructs portable DSH history. All three representations continue through one standard LCX request builder and one LCX transport/parser bridge. Compact is the standard request plus `compaction_trigger` and the Remote V2 feature header; native replay is the standard request plus opaque history. Portable migration never recursively bypasses LCX back to `PiAiAdapter`.
|
|
17
|
+
|
|
18
|
+
Ordinary and replay requests perform one provider attempt and emit DSH terminal failure chunks using the host taxonomy, leaving visible retries to DSH's `agent/request-error` policy. Native compaction retains its bounded idempotent retry and first-checkpoint Basic fallback. Genuine upstream tool namespace is stored only in adapter-private replay metadata because DSH rc.2's visible ToolCall block has no namespace field.
|
|
19
|
+
|
|
20
|
+
## Gateway continuity
|
|
21
|
+
|
|
22
|
+
The stable DSH session id drives Pi-compatible prompt-cache/session-affinity fields. Sub2API v0.1.183 accepts `session_id`, `session-id`, and `x-client-request-id` for Codex account stickiness and repairs recovered custom-tool/tool-search item ID prefixes. Runtime acceptance therefore verifies both LCX wire continuity and gateway account continuity; release prose is not treated as the wire contract.
|
|
23
|
+
|
|
24
|
+
---
|
|
1
25
|
|
|
2
26
|
## rc.6 pressure coordination
|
|
3
27
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.2-pre.1 - candidate under runtime acceptance
|
|
4
|
+
|
|
5
|
+
### Architecture
|
|
6
|
+
|
|
7
|
+
- Make the LCX master switch the GPT Responses lifecycle ownership switch: when enabled, the first ordinary Agent turn, tools, Native V2 compaction, native replay and portable GPT-model migration share one LCX final-wire path.
|
|
8
|
+
- Upgrade only the plugin's direct Pi dependency to `0.84.3`; DSH 0.1.1-rc.2 and its host Pi 0.82.x remain isolated and unchanged.
|
|
9
|
+
- Reuse Pi 0.84 canonical Responses serializers and stream semantics for strict/grammar/custom tools, deferred `additional_tools` / `tool_search`, namespace, reasoning, IDs and cache behavior.
|
|
10
|
+
- Add one standard Responses request builder and shared LCX HTTP/SSE transport/parser bridge; remove the old portable recursive bypass back to the DSH adapter.
|
|
11
|
+
- Preserve checkpoint v5, DSH compaction transactions, Native-first pressure coordination, first-checkpoint Basic fallback, restart/resume and Search/#20 architecture.
|
|
12
|
+
- Normalize ordinary/replay failures into DSH terminal taxonomy and keep DSH as the visible retry owner.
|
|
13
|
+
|
|
14
|
+
### Validation state
|
|
15
|
+
|
|
16
|
+
- Engineering gates pass on the private work branch; exact commit-bound DSH + Sub2API runtime acceptance remains mandatory before integration, publication or a `VERIFIED` compatibility claim.
|
|
17
|
+
|
|
3
18
|
## 0.4.1 - 2026-08-25
|
|
4
19
|
|
|
5
20
|
### Stable promotion
|
package/cordis.patch.yml
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
baseURL: https://api.lcxbot.com/v1
|
|
11
11
|
apiKeyEnv: LCX_API_KEY
|
|
12
12
|
model: gpt-5.6-sol
|
|
13
|
+
supportsExplicitPromptCacheMode: true
|
|
13
14
|
legacyCheckpointPath: !!js dshHomePath('storages/lcx-codex/checkpoints-v3.json')
|
|
14
15
|
alphaCapabilityPath: !!js dshHomePath('storages/lcx-codex/web-alpha-capabilities.json')
|
|
15
16
|
alphaRefPath: !!js dshHomePath('storages/lcx-codex/web-alpha-refs.json')
|
package/lib/client.js
CHANGED
|
@@ -32,8 +32,9 @@ window.__ModuleLoader__.load({
|
|
|
32
32
|
const copy = {
|
|
33
33
|
zh: {
|
|
34
34
|
title: 'Responses / Codex 能力',
|
|
35
|
-
desc: '
|
|
36
|
-
enabled: '
|
|
35
|
+
desc: 'LCX 开启后从第一轮普通请求开始接管当前 GPT Responses 会话,并统一管理普通请求、工具、Native V2 压缩和续接。',
|
|
36
|
+
enabled: '启用 LCX(接管当前 GPT Responses 会话)',
|
|
37
|
+
enabledHelp: '切换 Claude、Gemini、DeepSeek 等非 GPT 模型前先关闭 LCX;模型切换本身不需要重启 DSH。',
|
|
37
38
|
web: '使用 GPT Hosted Search 作为 DSH web_search 后端',
|
|
38
39
|
webHelp: '不会新增第二个普通搜索工具;普通 web_search 自动跟随当前 Agent 的 GPT Responses 模型。',
|
|
39
40
|
searchTimeout: 'web_search 超时(秒)',
|
|
@@ -42,8 +43,6 @@ window.__ModuleLoader__.load({
|
|
|
42
43
|
advancedHelp: '只在需要域名过滤、位置、search context、图片等原生 Hosted 参数时使用;默认关闭以保持工具 schema 稳定。',
|
|
43
44
|
alpha: '启用 Alpha command(websearch_alpha)',
|
|
44
45
|
alphaHelp: '仅 capability probe 对当前 endpoint/provider/model/schema 验证通过后才真正注册。',
|
|
45
|
-
compact: '启用 Native V2 远程压缩',
|
|
46
|
-
compactHelp: 'DSH 仍负责范围、事务和 /compact;Native state 保存在 DSH session log。',
|
|
47
46
|
autoCompact: '启用 Native-first 自动压缩',
|
|
48
47
|
autoCompactHelp: '到主动阈值前不让 DSH 的 80% tool-result prune 改写历史;到主动阈值后优先 Native V2。',
|
|
49
48
|
autoThreshold: 'Native 自动压缩阈值(%)',
|
|
@@ -52,14 +51,15 @@ window.__ModuleLoader__.load({
|
|
|
52
51
|
emergencyThresholdHelp: '建议 95%。达到这里才允许 DSH tool-result-pruner 先救场;必须高于 Native 阈值。',
|
|
53
52
|
fallback: 'Native 失败后回退 DSH basic compaction',
|
|
54
53
|
fallbackHelp: 'Remote-first:只有 Native 请求失败才调用 basic summary,不并行双跑。',
|
|
55
|
-
endpoint: '
|
|
56
|
-
model: '
|
|
54
|
+
endpoint: 'Responses 地址',
|
|
55
|
+
model: 'GPT 模型',
|
|
57
56
|
save: '保存', discard: '放弃修改', saving: '保存中…',
|
|
58
57
|
},
|
|
59
58
|
en: {
|
|
60
59
|
title: 'Responses / Codex capabilities',
|
|
61
|
-
desc: '
|
|
62
|
-
enabled: 'Enable
|
|
60
|
+
desc: 'When enabled, LCX owns the selected GPT Responses conversation from the first ordinary turn through tools, Native V2 compaction and continuation.',
|
|
61
|
+
enabled: 'Enable LCX (own current GPT Responses conversation)',
|
|
62
|
+
enabledHelp: 'Turn LCX off before switching to Claude, Gemini, DeepSeek, or another non-GPT model. Model switching itself does not require a DSH restart.',
|
|
63
63
|
web: 'Use GPT Hosted Search as DSH web_search backend',
|
|
64
64
|
webHelp: 'Keeps DSH web_search as the single ordinary search tool and follows the active Agent GPT Responses model.',
|
|
65
65
|
searchTimeout: 'web_search timeout (seconds)',
|
|
@@ -68,8 +68,6 @@ window.__ModuleLoader__.load({
|
|
|
68
68
|
advancedHelp: 'Only for native Hosted controls such as domains, location, context size and image search; off by default for stable tool schemas.',
|
|
69
69
|
alpha: 'Enable Alpha command (websearch_alpha)',
|
|
70
70
|
alphaHelp: 'Registered only after a matching capability probe.',
|
|
71
|
-
compact: 'Enable Native V2 remote compaction',
|
|
72
|
-
compactHelp: 'DSH still owns range/transactions and /compact; native state is persisted in the DSH session log.',
|
|
73
71
|
autoCompact: 'Enable Native-first automatic compaction',
|
|
74
72
|
autoCompactHelp: 'Suppresses the stock 80% tool-result prune before the proactive threshold, then prefers Native V2.',
|
|
75
73
|
autoThreshold: 'Native auto-compaction threshold (%)',
|
|
@@ -78,7 +76,7 @@ window.__ModuleLoader__.load({
|
|
|
78
76
|
emergencyThresholdHelp: 'Default 95%. DSH tool-result pruning is allowed only in this emergency zone; must exceed the Native threshold.',
|
|
79
77
|
fallback: 'Fall back to DSH basic compaction on Native failure',
|
|
80
78
|
fallbackHelp: 'Remote-first: basic summary runs only after Native failure, never in parallel.',
|
|
81
|
-
endpoint: '
|
|
79
|
+
endpoint: 'Responses endpoint', model: 'GPT model',
|
|
82
80
|
save: 'Save', discard: 'Discard', saving: 'Saving…',
|
|
83
81
|
},
|
|
84
82
|
}
|
|
@@ -96,7 +94,7 @@ window.__ModuleLoader__.load({
|
|
|
96
94
|
field(name) { return this.draftValue()[name] }
|
|
97
95
|
projection() {
|
|
98
96
|
const s = this.snapshot()
|
|
99
|
-
const bools = ['enabled','webSearch','advancedHostedSearch','alphaSearch','
|
|
97
|
+
const bools = ['enabled','webSearch','advancedHostedSearch','alphaSearch','fallbackToBasicCompaction','autoCompaction']
|
|
100
98
|
return {
|
|
101
99
|
available: s.status === 'ready', writable: s.writable, dirty: this.dirty, saving: this.saving,
|
|
102
100
|
...Object.fromEntries(bools.map((f) => [f, { value: f === 'fallbackToBasicCompaction' || f === 'autoCompaction' ? this.field(f) !== false : Boolean(this.field(f)) }])),
|
|
@@ -139,18 +137,17 @@ window.__ModuleLoader__.load({
|
|
|
139
137
|
React.createElement('button', { className:'lcx-head', type:'button', onClick:()=>setOpen(!open) }, React.createElement('strong',null,t.title), React.createElement('span',null,open?'⌃':'⌄')),
|
|
140
138
|
open ? React.createElement('div', { className:'lcx-body' },
|
|
141
139
|
React.createElement('p', { className:'lcx-help' }, t.desc),
|
|
142
|
-
React.createElement(Row, { id:'lcx-enabled', label:t.enabled, checked:s.enabled.value, disabled, onChange:v=>props.edit('enabled',v) }),
|
|
140
|
+
React.createElement(Row, { id:'lcx-enabled', label:t.enabled, help:t.enabledHelp, checked:s.enabled.value, disabled, onChange:v=>props.edit('enabled',v) }),
|
|
143
141
|
React.createElement(Row, { id:'lcx-web', label:t.web, help:t.webHelp, checked:s.webSearch.value, disabled:disabled||!s.enabled.value, onChange:v=>props.edit('webSearch',v) }),
|
|
144
142
|
React.createElement('div', { className:'lcx-fields' },
|
|
145
143
|
React.createElement(Field, { label:t.searchTimeout, help:t.searchTimeoutHelp, type:'number', min:30, max:600, step:30, value:s.webSearchTimeoutSeconds, disabled:disabled||!s.enabled.value||!s.webSearch.value, onChange:numeric('webSearchTimeoutSeconds') })),
|
|
146
144
|
React.createElement(Row, { id:'lcx-advanced', label:t.advanced, help:t.advancedHelp, checked:s.advancedHostedSearch.value, disabled:disabled||!s.enabled.value||!s.webSearch.value, onChange:v=>props.edit('advancedHostedSearch',v) }),
|
|
147
145
|
React.createElement(Row, { id:'lcx-alpha', label:t.alpha, help:t.alphaHelp, checked:s.alphaSearch.value, disabled:disabled||!s.enabled.value, onChange:v=>props.edit('alphaSearch',v) }),
|
|
148
|
-
React.createElement(Row, { id:'lcx-compact', label:t.
|
|
149
|
-
React.createElement(Row, { id:'lcx-auto-compact', label:t.autoCompact, help:t.autoCompactHelp, checked:s.autoCompaction.value, disabled:disabled||!s.enabled.value||!s.remoteCompaction.value, onChange:v=>props.edit('autoCompaction',v) }),
|
|
146
|
+
React.createElement(Row, { id:'lcx-auto-compact', label:t.autoCompact, help:t.autoCompactHelp, checked:s.autoCompaction.value, disabled:disabled||!s.enabled.value, onChange:v=>props.edit('autoCompaction',v) }),
|
|
150
147
|
React.createElement('div', { className:'lcx-fields' },
|
|
151
|
-
React.createElement(Field, { label:t.autoThreshold, help:t.autoThresholdHelp, type:'number', min:85, max:95, step:1, value:s.autoCompactionThresholdPercent, disabled:disabled||!s.enabled.value||!s.
|
|
152
|
-
React.createElement(Field, { label:t.emergencyThreshold, help:t.emergencyThresholdHelp, type:'number', min:90, max:99, step:1, value:s.emergencyPruneThresholdPercent, disabled:disabled||!s.enabled.value||!s.
|
|
153
|
-
React.createElement(Row, { id:'lcx-fallback', label:t.fallback, help:t.fallbackHelp, checked:s.fallbackToBasicCompaction.value, disabled:disabled||!s.enabled.value
|
|
148
|
+
React.createElement(Field, { label:t.autoThreshold, help:t.autoThresholdHelp, type:'number', min:85, max:95, step:1, value:s.autoCompactionThresholdPercent, disabled:disabled||!s.enabled.value||!s.autoCompaction.value, onChange:numeric('autoCompactionThresholdPercent') }),
|
|
149
|
+
React.createElement(Field, { label:t.emergencyThreshold, help:t.emergencyThresholdHelp, type:'number', min:90, max:99, step:1, value:s.emergencyPruneThresholdPercent, disabled:disabled||!s.enabled.value||!s.autoCompaction.value, onChange:numeric('emergencyPruneThresholdPercent') })),
|
|
150
|
+
React.createElement(Row, { id:'lcx-fallback', label:t.fallback, help:t.fallbackHelp, checked:s.fallbackToBasicCompaction.value, disabled:disabled||!s.enabled.value, onChange:v=>props.edit('fallbackToBasicCompaction',v) }),
|
|
154
151
|
React.createElement('div', { className:'lcx-fields' },
|
|
155
152
|
React.createElement(Field, { label:t.endpoint, value:s.baseURL, disabled, onChange:e=>props.edit('baseURL',e.target.value) }),
|
|
156
153
|
React.createElement(Field, { label:t.model, value:s.model, disabled, onChange:e=>props.edit('model',e.target.value) })),
|
package/lib/compact-v2.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import { consumeSse, fetchSseWithRetry } from './transport.js'
|
|
4
|
-
import {
|
|
4
|
+
import { buildCompactionResponsesBody, responsesGenerationEnvelope as standardGenerationEnvelope } from './responses-request.js'
|
|
5
5
|
|
|
6
6
|
/** @typedef {Record<string, unknown>} UnknownRecord */
|
|
7
7
|
/** @typedef {Record<string, string>} HeaderMap */
|
|
@@ -16,11 +16,13 @@ import { responsesTools } from './dsh-responses.js'
|
|
|
16
16
|
/**
|
|
17
17
|
* @typedef {object} NativeCompactionBodyOptions
|
|
18
18
|
* @property {string} model
|
|
19
|
+
* @property {unknown} [modelDescriptor]
|
|
19
20
|
* @property {unknown[]} input
|
|
20
21
|
* @property {string} [instructions]
|
|
21
22
|
* @property {unknown} [tools]
|
|
22
23
|
* @property {string} [promptCacheKey]
|
|
23
24
|
* @property {string} [promptCacheRetention]
|
|
25
|
+
* @property {'none' | 'short' | 'long'} [cacheRetention]
|
|
24
26
|
* @property {unknown} [reasoningEffort]
|
|
25
27
|
* @property {unknown} [temperature]
|
|
26
28
|
* @property {unknown} [maxTokens]
|
|
@@ -37,6 +39,7 @@ import { responsesTools } from './dsh-responses.js'
|
|
|
37
39
|
* @property {unknown[]} [tools]
|
|
38
40
|
* @property {string} [prompt_cache_key]
|
|
39
41
|
* @property {string} [prompt_cache_retention]
|
|
42
|
+
* @property {{ mode?: 'explicit', ttl?: '30m' }} [prompt_cache_options]
|
|
40
43
|
* @property {{ effort: string, summary: 'auto' }} [reasoning]
|
|
41
44
|
* @property {string[]} [include]
|
|
42
45
|
* @property {number} [temperature]
|
|
@@ -97,46 +100,27 @@ export function mergeFeatureHeader(headers = {}) {
|
|
|
97
100
|
* @param {GenerationControls} [controls]
|
|
98
101
|
* @returns {GenerationEnvelope}
|
|
99
102
|
*/
|
|
100
|
-
export function responsesGenerationEnvelope(
|
|
101
|
-
|
|
102
|
-
const result = {}
|
|
103
|
-
if (reasoningEffort !== undefined && reasoningEffort !== 'off') {
|
|
104
|
-
result.reasoning = { effort: String(reasoningEffort), summary: 'auto' }
|
|
105
|
-
result.include = ['reasoning.encrypted_content']
|
|
106
|
-
}
|
|
107
|
-
if (temperature !== undefined) {
|
|
108
|
-
if (!Number.isFinite(temperature)) throw fail('Responses temperature must be finite', 'LCX_COMPACT_INVALID_INPUT')
|
|
109
|
-
result.temperature = Number(temperature)
|
|
110
|
-
}
|
|
111
|
-
if (maxTokens !== undefined) {
|
|
112
|
-
if (!Number.isSafeInteger(maxTokens) || /** @type {number} */ (maxTokens) <= 0) throw fail('Responses maxTokens must be a positive safe integer', 'LCX_COMPACT_INVALID_INPUT')
|
|
113
|
-
result.max_output_tokens = Math.max(16, /** @type {number} */ (maxTokens))
|
|
114
|
-
}
|
|
115
|
-
return result
|
|
103
|
+
export function responsesGenerationEnvelope(controls = {}) {
|
|
104
|
+
return standardGenerationEnvelope(controls)
|
|
116
105
|
}
|
|
117
106
|
|
|
118
107
|
/**
|
|
119
108
|
* @param {NativeCompactionBodyOptions} options
|
|
120
109
|
* @returns {NativeCompactionBody}
|
|
121
110
|
*/
|
|
122
|
-
export function buildNativeCompactionBody({ model, input, instructions, tools, promptCacheKey, promptCacheRetention, reasoningEffort, temperature, maxTokens }) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
...(nativeTools !== undefined ? { tools: nativeTools } : {}),
|
|
136
|
-
...(promptCacheKey ? { prompt_cache_key: String(promptCacheKey) } : {}),
|
|
137
|
-
...(promptCacheRetention ? { prompt_cache_retention: String(promptCacheRetention) } : {}),
|
|
138
|
-
...responsesGenerationEnvelope({ reasoningEffort, temperature, maxTokens }),
|
|
139
|
-
}
|
|
111
|
+
export function buildNativeCompactionBody({ model, modelDescriptor, input, instructions, tools, promptCacheKey, promptCacheRetention, cacheRetention, reasoningEffort, temperature, maxTokens }) {
|
|
112
|
+
return /** @type {NativeCompactionBody} */ (/** @type {unknown} */ (buildCompactionResponsesBody({
|
|
113
|
+
model: /** @type {any} */ (modelDescriptor ?? model),
|
|
114
|
+
input,
|
|
115
|
+
instructions,
|
|
116
|
+
tools,
|
|
117
|
+
promptCacheKey,
|
|
118
|
+
promptCacheRetention,
|
|
119
|
+
cacheRetention: cacheRetention ?? (promptCacheKey ? (promptCacheRetention ? 'long' : 'short') : 'none'),
|
|
120
|
+
reasoningEffort,
|
|
121
|
+
temperature,
|
|
122
|
+
maxTokens,
|
|
123
|
+
})))
|
|
140
124
|
}
|
|
141
125
|
|
|
142
126
|
/**
|
|
@@ -260,8 +244,8 @@ export async function parseNativeCompactionSse(response, options = {}) {
|
|
|
260
244
|
/**
|
|
261
245
|
* @param {NativeCompactionRequestOptions} options
|
|
262
246
|
*/
|
|
263
|
-
export async function requestNativeCompaction({ baseURL, model, input, instructions, tools, promptCacheKey, promptCacheRetention, reasoningEffort, temperature, maxTokens, idempotencyKey, headers, signal, timeoutMs, maxAttempts, maxResponseBytes }) {
|
|
264
|
-
const body = buildNativeCompactionBody({ model, input, instructions, tools, promptCacheKey, promptCacheRetention, reasoningEffort, temperature, maxTokens })
|
|
247
|
+
export async function requestNativeCompaction({ baseURL, model, modelDescriptor, input, instructions, tools, promptCacheKey, promptCacheRetention, cacheRetention, reasoningEffort, temperature, maxTokens, idempotencyKey, headers, signal, timeoutMs, maxAttempts, maxResponseBytes }) {
|
|
248
|
+
const body = buildNativeCompactionBody({ model, modelDescriptor, input, instructions, tools, promptCacheKey, promptCacheRetention, cacheRetention, reasoningEffort, temperature, maxTokens })
|
|
265
249
|
const requestHeaders = mergeFeatureHeader({ ...headers, ...(idempotencyKey ? { 'idempotency-key': String(idempotencyKey) } : {}) })
|
|
266
250
|
return fetchSseWithRetry(`${String(baseURL).replace(/\/+$/u, '')}/responses`, body, requestHeaders, signal, timeoutMs, {
|
|
267
251
|
maxAttempts,
|
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() }
|