dsh-context-compression-improved 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.ja.md +34 -0
- package/CHANGELOG.ko.md +34 -0
- package/CHANGELOG.md +38 -0
- package/CHANGELOG.zh.md +30 -0
- package/docs/installation.md +25 -1
- package/docs/installation.zh.md +24 -1
- package/package.json +1 -1
- package/packages/selector/lib/{review-registry.js → advisor-state.js} +105 -4
- package/packages/selector/lib/client.d.ts +7 -0
- package/packages/selector/lib/client.js +32 -2
- package/packages/selector/lib/index.d.ts +7 -0
- package/packages/selector/lib/index.js +150 -3
- package/packages/selector/lib/pruner.d.ts +82 -0
- package/packages/selector/lib/pruner.js +545 -11
- package/packages/selector/src/client/preset-options.ts +2 -0
- package/packages/selector/src/index.ts +108 -0
- package/packages/selector/src/preset-overlay.ts +60 -1
- package/packages/selector/src/profiles.ts +48 -0
- package/packages/selector/src/pruner/state.ts +3 -0
- package/packages/selector/src/pruner.ts +113 -0
- package/packages/selector/src/runtime/audit.ts +22 -0
- package/packages/selector/src/runtime/config.ts +58 -0
- package/packages/selector/src/runtime/tokenpilot/advisor-prompt.ts +188 -0
- package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +133 -0
- package/packages/selector/src/runtime/tokenpilot/advisor.ts +419 -0
- package/packages/selector/src/runtime/tokenpilot/sidechannel.ts +24 -9
- package/packages/selector/src/runtime/types.ts +21 -0
- package/packages/selector/tests/advisor-report.host.spec.ts +223 -0
- package/packages/selector/tests/built/client-artifact.spec.ts +9 -5
- package/packages/selector/tests/runtime/advisor-invariant.spec.ts +272 -0
- package/packages/selector/tests/runtime/advisor.spec.ts +226 -0
- package/packages/selector/tests/runtime/audit.spec.ts +44 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +12 -0
- package/packages/selector/tests/standing-generation.host.spec.ts +54 -5
- package/scripts/packed-components-smoke.mjs +30 -8
- package/scripts/packed-install-e2e.mjs +69 -15
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as COMPRESSION_PROFILES,
|
|
1
|
+
import { C as DEFAULT_CUSTOM_COMPRESSION_POLICY, D as COMPRESSION_PROFILES, E as deepFreeze, S as CustomCompressionPolicySchema, T as assertNever, _ as isCompressionProfile, a as registerReviewPruner, b as resolveConfig, c as ReviewQueue, d as ContextCompressionSettingsSchema, f as DEFAULTS, g as codePointLength, h as charsToTokens, i as recordScore, l as AUTO_COMPACT_THRESHOLD_LIMITS, m as charsForTokens, n as invalidateOnTaskChange, p as PRUNE_MARKER, r as recordRecertified, s as sharedReviewStore, t as getAdvisorState, u as CONTEXT_COMPRESSION_SETTINGS_NAMESPACE, v as isValidAutoCompactThresholdPercent, w as resolveCustomPolicy, x as resolvePolicy, y as parseContextCompressionSettings } from "./advisor-state.js";
|
|
2
2
|
import { a as validatePublishedTailTrim, i as tailTrimStub, n as tailTrimMessage, o as eventBySeq, r as tailTrimRef, s as sessionEvents, t as parseTailTrimRef } from "./tail-trim.js";
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
@@ -2810,20 +2810,31 @@ function documentCensus(text, omittedLines) {
|
|
|
2810
2810
|
var SideChannel = class {
|
|
2811
2811
|
ctx;
|
|
2812
2812
|
options;
|
|
2813
|
-
|
|
2813
|
+
overrides;
|
|
2814
|
+
/**
|
|
2815
|
+
* @param overrides - per-consumer overrides of the estimator-named options.
|
|
2816
|
+
* The estimator itself never passes them (byte-identical behavior); the
|
|
2817
|
+
* advisory advisor passes its own mode/timeout/output budget so both
|
|
2818
|
+
* consumers share one transport without sharing one configuration.
|
|
2819
|
+
*/
|
|
2820
|
+
constructor(ctx, options, overrides) {
|
|
2814
2821
|
this.ctx = ctx;
|
|
2815
2822
|
this.options = options;
|
|
2823
|
+
this.overrides = overrides;
|
|
2824
|
+
}
|
|
2825
|
+
get mode() {
|
|
2826
|
+
return this.overrides?.mode ?? this.options.estimatorMode ?? "";
|
|
2816
2827
|
}
|
|
2817
2828
|
get enabled() {
|
|
2818
|
-
return this.
|
|
2829
|
+
return this.mode === "host" || this.mode === "direct";
|
|
2819
2830
|
}
|
|
2820
2831
|
async ask(request) {
|
|
2821
|
-
const timeoutMs = this.options.estimatorTimeoutMs ?? 3e3;
|
|
2832
|
+
const timeoutMs = this.overrides?.timeoutMs ?? this.options.estimatorTimeoutMs ?? 3e3;
|
|
2822
2833
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
2823
2834
|
const signal = typeof AbortSignal.any === "function" ? AbortSignal.any([request.signal, timeout]) : timeout;
|
|
2824
2835
|
try {
|
|
2825
|
-
if (this.
|
|
2826
|
-
if (this.
|
|
2836
|
+
if (this.mode === "host") return await this.askHost(request.system, request.user, signal);
|
|
2837
|
+
if (this.mode === "direct") return await this.askDirect(request.system, request.user, signal);
|
|
2827
2838
|
return;
|
|
2828
2839
|
} catch {
|
|
2829
2840
|
return;
|
|
@@ -2837,7 +2848,7 @@ var SideChannel = class {
|
|
|
2837
2848
|
ok: text !== void 0,
|
|
2838
2849
|
latencyMs: Date.now() - now,
|
|
2839
2850
|
...this.identity() !== void 0 ? { channel: this.identity() } : {},
|
|
2840
|
-
...text === void 0 ? { reason: "channel returned no content (timeout, non-2xx, parse failure, or reasoning ate the
|
|
2851
|
+
...text === void 0 ? { reason: "channel returned no content (timeout, non-2xx, parse failure, or reasoning ate the output-token budget)" } : {}
|
|
2841
2852
|
};
|
|
2842
2853
|
return {
|
|
2843
2854
|
...text === void 0 ? {} : { text },
|
|
@@ -2845,8 +2856,8 @@ var SideChannel = class {
|
|
|
2845
2856
|
};
|
|
2846
2857
|
}
|
|
2847
2858
|
identity() {
|
|
2848
|
-
if (this.
|
|
2849
|
-
if (this.
|
|
2859
|
+
if (this.mode === "direct") return `direct:${this.options.estimatorModel ?? ""}`;
|
|
2860
|
+
if (this.mode === "host") {
|
|
2850
2861
|
const route = this.resolveHostRoute();
|
|
2851
2862
|
return route === void 0 ? "host" : `host:${route.provider}/${route.model}`;
|
|
2852
2863
|
}
|
|
@@ -2893,7 +2904,7 @@ var SideChannel = class {
|
|
|
2893
2904
|
system,
|
|
2894
2905
|
temperature: 0,
|
|
2895
2906
|
reasoningEffort: "off",
|
|
2896
|
-
maxTokens: 256,
|
|
2907
|
+
maxTokens: this.overrides?.maxTokens ?? 256,
|
|
2897
2908
|
signal
|
|
2898
2909
|
});
|
|
2899
2910
|
for await (const chunk of stream) if (chunk.type === "text-delta" && typeof chunk.text === "string") text += chunk.text;
|
|
@@ -2920,7 +2931,7 @@ var SideChannel = class {
|
|
|
2920
2931
|
content: user
|
|
2921
2932
|
}],
|
|
2922
2933
|
temperature: 0,
|
|
2923
|
-
max_tokens: 256
|
|
2934
|
+
max_tokens: this.overrides?.maxTokens ?? 256
|
|
2924
2935
|
}),
|
|
2925
2936
|
signal
|
|
2926
2937
|
});
|
|
@@ -3332,6 +3343,445 @@ async function openReviewStorage(getService) {
|
|
|
3332
3343
|
return new StorageDomainReviewStore((await service.open(reviewStorageSpec())).table(REVIEW_STORAGE_TABLE));
|
|
3333
3344
|
}
|
|
3334
3345
|
//#endregion
|
|
3346
|
+
//#region src/runtime/tokenpilot/advisor-prompt.ts
|
|
3347
|
+
function buildAdvisorSummarySystemPrompt() {
|
|
3348
|
+
return [
|
|
3349
|
+
"You summarize what an agent session is working on, for relevance statistics only.",
|
|
3350
|
+
"Input: the session todolist snapshot and a recent tail of assistant narration.",
|
|
3351
|
+
"Answer with ONLY one JSON object:",
|
|
3352
|
+
"{\"overallTask\":\"<one sentence>\",\"activeSubtasks\":[\"<subtask>\"],\"keywords\":[\"<task keyword>\"]}.",
|
|
3353
|
+
"keywords must be 3-10 short distinctive words describing the CURRENT task.",
|
|
3354
|
+
"Never add commentary; never invent tasks that the input does not support."
|
|
3355
|
+
].join(" ");
|
|
3356
|
+
}
|
|
3357
|
+
function buildAdvisorSummaryUserPrompt(taskText, tailText) {
|
|
3358
|
+
return [`todolist:\n${taskText}`, tailText.trim().length > 0 ? `recent tail:\n${tailText.trim()}` : "recent tail: (none)"].join("\n\n");
|
|
3359
|
+
}
|
|
3360
|
+
function buildAdvisorScoringSystemPrompt() {
|
|
3361
|
+
return [
|
|
3362
|
+
"You score how relevant each historical session artifact is to the current task,",
|
|
3363
|
+
"for statistics only. Relevance covers both the artifact content and its comments",
|
|
3364
|
+
"(comment semantics count too). 0 means unrelated, 1 means the live agent will",
|
|
3365
|
+
"very likely need this exact content again.",
|
|
3366
|
+
"Answer with ONLY one JSON object per input line:",
|
|
3367
|
+
"{\"seq\":<number>,\"score\":<number between 0 and 1>,\"reason\":\"<short>\"}",
|
|
3368
|
+
"one per line, same order as the input. Never invent seq values; never add commentary."
|
|
3369
|
+
].join(" ");
|
|
3370
|
+
}
|
|
3371
|
+
function buildAdvisorScoringUserPrompt(taskText, activeSubtasks, candidates) {
|
|
3372
|
+
return [
|
|
3373
|
+
`task: ${taskText.replace(/\s+/gu, " ").slice(0, 600)}`,
|
|
3374
|
+
activeSubtasks.length > 0 ? `active subtasks: ${activeSubtasks.join("; ").slice(0, 300)}` : "active subtasks: (none)",
|
|
3375
|
+
"",
|
|
3376
|
+
...candidates.map((candidate) => `seq=${String(candidate.seq)} | ${candidate.preview.replace(/\s+/gu, " ")}`)
|
|
3377
|
+
].join("\n");
|
|
3378
|
+
}
|
|
3379
|
+
/** Pull the first balanced JSON object out of a possibly chatty answer. */
|
|
3380
|
+
function firstJsonObject(text) {
|
|
3381
|
+
const start = text.indexOf("{");
|
|
3382
|
+
if (start < 0) return void 0;
|
|
3383
|
+
let depth = 0;
|
|
3384
|
+
let inString = false;
|
|
3385
|
+
let escaped = false;
|
|
3386
|
+
for (let index = start; index < text.length; index += 1) {
|
|
3387
|
+
const char = text[index];
|
|
3388
|
+
if (inString) {
|
|
3389
|
+
if (escaped) escaped = false;
|
|
3390
|
+
else if (char === "\\") escaped = true;
|
|
3391
|
+
else if (char === "\"") inString = false;
|
|
3392
|
+
continue;
|
|
3393
|
+
}
|
|
3394
|
+
if (char === "\"") inString = true;
|
|
3395
|
+
else if (char === "{") depth += 1;
|
|
3396
|
+
else if (char === "}") {
|
|
3397
|
+
depth -= 1;
|
|
3398
|
+
if (depth === 0) try {
|
|
3399
|
+
const parsed = JSON.parse(text.slice(start, index + 1));
|
|
3400
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
3401
|
+
} catch {
|
|
3402
|
+
return;
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
function stringList(value, limit) {
|
|
3408
|
+
if (!Array.isArray(value)) return [];
|
|
3409
|
+
return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).slice(0, limit).map((entry) => entry.trim());
|
|
3410
|
+
}
|
|
3411
|
+
/**
|
|
3412
|
+
* Parse one summary answer. Fail-open: `undefined` on any malformed or
|
|
3413
|
+
* missing field, so a broken channel can never poison the cached summary.
|
|
3414
|
+
*/
|
|
3415
|
+
function parseAdvisorSummary(text) {
|
|
3416
|
+
if (text === void 0 || text.trim().length === 0) return void 0;
|
|
3417
|
+
const object = firstJsonObject(text);
|
|
3418
|
+
if (object === void 0) return void 0;
|
|
3419
|
+
const overallTask = object.overallTask;
|
|
3420
|
+
if (typeof overallTask !== "string" || overallTask.trim().length === 0) return void 0;
|
|
3421
|
+
const activeSubtasks = stringList(object.activeSubtasks, 12);
|
|
3422
|
+
const keywords = stringList(object.keywords, 12);
|
|
3423
|
+
if (keywords.length === 0 && activeSubtasks.length === 0) return void 0;
|
|
3424
|
+
return {
|
|
3425
|
+
overallTask: overallTask.trim(),
|
|
3426
|
+
activeSubtasks,
|
|
3427
|
+
keywords
|
|
3428
|
+
};
|
|
3429
|
+
}
|
|
3430
|
+
/** Extract every balanced JSON object from a JSON-lines or chatty answer. */
|
|
3431
|
+
function jsonObjects(text) {
|
|
3432
|
+
const objects = [];
|
|
3433
|
+
let depth = 0;
|
|
3434
|
+
let start = -1;
|
|
3435
|
+
let inString = false;
|
|
3436
|
+
let escaped = false;
|
|
3437
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
3438
|
+
const char = text[index];
|
|
3439
|
+
if (inString) {
|
|
3440
|
+
if (escaped) escaped = false;
|
|
3441
|
+
else if (char === "\\") escaped = true;
|
|
3442
|
+
else if (char === "\"") inString = false;
|
|
3443
|
+
continue;
|
|
3444
|
+
}
|
|
3445
|
+
if (char === "\"") inString = true;
|
|
3446
|
+
else if (char === "{") {
|
|
3447
|
+
if (depth === 0) start = index;
|
|
3448
|
+
depth += 1;
|
|
3449
|
+
} else if (char === "}") {
|
|
3450
|
+
depth -= 1;
|
|
3451
|
+
if (depth === 0 && start >= 0) {
|
|
3452
|
+
try {
|
|
3453
|
+
const parsed = JSON.parse(text.slice(start, index + 1));
|
|
3454
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) objects.push(parsed);
|
|
3455
|
+
} catch {}
|
|
3456
|
+
start = -1;
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
return objects;
|
|
3461
|
+
}
|
|
3462
|
+
/**
|
|
3463
|
+
* Parse one scoring answer. Fail-open: returns the valid rows it could read
|
|
3464
|
+
* (`undefined` when nothing valid remains) — a partially garbage answer still
|
|
3465
|
+
* contributes its good rows, mirroring the estimator's per-item tolerance.
|
|
3466
|
+
*/
|
|
3467
|
+
function parseAdvisorScores(text, validSeqs) {
|
|
3468
|
+
if (text === void 0 || text.trim().length === 0) return void 0;
|
|
3469
|
+
const scores = /* @__PURE__ */ new Map();
|
|
3470
|
+
for (const object of jsonObjects(text)) {
|
|
3471
|
+
const seq = object.seq;
|
|
3472
|
+
const score = object.score;
|
|
3473
|
+
if (typeof seq !== "number" || !Number.isSafeInteger(seq) || !validSeqs.has(seq)) continue;
|
|
3474
|
+
if (typeof score !== "number" || !Number.isFinite(score) || score < 0 || score > 1) continue;
|
|
3475
|
+
const reason = typeof object.reason === "string" && object.reason.trim().length > 0 ? object.reason.trim() : void 0;
|
|
3476
|
+
scores.set(seq, {
|
|
3477
|
+
seq,
|
|
3478
|
+
score,
|
|
3479
|
+
...reason !== void 0 ? { reason } : {}
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
return scores.size > 0 ? scores : void 0;
|
|
3483
|
+
}
|
|
3484
|
+
//#endregion
|
|
3485
|
+
//#region src/runtime/tokenpilot/advisor.ts
|
|
3486
|
+
/** Character cap for the recent-text fallback and the tail-text summary input. */
|
|
3487
|
+
const TAIL_TEXT_CHAR_BUDGET = 4e3;
|
|
3488
|
+
/** Score the advisor assigns to candidates it has no answer for. */
|
|
3489
|
+
const NEUTRAL_RELEVANCE = .5;
|
|
3490
|
+
/** Deterministic djb2-derived hex digest for task-semantics versioning. */
|
|
3491
|
+
function versionDigest(text) {
|
|
3492
|
+
let hash = 5381;
|
|
3493
|
+
for (let index = 0; index < text.length; index += 1) hash = (hash * 33 ^ text.charCodeAt(index)) >>> 0;
|
|
3494
|
+
return hash.toString(16).padStart(8, "0");
|
|
3495
|
+
}
|
|
3496
|
+
/** Truncate on the character basis (Unicode code points), never UTF-16 units. */
|
|
3497
|
+
function truncateChars(text, budget) {
|
|
3498
|
+
if (codePointLength(text) <= budget) return text;
|
|
3499
|
+
return Array.from(text).slice(0, budget).join("");
|
|
3500
|
+
}
|
|
3501
|
+
/** Character cap of one candidate preview line offered to the scoring prompt. */
|
|
3502
|
+
const PREVIEW_CHAR_BUDGET = 200;
|
|
3503
|
+
/**
|
|
3504
|
+
* One candidate face for the scoring prompt: tool-call name plus the head of
|
|
3505
|
+
* the result text. Pure and shape-defensive.
|
|
3506
|
+
*/
|
|
3507
|
+
function advisorCandidatePreview(callName, blocks) {
|
|
3508
|
+
return truncateChars(`${callName} ${textBlocks(blocks)}`.trim(), PREVIEW_CHAR_BUDGET);
|
|
3509
|
+
}
|
|
3510
|
+
/**
|
|
3511
|
+
* Collect the recent assistant narration tail (bounded, oldest-first join) as
|
|
3512
|
+
* summary-prompt context. Pure.
|
|
3513
|
+
*/
|
|
3514
|
+
function collectTailText(events, budget = TAIL_TEXT_CHAR_BUDGET) {
|
|
3515
|
+
const parts = [];
|
|
3516
|
+
let size = 0;
|
|
3517
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
3518
|
+
const event = events[index];
|
|
3519
|
+
if (event?.type !== "assistant/message") continue;
|
|
3520
|
+
const text = textBlocks(event.data).trim();
|
|
3521
|
+
if (text.length === 0) continue;
|
|
3522
|
+
parts.unshift(text);
|
|
3523
|
+
size += codePointLength(text);
|
|
3524
|
+
if (size >= budget) break;
|
|
3525
|
+
}
|
|
3526
|
+
return truncateChars(parts.join("\n"), budget);
|
|
3527
|
+
}
|
|
3528
|
+
function textBlocks(data) {
|
|
3529
|
+
const content = data?.content;
|
|
3530
|
+
if (!Array.isArray(content)) return "";
|
|
3531
|
+
const parts = [];
|
|
3532
|
+
for (const block of content) if (block?.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
3533
|
+
return parts.join("\n");
|
|
3534
|
+
}
|
|
3535
|
+
/** Structured probe of one `todo/write` payload: the list of task strings, or undefined. */
|
|
3536
|
+
function extractTodoItems(data) {
|
|
3537
|
+
const todos = data?.todos;
|
|
3538
|
+
const list = Array.isArray(todos) ? todos : Array.isArray(data) ? data : void 0;
|
|
3539
|
+
if (list === void 0 || list.length === 0) return void 0;
|
|
3540
|
+
const items = [];
|
|
3541
|
+
for (const entry of list) {
|
|
3542
|
+
if (typeof entry === "string") {
|
|
3543
|
+
if (entry.trim().length > 0) items.push(entry.trim());
|
|
3544
|
+
continue;
|
|
3545
|
+
}
|
|
3546
|
+
if (entry !== null && typeof entry === "object") {
|
|
3547
|
+
const record = entry;
|
|
3548
|
+
const text = [
|
|
3549
|
+
record.content,
|
|
3550
|
+
record.text,
|
|
3551
|
+
record.title,
|
|
3552
|
+
record.name
|
|
3553
|
+
].find((candidate) => typeof candidate === "string" && candidate.trim().length > 0);
|
|
3554
|
+
if (typeof text === "string") {
|
|
3555
|
+
items.push(text.trim());
|
|
3556
|
+
continue;
|
|
3557
|
+
}
|
|
3558
|
+
items.push(JSON.stringify(record));
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
return items.length > 0 ? items : void 0;
|
|
3562
|
+
}
|
|
3563
|
+
/**
|
|
3564
|
+
* Harvest task semantics for the summary/scoring prompts: the most recent
|
|
3565
|
+
* `todo/write` event (structured probe first, then the raw JSON string),
|
|
3566
|
+
* falling back to recent user/message text. Pure — log in, semantics out.
|
|
3567
|
+
*/
|
|
3568
|
+
function collectTaskSemantics(events) {
|
|
3569
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
3570
|
+
const event = events[index];
|
|
3571
|
+
if (event === void 0 || event.type !== "todo/write") continue;
|
|
3572
|
+
const data = event.data;
|
|
3573
|
+
const items = extractTodoItems(data);
|
|
3574
|
+
if (items !== void 0) {
|
|
3575
|
+
const taskText = truncateChars(items.join("\n"), TAIL_TEXT_CHAR_BUDGET);
|
|
3576
|
+
return {
|
|
3577
|
+
source: "todos",
|
|
3578
|
+
todoVersion: versionDigest(taskText),
|
|
3579
|
+
taskText
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
const raw = truncateChars(JSON.stringify(event.data) ?? "", TAIL_TEXT_CHAR_BUDGET);
|
|
3583
|
+
if (raw.length > 2) return {
|
|
3584
|
+
source: "raw-todo",
|
|
3585
|
+
todoVersion: versionDigest(raw),
|
|
3586
|
+
taskText: raw
|
|
3587
|
+
};
|
|
3588
|
+
}
|
|
3589
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
3590
|
+
const event = events[index];
|
|
3591
|
+
if (event?.type !== "user/message") continue;
|
|
3592
|
+
const text = truncateChars(textBlocks(event.data).trim(), TAIL_TEXT_CHAR_BUDGET);
|
|
3593
|
+
if (text.length === 0) continue;
|
|
3594
|
+
return {
|
|
3595
|
+
source: "messages",
|
|
3596
|
+
todoVersion: versionDigest(text),
|
|
3597
|
+
taskText: text
|
|
3598
|
+
};
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
/**
|
|
3602
|
+
* Prefix-decay figure: 1 minus the character-pressure-weighted mean relevance
|
|
3603
|
+
* of the prefix candidates. Unscored candidates count as neutral 0.5. Pure,
|
|
3604
|
+
* deterministic, no LLM and no I/O.
|
|
3605
|
+
*/
|
|
3606
|
+
function prefixDecay(candidates, scores) {
|
|
3607
|
+
let totalWeight = 0;
|
|
3608
|
+
let weightedRelevance = 0;
|
|
3609
|
+
for (const candidate of candidates) {
|
|
3610
|
+
const weight = candidate.characterPressure > 0 ? candidate.characterPressure : 0;
|
|
3611
|
+
if (weight === 0) continue;
|
|
3612
|
+
totalWeight += weight;
|
|
3613
|
+
weightedRelevance += weight * (scores.get(candidate.seq)?.score ?? NEUTRAL_RELEVANCE);
|
|
3614
|
+
}
|
|
3615
|
+
if (totalWeight === 0) return {
|
|
3616
|
+
decay: 0,
|
|
3617
|
+
weightedChars: 0
|
|
3618
|
+
};
|
|
3619
|
+
return {
|
|
3620
|
+
decay: 1 - weightedRelevance / totalWeight,
|
|
3621
|
+
weightedChars: totalWeight
|
|
3622
|
+
};
|
|
3623
|
+
}
|
|
3624
|
+
/** Lowercase word tokens used by the local keyword-overlap prescreen. */
|
|
3625
|
+
function keywordsOf(text) {
|
|
3626
|
+
const matches = text.toLowerCase().match(/[\p{L}\p{N}_-]{3,}/gu) ?? [];
|
|
3627
|
+
return new Set(matches);
|
|
3628
|
+
}
|
|
3629
|
+
function overlapCount(left, right) {
|
|
3630
|
+
let count = 0;
|
|
3631
|
+
for (const token of right) if (left.has(token)) count += 1;
|
|
3632
|
+
return count;
|
|
3633
|
+
}
|
|
3634
|
+
/**
|
|
3635
|
+
* Incremental scoring selection: candidates newer than the watermark whose
|
|
3636
|
+
* character pressure reaches the token-named floor, ranked by local keyword
|
|
3637
|
+
* overlap with the task semantics and cut at the sample limit. When the task
|
|
3638
|
+
* semantics changed, the watermark is ignored so every eligible candidate can
|
|
3639
|
+
* rescore. Pure.
|
|
3640
|
+
*/
|
|
3641
|
+
function selectScoringCandidates(candidates, state, input) {
|
|
3642
|
+
const eligible = [];
|
|
3643
|
+
for (const candidate of candidates) {
|
|
3644
|
+
if (!input.taskChanged && candidate.seq <= state.watermarkSeq) continue;
|
|
3645
|
+
if (candidate.characterPressure < input.minChars) continue;
|
|
3646
|
+
eligible.push({
|
|
3647
|
+
...candidate,
|
|
3648
|
+
overlap: overlapCount(input.taskKeywords, keywordsOf(candidate.preview))
|
|
3649
|
+
});
|
|
3650
|
+
}
|
|
3651
|
+
eligible.sort((left, right) => right.overlap - left.overlap || right.characterPressure - left.characterPressure);
|
|
3652
|
+
return eligible.slice(0, input.sampleLimit).map(({ overlap: _overlap, ...candidate }) => candidate);
|
|
3653
|
+
}
|
|
3654
|
+
function advisorAudit(input, phase, fields) {
|
|
3655
|
+
return {
|
|
3656
|
+
schemaVersion: 1,
|
|
3657
|
+
kind: "advisor-outcome",
|
|
3658
|
+
sessionId: input.sessionId,
|
|
3659
|
+
phase,
|
|
3660
|
+
turnIndex: input.turn,
|
|
3661
|
+
...fields
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
/**
|
|
3665
|
+
* One full advisor pass: summary refresh (todo change or every refreshTurns),
|
|
3666
|
+
* incremental batch scoring with recertification marks, then the decay
|
|
3667
|
+
* figure. State is written only on success; any failure leaves state
|
|
3668
|
+
* untouched, emits ok:false audits with reason codes, and never throws.
|
|
3669
|
+
*/
|
|
3670
|
+
async function runAdvisorPass(state, channel, emit, input) {
|
|
3671
|
+
if (input.task === void 0) return void 0;
|
|
3672
|
+
const taskChanged = invalidateOnTaskChange(state, input.task.todoVersion);
|
|
3673
|
+
const turn = input.turn;
|
|
3674
|
+
if (state.summary === void 0 || turn - state.lastSummaryTurn >= input.advisor.refreshTurns) {
|
|
3675
|
+
const summaryStarted = Date.now();
|
|
3676
|
+
const summaryText = await channel.ask({
|
|
3677
|
+
system: buildAdvisorSummarySystemPrompt(),
|
|
3678
|
+
user: buildAdvisorSummaryUserPrompt(input.task.taskText, input.tailText),
|
|
3679
|
+
signal: input.signal
|
|
3680
|
+
});
|
|
3681
|
+
const summaryLatencyMs = Date.now() - summaryStarted;
|
|
3682
|
+
const summary = input.signal.aborted ? void 0 : parseAdvisorSummary(summaryText);
|
|
3683
|
+
if (summary === void 0) {
|
|
3684
|
+
emit(advisorAudit(input, "summary", {
|
|
3685
|
+
ok: false,
|
|
3686
|
+
latencyMs: summaryLatencyMs,
|
|
3687
|
+
...input.signal.aborted ? { reason: "aborted" } : summaryText === void 0 ? { reason: "channel-empty" } : { reason: "parse-failed" }
|
|
3688
|
+
}));
|
|
3689
|
+
return;
|
|
3690
|
+
}
|
|
3691
|
+
state.summary = {
|
|
3692
|
+
...summary,
|
|
3693
|
+
todoVersion: input.task.todoVersion,
|
|
3694
|
+
turn
|
|
3695
|
+
};
|
|
3696
|
+
state.lastSummaryTurn = turn;
|
|
3697
|
+
emit(advisorAudit(input, "summary", {
|
|
3698
|
+
ok: true,
|
|
3699
|
+
latencyMs: summaryLatencyMs
|
|
3700
|
+
}));
|
|
3701
|
+
}
|
|
3702
|
+
const summary = state.summary;
|
|
3703
|
+
if (summary === void 0) return void 0;
|
|
3704
|
+
const sampled = selectScoringCandidates(input.candidates, state, {
|
|
3705
|
+
taskKeywords: keywordsOf(`${input.task.taskText}\n${summary.keywords.join(" ")}`),
|
|
3706
|
+
minChars: charsForTokens(input.advisor.minTokens),
|
|
3707
|
+
sampleLimit: input.advisor.sampleLimit,
|
|
3708
|
+
taskChanged
|
|
3709
|
+
});
|
|
3710
|
+
let scored = 0;
|
|
3711
|
+
let highestScored = 0;
|
|
3712
|
+
if (sampled.length > 0) {
|
|
3713
|
+
const scoringStarted = Date.now();
|
|
3714
|
+
const scoresText = await channel.ask({
|
|
3715
|
+
system: buildAdvisorScoringSystemPrompt(),
|
|
3716
|
+
user: buildAdvisorScoringUserPrompt(input.task.taskText, summary.activeSubtasks, sampled),
|
|
3717
|
+
signal: input.signal
|
|
3718
|
+
});
|
|
3719
|
+
const scoringLatencyMs = Date.now() - scoringStarted;
|
|
3720
|
+
const scores = input.signal.aborted ? void 0 : parseAdvisorScores(scoresText, new Set(sampled.map((item) => item.seq)));
|
|
3721
|
+
if (scores === void 0 || scores.size === 0) {
|
|
3722
|
+
emit(advisorAudit(input, "scoring", {
|
|
3723
|
+
ok: false,
|
|
3724
|
+
sampledCount: sampled.length,
|
|
3725
|
+
latencyMs: scoringLatencyMs,
|
|
3726
|
+
...input.signal.aborted ? { reason: "aborted" } : scoresText === void 0 ? { reason: "channel-empty" } : { reason: "parse-failed" }
|
|
3727
|
+
}));
|
|
3728
|
+
return;
|
|
3729
|
+
}
|
|
3730
|
+
for (const candidate of sampled) {
|
|
3731
|
+
const answer = scores.get(candidate.seq);
|
|
3732
|
+
if (answer === void 0) continue;
|
|
3733
|
+
recordScore(state, candidate.seq, {
|
|
3734
|
+
score: answer.score,
|
|
3735
|
+
turn
|
|
3736
|
+
});
|
|
3737
|
+
scored += 1;
|
|
3738
|
+
if (candidate.seq > highestScored) highestScored = candidate.seq;
|
|
3739
|
+
if (answer.score < input.advisor.scoreThreshold) recordRecertified(state, candidate.seq, turn);
|
|
3740
|
+
}
|
|
3741
|
+
emit(advisorAudit(input, "scoring", {
|
|
3742
|
+
ok: true,
|
|
3743
|
+
sampledCount: sampled.length,
|
|
3744
|
+
latencyMs: scoringLatencyMs
|
|
3745
|
+
}));
|
|
3746
|
+
if (highestScored > state.watermarkSeq) state.watermarkSeq = highestScored;
|
|
3747
|
+
}
|
|
3748
|
+
const decay = prefixDecay(input.candidates, state.scores);
|
|
3749
|
+
state.lastDecay = {
|
|
3750
|
+
decay: decay.decay,
|
|
3751
|
+
weightedChars: decay.weightedChars,
|
|
3752
|
+
turn
|
|
3753
|
+
};
|
|
3754
|
+
emit(advisorAudit(input, "decay", {
|
|
3755
|
+
ok: true,
|
|
3756
|
+
sampledCount: scored,
|
|
3757
|
+
decay: decay.decay,
|
|
3758
|
+
weightedChars: decay.weightedChars,
|
|
3759
|
+
latencyMs: 0
|
|
3760
|
+
}));
|
|
3761
|
+
return {
|
|
3762
|
+
decay: decay.decay,
|
|
3763
|
+
weightedChars: decay.weightedChars,
|
|
3764
|
+
sampled: sampled.length
|
|
3765
|
+
};
|
|
3766
|
+
}
|
|
3767
|
+
/**
|
|
3768
|
+
* Convenience entry used by the pruner: fetch-or-create the session state and
|
|
3769
|
+
* run one pass against it.
|
|
3770
|
+
*/
|
|
3771
|
+
async function runSessionAdvisorPass(session, channel, emit, input) {
|
|
3772
|
+
const state = getAdvisorState(session);
|
|
3773
|
+
if (state.inFlight) return void 0;
|
|
3774
|
+
state.inFlight = true;
|
|
3775
|
+
try {
|
|
3776
|
+
return await runAdvisorPass(state, channel, emit, {
|
|
3777
|
+
...input,
|
|
3778
|
+
sessionId: input.sessionId ?? String(session.id)
|
|
3779
|
+
});
|
|
3780
|
+
} finally {
|
|
3781
|
+
state.inFlight = false;
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
//#endregion
|
|
3335
3785
|
//#region src/runtime/deepseek-official-pricing.ts
|
|
3336
3786
|
/** Checked-in DeepSeek official prices and fixed-point provider-usage accounting. */
|
|
3337
3787
|
const DEEPSEEK_OFFICIAL_PRICE_CATALOG_VERSION = "deepseek-official-2026-08-25";
|
|
@@ -3766,6 +4216,7 @@ var ToolResultPruner = class extends Service {
|
|
|
3766
4216
|
reviewQueues: /* @__PURE__ */ new WeakMap(),
|
|
3767
4217
|
reviewClocks: /* @__PURE__ */ new WeakMap(),
|
|
3768
4218
|
estimatorRemainingTurns: /* @__PURE__ */ new WeakMap(),
|
|
4219
|
+
advisorChannels: /* @__PURE__ */ new WeakMap(),
|
|
3769
4220
|
reviewSummaries: /* @__PURE__ */ new WeakMap()
|
|
3770
4221
|
};
|
|
3771
4222
|
ctx.effect(() => registerReviewPruner(this), "contextCompressionSelector.reviewRegistry()");
|
|
@@ -3821,6 +4272,7 @@ var ToolResultPruner = class extends Service {
|
|
|
3821
4272
|
ctx.logger.warn("context-compression review turn-boundary pass failed open: %o", error);
|
|
3822
4273
|
}
|
|
3823
4274
|
this.postflightEstimatorPass(agent.session, signal).catch(() => void 0);
|
|
4275
|
+
this.postflightAdvisorPass(agent.session, turn, signal).catch(() => void 0);
|
|
3824
4276
|
});
|
|
3825
4277
|
}
|
|
3826
4278
|
/**
|
|
@@ -4068,6 +4520,88 @@ var ToolResultPruner = class extends Service {
|
|
|
4068
4520
|
});
|
|
4069
4521
|
}
|
|
4070
4522
|
/**
|
|
4523
|
+
* Advisory advisor pass at the turn boundary, strictly fire-and-forget.
|
|
4524
|
+
* Produces todolist-bound tail-task summaries, incremental relevance
|
|
4525
|
+
* scores, and a prefix-decay figure — all observational. Every short
|
|
4526
|
+
* circuit below (mode off, re-entry, cooldown, no task semantics, no
|
|
4527
|
+
* direct endpoint) returns without touching any state the pruning chain
|
|
4528
|
+
* reads, so the default configuration adds exactly zero behavior.
|
|
4529
|
+
*/
|
|
4530
|
+
async postflightAdvisorPass(session, turn, signal) {
|
|
4531
|
+
const policy = this.activePolicy(session);
|
|
4532
|
+
const presetOptions = policy?.presetOptions;
|
|
4533
|
+
const advisor = presetOptions?.advisor;
|
|
4534
|
+
if (policy === void 0 || presetOptions === void 0 || advisor === void 0 || advisor.mode === "") return;
|
|
4535
|
+
const advisorState = getAdvisorState(session);
|
|
4536
|
+
if (advisorState.inFlight) return;
|
|
4537
|
+
if (isCoolingDown(advisorState.failures, Date.now())) return;
|
|
4538
|
+
const events = sessionEvents(session);
|
|
4539
|
+
const task = collectTaskSemantics(events);
|
|
4540
|
+
if (task === void 0) return;
|
|
4541
|
+
const settings = this.activeSettings(session).presetOptions ?? {};
|
|
4542
|
+
if (advisor.mode === "direct" && (settings.estimatorBaseUrl === void 0 || settings.estimatorBaseUrl.length === 0 || settings.estimatorModel === void 0 || settings.estimatorModel.length === 0)) {
|
|
4543
|
+
emitCompressionAudit(this.ctx.logger, {
|
|
4544
|
+
schemaVersion: 1,
|
|
4545
|
+
kind: "advisor-outcome",
|
|
4546
|
+
sessionId: String(session.id),
|
|
4547
|
+
phase: "summary",
|
|
4548
|
+
channel: "direct",
|
|
4549
|
+
ok: false,
|
|
4550
|
+
turnIndex: turn,
|
|
4551
|
+
reason: "no-direct-endpoint",
|
|
4552
|
+
latencyMs: 0
|
|
4553
|
+
});
|
|
4554
|
+
advisorState.failures = {
|
|
4555
|
+
failures: (advisorState.failures?.failures ?? 0) + 1,
|
|
4556
|
+
cooldownUntil: Date.now() + backoffCooldownMs((advisorState.failures?.failures ?? 0) + 1)
|
|
4557
|
+
};
|
|
4558
|
+
return;
|
|
4559
|
+
}
|
|
4560
|
+
let channel = this.state.advisorChannels.get(session);
|
|
4561
|
+
if (channel === void 0) {
|
|
4562
|
+
channel = new SideChannel(this.ctx, settings, {
|
|
4563
|
+
mode: advisor.mode,
|
|
4564
|
+
timeoutMs: advisor.timeoutMs,
|
|
4565
|
+
maxTokens: 512
|
|
4566
|
+
});
|
|
4567
|
+
this.state.advisorChannels.set(session, channel);
|
|
4568
|
+
}
|
|
4569
|
+
const view = measureForCompaction(this.ctx, session);
|
|
4570
|
+
const candidates = this.snapshot(session, view).filter((candidate) => !this.isRecoveryExempt(session, candidate)).map((candidate) => ({
|
|
4571
|
+
seq: candidate.seq,
|
|
4572
|
+
characterPressure: candidate.characterPressure,
|
|
4573
|
+
preview: advisorCandidatePreview(candidate.call.name, candidate.event.data.message.content)
|
|
4574
|
+
}));
|
|
4575
|
+
let sawFailure = false;
|
|
4576
|
+
const outcome = await runSessionAdvisorPass(session, channel, (record) => {
|
|
4577
|
+
if (record.ok === false) sawFailure = true;
|
|
4578
|
+
emitCompressionAudit(this.ctx.logger, record);
|
|
4579
|
+
}, {
|
|
4580
|
+
profile: policy.profile,
|
|
4581
|
+
sessionId: String(session.id),
|
|
4582
|
+
turn,
|
|
4583
|
+
candidates,
|
|
4584
|
+
task: {
|
|
4585
|
+
source: task.source,
|
|
4586
|
+
todoVersion: task.todoVersion,
|
|
4587
|
+
taskText: task.taskText
|
|
4588
|
+
},
|
|
4589
|
+
advisor: {
|
|
4590
|
+
refreshTurns: advisor.refreshTurns,
|
|
4591
|
+
scoreThreshold: advisor.scoreThreshold,
|
|
4592
|
+
sampleLimit: advisor.sampleLimit,
|
|
4593
|
+
minTokens: advisor.minTokens
|
|
4594
|
+
},
|
|
4595
|
+
tailText: collectTailText(events),
|
|
4596
|
+
signal
|
|
4597
|
+
});
|
|
4598
|
+
if (outcome === void 0 && sawFailure && signal.aborted === false) advisorState.failures = {
|
|
4599
|
+
failures: (advisorState.failures?.failures ?? 0) + 1,
|
|
4600
|
+
cooldownUntil: Date.now() + backoffCooldownMs((advisorState.failures?.failures ?? 0) + 1)
|
|
4601
|
+
};
|
|
4602
|
+
else if (outcome !== void 0) advisorState.failures = void 0;
|
|
4603
|
+
}
|
|
4604
|
+
/**
|
|
4071
4605
|
* The per-session review queue, or `undefined` while review mode is off
|
|
4072
4606
|
* (every review path must then behave exactly like before).
|
|
4073
4607
|
*/
|
|
@@ -25,6 +25,8 @@ const PRESET_OPTION_KEYS = [
|
|
|
25
25
|
'dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'estimatorMode',
|
|
26
26
|
'estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey', 'estimatorTimeoutMs',
|
|
27
27
|
'reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens',
|
|
28
|
+
'advisorMode', 'advisorTimeoutMs', 'advisorRefreshTurns', 'advisorScoreThreshold', 'advisorSampleLimit',
|
|
29
|
+
'advisorMinTokens',
|
|
28
30
|
] as const
|
|
29
31
|
|
|
30
32
|
/** One partial edit of `presetOptions`; `undefined` clears the named field. */
|