dsh-context-compression-improved 0.5.1 → 0.5.3
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/.gitattributes +1 -0
- package/CHANGELOG.ja.md +144 -83
- package/CHANGELOG.ko.md +143 -82
- package/CHANGELOG.md +278 -212
- package/CHANGELOG.zh.md +131 -77
- package/docs/installation.md +103 -103
- package/docs/installation.zh.md +100 -100
- package/package.json +1 -1
- package/packages/selector/cordis.patch.yml +5 -6
- package/packages/selector/lib/advisor-state.js +4 -231
- package/packages/selector/lib/client.d.ts +0 -24
- package/packages/selector/lib/client.js +6 -501
- package/packages/selector/lib/index.d.ts +4 -10
- package/packages/selector/lib/index.js +16 -234
- package/packages/selector/lib/pruner.d.ts +13 -248
- package/packages/selector/lib/pruner.js +148 -552
- package/packages/selector/src/client/EstimatorControls.tsx +0 -101
- package/packages/selector/src/client/index.ts +0 -17
- package/packages/selector/src/client/locales.ts +0 -38
- package/packages/selector/src/client/preset-options.ts +3 -2
- package/packages/selector/src/client/settings-section.tsx +8 -17
- package/packages/selector/src/index.ts +24 -271
- package/packages/selector/src/profiles.ts +4 -27
- package/packages/selector/src/pruner/state.ts +2 -25
- package/packages/selector/src/pruner.ts +75 -403
- package/packages/selector/src/runtime/audit.ts +27 -21
- package/packages/selector/src/runtime/config.ts +6 -32
- package/packages/selector/src/runtime/tokenpilot/advisor-prompt.ts +188 -188
- package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +149 -133
- package/packages/selector/src/runtime/tokenpilot/advisor.ts +419 -419
- package/packages/selector/src/runtime/tokenpilot/benefit.ts +200 -0
- package/packages/selector/src/runtime/types.ts +0 -17
- package/packages/selector/tests/advisor-report.host.spec.ts +223 -223
- package/packages/selector/tests/preset-options-write.client.spec.ts +7 -23
- package/packages/selector/tests/public/package-contract.client.spec.ts +20 -0
- package/packages/selector/tests/runtime/advice-never-withholds.host.spec.ts +232 -0
- package/packages/selector/tests/runtime/advisor-invariant.spec.ts +272 -272
- package/packages/selector/tests/runtime/advisor.spec.ts +226 -226
- package/packages/selector/tests/runtime/audit.spec.ts +35 -21
- package/packages/selector/tests/runtime/char-basis.spec.ts +30 -30
- package/packages/selector/tests/runtime/deprecated-preset-options.spec.ts +96 -0
- package/packages/selector/tests/runtime/tokenpilot/benefit.spec.ts +217 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +4 -5
- package/packages/selector/tests/settings-seat.client.spec.ts +45 -14
- package/scripts/toolclass-corpus-replay.mjs +281 -281
- package/packages/selector/src/client/ReviewOverlay.tsx +0 -320
- package/packages/selector/src/client/review-scope.ts +0 -16
- package/packages/selector/src/runtime/tokenpilot/proposal.ts +0 -267
- package/packages/selector/src/runtime/tokenpilot/review-queue.ts +0 -231
- package/packages/selector/src/runtime/tokenpilot/review-registry.ts +0 -117
- package/packages/selector/src/runtime/tokenpilot/review-storage.ts +0 -122
- package/packages/selector/tests/review-overlay.client.spec.tsx +0 -118
- package/packages/selector/tests/review-routes-registry.host.spec.ts +0 -142
- package/packages/selector/tests/review-routes.host.spec.ts +0 -290
- package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +0 -393
- package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +0 -382
- package/packages/selector/tests/runtime/tokenpilot/review-queue.spec.ts +0 -168
|
@@ -127,6 +127,11 @@ export function parsePresetOptionsSettings(value: unknown): PresetOptionsSetting
|
|
|
127
127
|
const allowed = new Set([
|
|
128
128
|
'dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'estimatorMode',
|
|
129
129
|
'estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey', 'estimatorTimeoutMs',
|
|
130
|
+
// Legacy keys of the retired human-gated review pipeline. They stay in the
|
|
131
|
+
// ACCEPTED set on purpose: the keys are persisted in existing settings
|
|
132
|
+
// documents, and rejecting them would make this plugin fail to load on an
|
|
133
|
+
// upgrade. They are read by nothing — the gate they configured no longer
|
|
134
|
+
// exists, and a reduction never blocks automatic processing.
|
|
130
135
|
'reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens',
|
|
131
136
|
'advisorMode', 'advisorTimeoutMs', 'advisorRefreshTurns', 'advisorScoreThreshold', 'advisorSampleLimit',
|
|
132
137
|
'advisorMinTokens',
|
|
@@ -135,7 +140,7 @@ export function parsePresetOptionsSettings(value: unknown): PresetOptionsSetting
|
|
|
135
140
|
if (unknown !== undefined) {
|
|
136
141
|
throw new TypeError(`Context-compression presetOptions: unknown key "${unknown}"`)
|
|
137
142
|
}
|
|
138
|
-
const booleans = ['dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState'
|
|
143
|
+
const booleans = ['dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState'] as const
|
|
139
144
|
for (const key of booleans) {
|
|
140
145
|
const entry = value[key]
|
|
141
146
|
if (entry !== undefined && typeof entry !== 'boolean') {
|
|
@@ -185,23 +190,6 @@ export function parsePresetOptionsSettings(value: unknown): PresetOptionsSetting
|
|
|
185
190
|
|| estimatorTimeoutMs < 100 || estimatorTimeoutMs > 60_000)) {
|
|
186
191
|
throw new TypeError('Context-compression presetOptions.estimatorTimeoutMs must be an integer between 100 and 60000')
|
|
187
192
|
}
|
|
188
|
-
const reviewTimeoutTurns = value.reviewTimeoutTurns
|
|
189
|
-
if (reviewTimeoutTurns !== undefined
|
|
190
|
-
&& (typeof reviewTimeoutTurns !== 'number' || !Number.isSafeInteger(reviewTimeoutTurns) || reviewTimeoutTurns < 1)) {
|
|
191
|
-
throw new TypeError('Context-compression presetOptions.reviewTimeoutTurns must be an integer of at least 1')
|
|
192
|
-
}
|
|
193
|
-
const cacheHitDiscountAlpha = value.cacheHitDiscountAlpha
|
|
194
|
-
if (cacheHitDiscountAlpha !== undefined
|
|
195
|
-
&& (typeof cacheHitDiscountAlpha !== 'number' || !Number.isFinite(cacheHitDiscountAlpha)
|
|
196
|
-
|| cacheHitDiscountAlpha <= 0 || cacheHitDiscountAlpha >= 1)) {
|
|
197
|
-
throw new TypeError('Context-compression presetOptions.cacheHitDiscountAlpha must be a number strictly between 0 and 1')
|
|
198
|
-
}
|
|
199
|
-
const reviewHighImpactTokens = value.reviewHighImpactTokens
|
|
200
|
-
if (reviewHighImpactTokens !== undefined
|
|
201
|
-
&& (typeof reviewHighImpactTokens !== 'number' || !Number.isSafeInteger(reviewHighImpactTokens)
|
|
202
|
-
|| reviewHighImpactTokens < 0)) {
|
|
203
|
-
throw new TypeError('Context-compression presetOptions.reviewHighImpactTokens must be a non-negative integer')
|
|
204
|
-
}
|
|
205
193
|
for (const key of ['estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey'] as const) {
|
|
206
194
|
const entry = value[key]
|
|
207
195
|
if (entry !== undefined && typeof entry !== 'string') {
|
|
@@ -221,10 +209,6 @@ export function parsePresetOptionsSettings(value: unknown): PresetOptionsSetting
|
|
|
221
209
|
if (value.estimatorBaseUrl !== undefined) result.estimatorBaseUrl = value.estimatorBaseUrl as string
|
|
222
210
|
if (value.estimatorApiKey !== undefined) result.estimatorApiKey = value.estimatorApiKey as string
|
|
223
211
|
if (estimatorTimeoutMs !== undefined) result.estimatorTimeoutMs = estimatorTimeoutMs as number
|
|
224
|
-
if (value.reviewMode !== undefined) result.reviewMode = value.reviewMode as boolean
|
|
225
|
-
if (reviewTimeoutTurns !== undefined) result.reviewTimeoutTurns = reviewTimeoutTurns as number
|
|
226
|
-
if (cacheHitDiscountAlpha !== undefined) result.cacheHitDiscountAlpha = cacheHitDiscountAlpha as number
|
|
227
|
-
if (reviewHighImpactTokens !== undefined) result.reviewHighImpactTokens = reviewHighImpactTokens as number
|
|
228
212
|
if (advisorMode !== undefined) result.advisorMode = advisorMode as '' | 'host' | 'direct'
|
|
229
213
|
if (advisorTimeoutMs !== undefined) result.advisorTimeoutMs = advisorTimeoutMs as number
|
|
230
214
|
if (advisorRefreshTurns !== undefined) result.advisorRefreshTurns = advisorRefreshTurns as number
|
|
@@ -498,12 +482,6 @@ const PRESET_OPTION_DEFAULTS: PresetOptions = deepFreeze({
|
|
|
498
482
|
prefixStabilizer: true,
|
|
499
483
|
readState: true,
|
|
500
484
|
estimator: { mode: '' },
|
|
501
|
-
// Review pipeline (beta) ships off: pending proposals never block the
|
|
502
|
-
// automatic path until the user opts in.
|
|
503
|
-
reviewMode: false,
|
|
504
|
-
reviewTimeoutTurns: 6,
|
|
505
|
-
cacheHitDiscountAlpha: 0.1,
|
|
506
|
-
reviewHighImpactTokens: 4000,
|
|
507
485
|
// Advisory advisor ships off: statistics and suggestions only, never a gate.
|
|
508
486
|
advisor: {
|
|
509
487
|
mode: '',
|
|
@@ -530,10 +508,6 @@ function mergePresetOptions(overrides: PresetOptionsSettings | undefined): Prese
|
|
|
530
508
|
prefixStabilizer: overrides.prefixStabilizer ?? PRESET_OPTION_DEFAULTS.prefixStabilizer,
|
|
531
509
|
readState: overrides.readState ?? PRESET_OPTION_DEFAULTS.readState,
|
|
532
510
|
estimator: { mode: overrides.estimatorMode ?? PRESET_OPTION_DEFAULTS.estimator.mode },
|
|
533
|
-
reviewMode: overrides.reviewMode ?? PRESET_OPTION_DEFAULTS.reviewMode,
|
|
534
|
-
reviewTimeoutTurns: overrides.reviewTimeoutTurns ?? PRESET_OPTION_DEFAULTS.reviewTimeoutTurns,
|
|
535
|
-
cacheHitDiscountAlpha: overrides.cacheHitDiscountAlpha ?? PRESET_OPTION_DEFAULTS.cacheHitDiscountAlpha,
|
|
536
|
-
reviewHighImpactTokens: overrides.reviewHighImpactTokens ?? PRESET_OPTION_DEFAULTS.reviewHighImpactTokens,
|
|
537
511
|
advisor: {
|
|
538
512
|
mode: overrides.advisorMode ?? PRESET_OPTION_DEFAULTS.advisor.mode,
|
|
539
513
|
timeoutMs: overrides.advisorTimeoutMs ?? PRESET_OPTION_DEFAULTS.advisor.timeoutMs,
|
|
@@ -1,188 +1,188 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prompt construction and answer parsing for the advisory relevance advisor.
|
|
3
|
-
*
|
|
4
|
-
* Same conventions as the estimator prompts: temperature-0 small-model calls,
|
|
5
|
-
* JSON-only output instructions, and fail-open parsers that return
|
|
6
|
-
* `undefined` (never throw) on any malformed answer so the advisor stays
|
|
7
|
-
* observational even against a hostile or broken channel.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/** One summary answer: overall task, active subtasks, prescreen keywords. */
|
|
11
|
-
export interface AdvisorSummaryAnswer {
|
|
12
|
-
readonly overallTask: string
|
|
13
|
-
readonly activeSubtasks: readonly string[]
|
|
14
|
-
readonly keywords: readonly string[]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** One scoring answer row. */
|
|
18
|
-
export interface AdvisorScoreAnswer {
|
|
19
|
-
readonly seq: number
|
|
20
|
-
/** Relevance in [0, 1]; out-of-range rows are dropped. */
|
|
21
|
-
readonly score: number
|
|
22
|
-
/** Short reason; kept out of audits, used only for diagnosis in tests. */
|
|
23
|
-
readonly reason?: string
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function buildAdvisorSummarySystemPrompt(): string {
|
|
27
|
-
return [
|
|
28
|
-
'You summarize what an agent session is working on, for relevance statistics only.',
|
|
29
|
-
'Input: the session todolist snapshot and a recent tail of assistant narration.',
|
|
30
|
-
'Answer with ONLY one JSON object:',
|
|
31
|
-
'{"overallTask":"<one sentence>","activeSubtasks":["<subtask>"],"keywords":["<task keyword>"]}.',
|
|
32
|
-
'keywords must be 3-10 short distinctive words describing the CURRENT task.',
|
|
33
|
-
'Never add commentary; never invent tasks that the input does not support.',
|
|
34
|
-
].join(' ')
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function buildAdvisorSummaryUserPrompt(taskText: string, tailText: string): string {
|
|
38
|
-
const lines = [
|
|
39
|
-
`todolist:\n${taskText}`,
|
|
40
|
-
tailText.trim().length > 0 ? `recent tail:\n${tailText.trim()}` : 'recent tail: (none)',
|
|
41
|
-
]
|
|
42
|
-
return lines.join('\n\n')
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function buildAdvisorScoringSystemPrompt(): string {
|
|
46
|
-
return [
|
|
47
|
-
'You score how relevant each historical session artifact is to the current task,',
|
|
48
|
-
'for statistics only. Relevance covers both the artifact content and its comments',
|
|
49
|
-
'(comment semantics count too). 0 means unrelated, 1 means the live agent will',
|
|
50
|
-
'very likely need this exact content again.',
|
|
51
|
-
'Answer with ONLY one JSON object per input line:',
|
|
52
|
-
'{"seq":<number>,"score":<number between 0 and 1>,"reason":"<short>"}',
|
|
53
|
-
'one per line, same order as the input. Never invent seq values; never add commentary.',
|
|
54
|
-
].join(' ')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function buildAdvisorScoringUserPrompt(
|
|
58
|
-
taskText: string,
|
|
59
|
-
activeSubtasks: readonly string[],
|
|
60
|
-
candidates: readonly { readonly seq: number, readonly preview: string }[],
|
|
61
|
-
): string {
|
|
62
|
-
const lines = [
|
|
63
|
-
`task: ${taskText.replace(/\s+/gu, ' ').slice(0, 600)}`,
|
|
64
|
-
activeSubtasks.length > 0 ? `active subtasks: ${activeSubtasks.join('; ').slice(0, 300)}` : 'active subtasks: (none)',
|
|
65
|
-
'',
|
|
66
|
-
...candidates.map(candidate => `seq=${String(candidate.seq)} | ${candidate.preview.replace(/\s+/gu, ' ')}`),
|
|
67
|
-
]
|
|
68
|
-
return lines.join('\n')
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Pull the first balanced JSON object out of a possibly chatty answer. */
|
|
72
|
-
function firstJsonObject(text: string): Record<string, unknown> | undefined {
|
|
73
|
-
const start = text.indexOf('{')
|
|
74
|
-
if (start < 0) return undefined
|
|
75
|
-
let depth = 0
|
|
76
|
-
let inString = false
|
|
77
|
-
let escaped = false
|
|
78
|
-
for (let index = start; index < text.length; index += 1) {
|
|
79
|
-
const char = text[index]
|
|
80
|
-
if (inString) {
|
|
81
|
-
if (escaped) escaped = false
|
|
82
|
-
else if (char === '\\') escaped = true
|
|
83
|
-
else if (char === '"') inString = false
|
|
84
|
-
continue
|
|
85
|
-
}
|
|
86
|
-
if (char === '"') inString = true
|
|
87
|
-
else if (char === '{') depth += 1
|
|
88
|
-
else if (char === '}') {
|
|
89
|
-
depth -= 1
|
|
90
|
-
if (depth === 0) {
|
|
91
|
-
try {
|
|
92
|
-
const parsed: unknown = JSON.parse(text.slice(start, index + 1))
|
|
93
|
-
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
94
|
-
? parsed as Record<string, unknown>
|
|
95
|
-
: undefined
|
|
96
|
-
} catch {
|
|
97
|
-
return undefined
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return undefined
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function stringList(value: unknown, limit: number): string[] {
|
|
106
|
-
if (!Array.isArray(value)) return []
|
|
107
|
-
return value
|
|
108
|
-
.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
|
|
109
|
-
.slice(0, limit)
|
|
110
|
-
.map(entry => entry.trim())
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Parse one summary answer. Fail-open: `undefined` on any malformed or
|
|
115
|
-
* missing field, so a broken channel can never poison the cached summary.
|
|
116
|
-
*/
|
|
117
|
-
export function parseAdvisorSummary(text: string | undefined): AdvisorSummaryAnswer | undefined {
|
|
118
|
-
if (text === undefined || text.trim().length === 0) return undefined
|
|
119
|
-
const object = firstJsonObject(text)
|
|
120
|
-
if (object === undefined) return undefined
|
|
121
|
-
const overallTask = object.overallTask
|
|
122
|
-
if (typeof overallTask !== 'string' || overallTask.trim().length === 0) return undefined
|
|
123
|
-
const activeSubtasks = stringList(object.activeSubtasks, 12)
|
|
124
|
-
const keywords = stringList(object.keywords, 12)
|
|
125
|
-
if (keywords.length === 0 && activeSubtasks.length === 0) return undefined
|
|
126
|
-
return { overallTask: overallTask.trim(), activeSubtasks, keywords }
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Extract every balanced JSON object from a JSON-lines or chatty answer. */
|
|
130
|
-
function jsonObjects(text: string): Record<string, unknown>[] {
|
|
131
|
-
const objects: Record<string, unknown>[] = []
|
|
132
|
-
let depth = 0
|
|
133
|
-
let start = -1
|
|
134
|
-
let inString = false
|
|
135
|
-
let escaped = false
|
|
136
|
-
for (let index = 0; index < text.length; index += 1) {
|
|
137
|
-
const char = text[index]
|
|
138
|
-
if (inString) {
|
|
139
|
-
if (escaped) escaped = false
|
|
140
|
-
else if (char === '\\') escaped = true
|
|
141
|
-
else if (char === '"') inString = false
|
|
142
|
-
continue
|
|
143
|
-
}
|
|
144
|
-
if (char === '"') inString = true
|
|
145
|
-
else if (char === '{') {
|
|
146
|
-
if (depth === 0) start = index
|
|
147
|
-
depth += 1
|
|
148
|
-
} else if (char === '}') {
|
|
149
|
-
depth -= 1
|
|
150
|
-
if (depth === 0 && start >= 0) {
|
|
151
|
-
try {
|
|
152
|
-
const parsed: unknown = JSON.parse(text.slice(start, index + 1))
|
|
153
|
-
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
154
|
-
objects.push(parsed as Record<string, unknown>)
|
|
155
|
-
}
|
|
156
|
-
} catch {
|
|
157
|
-
// Skip one malformed object; the rest of the answer can still parse.
|
|
158
|
-
}
|
|
159
|
-
start = -1
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return objects
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Parse one scoring answer. Fail-open: returns the valid rows it could read
|
|
168
|
-
* (`undefined` when nothing valid remains) — a partially garbage answer still
|
|
169
|
-
* contributes its good rows, mirroring the estimator's per-item tolerance.
|
|
170
|
-
*/
|
|
171
|
-
export function parseAdvisorScores(
|
|
172
|
-
text: string | undefined,
|
|
173
|
-
validSeqs: ReadonlySet<number>,
|
|
174
|
-
): Map<number, AdvisorScoreAnswer> | undefined {
|
|
175
|
-
if (text === undefined || text.trim().length === 0) return undefined
|
|
176
|
-
const scores = new Map<number, AdvisorScoreAnswer>()
|
|
177
|
-
for (const object of jsonObjects(text)) {
|
|
178
|
-
const seq = object.seq
|
|
179
|
-
const score = object.score
|
|
180
|
-
if (typeof seq !== 'number' || !Number.isSafeInteger(seq) || !validSeqs.has(seq)) continue
|
|
181
|
-
if (typeof score !== 'number' || !Number.isFinite(score) || score < 0 || score > 1) continue
|
|
182
|
-
const reason = typeof object.reason === 'string' && object.reason.trim().length > 0
|
|
183
|
-
? object.reason.trim()
|
|
184
|
-
: undefined
|
|
185
|
-
scores.set(seq, { seq, score, ...(reason !== undefined ? { reason } : {}) })
|
|
186
|
-
}
|
|
187
|
-
return scores.size > 0 ? scores : undefined
|
|
188
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Prompt construction and answer parsing for the advisory relevance advisor.
|
|
3
|
+
*
|
|
4
|
+
* Same conventions as the estimator prompts: temperature-0 small-model calls,
|
|
5
|
+
* JSON-only output instructions, and fail-open parsers that return
|
|
6
|
+
* `undefined` (never throw) on any malformed answer so the advisor stays
|
|
7
|
+
* observational even against a hostile or broken channel.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** One summary answer: overall task, active subtasks, prescreen keywords. */
|
|
11
|
+
export interface AdvisorSummaryAnswer {
|
|
12
|
+
readonly overallTask: string
|
|
13
|
+
readonly activeSubtasks: readonly string[]
|
|
14
|
+
readonly keywords: readonly string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** One scoring answer row. */
|
|
18
|
+
export interface AdvisorScoreAnswer {
|
|
19
|
+
readonly seq: number
|
|
20
|
+
/** Relevance in [0, 1]; out-of-range rows are dropped. */
|
|
21
|
+
readonly score: number
|
|
22
|
+
/** Short reason; kept out of audits, used only for diagnosis in tests. */
|
|
23
|
+
readonly reason?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildAdvisorSummarySystemPrompt(): string {
|
|
27
|
+
return [
|
|
28
|
+
'You summarize what an agent session is working on, for relevance statistics only.',
|
|
29
|
+
'Input: the session todolist snapshot and a recent tail of assistant narration.',
|
|
30
|
+
'Answer with ONLY one JSON object:',
|
|
31
|
+
'{"overallTask":"<one sentence>","activeSubtasks":["<subtask>"],"keywords":["<task keyword>"]}.',
|
|
32
|
+
'keywords must be 3-10 short distinctive words describing the CURRENT task.',
|
|
33
|
+
'Never add commentary; never invent tasks that the input does not support.',
|
|
34
|
+
].join(' ')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildAdvisorSummaryUserPrompt(taskText: string, tailText: string): string {
|
|
38
|
+
const lines = [
|
|
39
|
+
`todolist:\n${taskText}`,
|
|
40
|
+
tailText.trim().length > 0 ? `recent tail:\n${tailText.trim()}` : 'recent tail: (none)',
|
|
41
|
+
]
|
|
42
|
+
return lines.join('\n\n')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildAdvisorScoringSystemPrompt(): string {
|
|
46
|
+
return [
|
|
47
|
+
'You score how relevant each historical session artifact is to the current task,',
|
|
48
|
+
'for statistics only. Relevance covers both the artifact content and its comments',
|
|
49
|
+
'(comment semantics count too). 0 means unrelated, 1 means the live agent will',
|
|
50
|
+
'very likely need this exact content again.',
|
|
51
|
+
'Answer with ONLY one JSON object per input line:',
|
|
52
|
+
'{"seq":<number>,"score":<number between 0 and 1>,"reason":"<short>"}',
|
|
53
|
+
'one per line, same order as the input. Never invent seq values; never add commentary.',
|
|
54
|
+
].join(' ')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildAdvisorScoringUserPrompt(
|
|
58
|
+
taskText: string,
|
|
59
|
+
activeSubtasks: readonly string[],
|
|
60
|
+
candidates: readonly { readonly seq: number, readonly preview: string }[],
|
|
61
|
+
): string {
|
|
62
|
+
const lines = [
|
|
63
|
+
`task: ${taskText.replace(/\s+/gu, ' ').slice(0, 600)}`,
|
|
64
|
+
activeSubtasks.length > 0 ? `active subtasks: ${activeSubtasks.join('; ').slice(0, 300)}` : 'active subtasks: (none)',
|
|
65
|
+
'',
|
|
66
|
+
...candidates.map(candidate => `seq=${String(candidate.seq)} | ${candidate.preview.replace(/\s+/gu, ' ')}`),
|
|
67
|
+
]
|
|
68
|
+
return lines.join('\n')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Pull the first balanced JSON object out of a possibly chatty answer. */
|
|
72
|
+
function firstJsonObject(text: string): Record<string, unknown> | undefined {
|
|
73
|
+
const start = text.indexOf('{')
|
|
74
|
+
if (start < 0) return undefined
|
|
75
|
+
let depth = 0
|
|
76
|
+
let inString = false
|
|
77
|
+
let escaped = false
|
|
78
|
+
for (let index = start; index < text.length; index += 1) {
|
|
79
|
+
const char = text[index]
|
|
80
|
+
if (inString) {
|
|
81
|
+
if (escaped) escaped = false
|
|
82
|
+
else if (char === '\\') escaped = true
|
|
83
|
+
else if (char === '"') inString = false
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
if (char === '"') inString = true
|
|
87
|
+
else if (char === '{') depth += 1
|
|
88
|
+
else if (char === '}') {
|
|
89
|
+
depth -= 1
|
|
90
|
+
if (depth === 0) {
|
|
91
|
+
try {
|
|
92
|
+
const parsed: unknown = JSON.parse(text.slice(start, index + 1))
|
|
93
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
94
|
+
? parsed as Record<string, unknown>
|
|
95
|
+
: undefined
|
|
96
|
+
} catch {
|
|
97
|
+
return undefined
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function stringList(value: unknown, limit: number): string[] {
|
|
106
|
+
if (!Array.isArray(value)) return []
|
|
107
|
+
return value
|
|
108
|
+
.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
|
|
109
|
+
.slice(0, limit)
|
|
110
|
+
.map(entry => entry.trim())
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Parse one summary answer. Fail-open: `undefined` on any malformed or
|
|
115
|
+
* missing field, so a broken channel can never poison the cached summary.
|
|
116
|
+
*/
|
|
117
|
+
export function parseAdvisorSummary(text: string | undefined): AdvisorSummaryAnswer | undefined {
|
|
118
|
+
if (text === undefined || text.trim().length === 0) return undefined
|
|
119
|
+
const object = firstJsonObject(text)
|
|
120
|
+
if (object === undefined) return undefined
|
|
121
|
+
const overallTask = object.overallTask
|
|
122
|
+
if (typeof overallTask !== 'string' || overallTask.trim().length === 0) return undefined
|
|
123
|
+
const activeSubtasks = stringList(object.activeSubtasks, 12)
|
|
124
|
+
const keywords = stringList(object.keywords, 12)
|
|
125
|
+
if (keywords.length === 0 && activeSubtasks.length === 0) return undefined
|
|
126
|
+
return { overallTask: overallTask.trim(), activeSubtasks, keywords }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Extract every balanced JSON object from a JSON-lines or chatty answer. */
|
|
130
|
+
function jsonObjects(text: string): Record<string, unknown>[] {
|
|
131
|
+
const objects: Record<string, unknown>[] = []
|
|
132
|
+
let depth = 0
|
|
133
|
+
let start = -1
|
|
134
|
+
let inString = false
|
|
135
|
+
let escaped = false
|
|
136
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
137
|
+
const char = text[index]
|
|
138
|
+
if (inString) {
|
|
139
|
+
if (escaped) escaped = false
|
|
140
|
+
else if (char === '\\') escaped = true
|
|
141
|
+
else if (char === '"') inString = false
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
if (char === '"') inString = true
|
|
145
|
+
else if (char === '{') {
|
|
146
|
+
if (depth === 0) start = index
|
|
147
|
+
depth += 1
|
|
148
|
+
} else if (char === '}') {
|
|
149
|
+
depth -= 1
|
|
150
|
+
if (depth === 0 && start >= 0) {
|
|
151
|
+
try {
|
|
152
|
+
const parsed: unknown = JSON.parse(text.slice(start, index + 1))
|
|
153
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
154
|
+
objects.push(parsed as Record<string, unknown>)
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// Skip one malformed object; the rest of the answer can still parse.
|
|
158
|
+
}
|
|
159
|
+
start = -1
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return objects
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Parse one scoring answer. Fail-open: returns the valid rows it could read
|
|
168
|
+
* (`undefined` when nothing valid remains) — a partially garbage answer still
|
|
169
|
+
* contributes its good rows, mirroring the estimator's per-item tolerance.
|
|
170
|
+
*/
|
|
171
|
+
export function parseAdvisorScores(
|
|
172
|
+
text: string | undefined,
|
|
173
|
+
validSeqs: ReadonlySet<number>,
|
|
174
|
+
): Map<number, AdvisorScoreAnswer> | undefined {
|
|
175
|
+
if (text === undefined || text.trim().length === 0) return undefined
|
|
176
|
+
const scores = new Map<number, AdvisorScoreAnswer>()
|
|
177
|
+
for (const object of jsonObjects(text)) {
|
|
178
|
+
const seq = object.seq
|
|
179
|
+
const score = object.score
|
|
180
|
+
if (typeof seq !== 'number' || !Number.isSafeInteger(seq) || !validSeqs.has(seq)) continue
|
|
181
|
+
if (typeof score !== 'number' || !Number.isFinite(score) || score < 0 || score > 1) continue
|
|
182
|
+
const reason = typeof object.reason === 'string' && object.reason.trim().length > 0
|
|
183
|
+
? object.reason.trim()
|
|
184
|
+
: undefined
|
|
185
|
+
scores.set(seq, { seq, score, ...(reason !== undefined ? { reason } : {}) })
|
|
186
|
+
}
|
|
187
|
+
return scores.size > 0 ? scores : undefined
|
|
188
|
+
}
|