dsh-switchman 1.10.2 → 1.11.0
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/README.de.md +5 -4
- package/README.es.md +5 -4
- package/README.fr.md +5 -4
- package/README.it.md +5 -4
- package/README.ja.md +5 -4
- package/README.ko.md +5 -4
- package/README.md +14 -5
- package/README.pt.md +5 -4
- package/README.ru.md +5 -4
- package/README.zh-TW.md +5 -4
- package/README.zh.md +5 -4
- package/client.js +323 -40
- package/cordis.patch.yml +23 -20
- package/host/config.js +51 -7
- package/host/context-trim/fork.js +5 -1
- package/host/context-trim/index.js +329 -178
- package/host/context-trim/metrics.js +7 -7
- package/host/context-trim/state.js +351 -50
- package/host/context-trim/summarizer.js +147 -33
- package/host/context-trim/surface.js +415 -11
- package/host/context-trim/trigger.js +170 -51
- package/host/context-trim/view.js +144 -20
- package/host/context-watch.js +80 -73
- package/host/routes.js +64 -4
- package/package.json +2 -2
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
/** dsh-switchman context-trim summarizer.
|
|
2
2
|
*
|
|
3
|
-
* Produces the summary text for one fold block,
|
|
3
|
+
* Produces the summary text for one fold block, three ways:
|
|
4
4
|
*
|
|
5
5
|
* - summarizeWithModel — the L3 path (design §5): a side-model call via
|
|
6
6
|
* llm.prepareCall + call.stream (receiver-bound on the llm service, the
|
|
7
7
|
* same waterfall pattern intent classification uses). Guarded by a hard
|
|
8
8
|
* deadline (AbortController); ANY failure (timeout, error chunk, empty
|
|
9
9
|
* output) returns null and the caller skips the fold cycle — fail-open.
|
|
10
|
+
* A length-finished (output-limit) summary is NOT a failure: it returns
|
|
11
|
+
* the text prefixed with TRUNCATED_SUMMARY_MARK so the missing tail
|
|
12
|
+
* stays visible (inspection S2).
|
|
13
|
+
* Used by BOTH the raw T1 folds (renderMaterial) and the ladder T2/T3
|
|
14
|
+
* distillations (renderLadderMaterial, LADDER_SYSTEM_PROMPT).
|
|
10
15
|
* - mechanicalSummary — the deterministic fallback used by the emergency
|
|
11
|
-
* tier: structural heads of each message, no model round trip.
|
|
16
|
+
* tier: structural heads of each message, no model round trip. Never
|
|
17
|
+
* used for ladder folds (mechanical summary-of-summaries reclaims little
|
|
18
|
+
* and corrupts craft — docs/ladder-fold.md).
|
|
12
19
|
*
|
|
13
|
-
* The summary
|
|
20
|
+
* The summary prompts carry the bili compression rules: message refs and
|
|
14
21
|
* file paths verbatim, decisions WITH rationale, exact values, open
|
|
15
22
|
* objectives carry forward, verbose logs drop.
|
|
16
23
|
*/
|
|
@@ -25,6 +32,14 @@ export const SUMMARY_TIMEOUT_MS = 120_000;
|
|
|
25
32
|
* (a 1M-token material would burn the side-model call for no fidelity). */
|
|
26
33
|
export const MAX_MATERIAL_CHARS = 400_000;
|
|
27
34
|
|
|
35
|
+
/** Prefix for a summary whose side-model output ended early (finish=length):
|
|
36
|
+
* the text otherwise reads as settled fact while its tail is missing — and
|
|
37
|
+
* ladder T2/T3 distillation builds on summaries as established facts, so
|
|
38
|
+
* the gap must stay visible on the wire. Prefixed, not rejected: the fold
|
|
39
|
+
* still relieves pressure, and re-running at the same maxTokens would
|
|
40
|
+
* truncate again. (inspection S2) */
|
|
41
|
+
export const TRUNCATED_SUMMARY_MARK = "[truncated summary: the summarizer's output hit its limit (finish=length) — content after the cut may be missing] ";
|
|
42
|
+
|
|
28
43
|
/** System prompt for the side-model summarizer. English per repo docs
|
|
29
44
|
* style; the OUTPUT language follows the summarized material. */
|
|
30
45
|
export const SUMMARY_SYSTEM_PROMPT = [
|
|
@@ -47,6 +62,33 @@ export const SUMMARY_SYSTEM_PROMPT = [
|
|
|
47
62
|
"- Output ONLY the summary text.",
|
|
48
63
|
].join("\n");
|
|
49
64
|
|
|
65
|
+
/** Flat text body of one message (content blocks joined, unserializable
|
|
66
|
+
* values tolerated). Shared by renderMaterial and the zero-material
|
|
67
|
+
* rescue path. */
|
|
68
|
+
function renderMessageBody(message) {
|
|
69
|
+
const content = message?.content;
|
|
70
|
+
if (typeof content === "string") return content;
|
|
71
|
+
if (Array.isArray(content)) {
|
|
72
|
+
const chunks = [];
|
|
73
|
+
for (const block of content) {
|
|
74
|
+
if (block !== null && typeof block === "object" && typeof block.text === "string") chunks.push(block.text);
|
|
75
|
+
else {
|
|
76
|
+
try {
|
|
77
|
+
chunks.push(JSON.stringify(block ?? null));
|
|
78
|
+
} catch {
|
|
79
|
+
chunks.push("[unserializable]");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return chunks.join("\n");
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
return JSON.stringify(message ?? null);
|
|
87
|
+
} catch {
|
|
88
|
+
return "[unserializable]";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
50
92
|
/** Render the fold material for one message range: labeled, ref-stamped
|
|
51
93
|
* blocks. Deterministic (same slice, same bytes) — refs come from refOf so
|
|
52
94
|
* id-less messages stay stable across re-sends. */
|
|
@@ -58,29 +100,7 @@ export function renderMaterial(messages, startIdx, endIdx, refOf) {
|
|
|
58
100
|
const message = messages[i];
|
|
59
101
|
const ref = refOf(message, i);
|
|
60
102
|
const role = typeof message?.role === "string" ? message.role : "unknown";
|
|
61
|
-
let text;
|
|
62
|
-
const content = message?.content;
|
|
63
|
-
if (typeof content === "string") text = content;
|
|
64
|
-
else if (Array.isArray(content)) {
|
|
65
|
-
const chunks = [];
|
|
66
|
-
for (const block of content) {
|
|
67
|
-
if (block !== null && typeof block === "object" && typeof block.text === "string") chunks.push(block.text);
|
|
68
|
-
else {
|
|
69
|
-
try {
|
|
70
|
-
chunks.push(JSON.stringify(block ?? null));
|
|
71
|
-
} catch {
|
|
72
|
-
chunks.push("[unserializable]");
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
text = chunks.join("\n");
|
|
77
|
-
} else {
|
|
78
|
-
try {
|
|
79
|
-
text = JSON.stringify(message ?? null);
|
|
80
|
-
} catch {
|
|
81
|
-
text = "[unserializable]";
|
|
82
|
-
}
|
|
83
|
-
}
|
|
103
|
+
let text = renderMessageBody(message);
|
|
84
104
|
if (Array.isArray(message?.tool_calls) && message.tool_calls.length > 0) {
|
|
85
105
|
try {
|
|
86
106
|
text += `\n[tool_calls] ${JSON.stringify(message.tool_calls)}`;
|
|
@@ -97,7 +117,71 @@ export function renderMaterial(messages, startIdx, endIdx, refOf) {
|
|
|
97
117
|
parts.push(head + body);
|
|
98
118
|
chars += head.length + body.length;
|
|
99
119
|
}
|
|
100
|
-
if (truncated)
|
|
120
|
+
if (truncated) {
|
|
121
|
+
// [2026-11-07]-[a single over-ceiling message used to render ZERO
|
|
122
|
+
// material]-[parts stayed empty, the side model was asked to compress
|
|
123
|
+
// nothing, and the giant message got permanently replaced by a
|
|
124
|
+
// content-free summary. Keep the FIRST message's head plus a capped
|
|
125
|
+
// prefix so the summary at least carries what the message is.]
|
|
126
|
+
if (parts.length === 0 && startIdx <= endIdx) {
|
|
127
|
+
const first = messages[startIdx];
|
|
128
|
+
const firstRef = refOf(first, startIdx);
|
|
129
|
+
const role = typeof first?.role === "string" ? first.role : "unknown";
|
|
130
|
+
parts.push(`--- [m${firstRef}] (${role}) ---\n${renderMessageBody(first).slice(0, 2_000)}\n\n`);
|
|
131
|
+
}
|
|
132
|
+
parts.push(`--- [...material truncated after ${chars} chars; ${endIdx - startIdx + 1 - parts.length} messages dropped from this render...] ---\n`);
|
|
133
|
+
}
|
|
134
|
+
return parts.join("");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Distillation system prompt for the ladder tiers (T2 merges active T1
|
|
138
|
+
* summaries; T3 merges active T2 summaries). The output replaces its
|
|
139
|
+
* inputs PERMANENTLY — the craft rules are the same KEEP/DROP priorities,
|
|
140
|
+
* applied one level up. */
|
|
141
|
+
export const LADDER_SYSTEM_PROMPT = [
|
|
142
|
+
"You distill several context-fold summaries of one coding-agent session into ONE shorter self-contained plain-text summary that permanently replaces them.",
|
|
143
|
+
"",
|
|
144
|
+
"Treat the input summaries as facts already settled: never re-derive, never contradict, merge overlapping points.",
|
|
145
|
+
"",
|
|
146
|
+
"KEEP VERBATIM:",
|
|
147
|
+
"- Full file paths with line numbers (lib/hooks.ts:347) — never bare filenames.",
|
|
148
|
+
"- Function/class/type signatures and the critical code line that encodes a finding.",
|
|
149
|
+
"- Error messages and stack-trace key lines (exact text).",
|
|
150
|
+
"- Exact values: versions, config keys, thresholds, magic numbers.",
|
|
151
|
+
"- Decisions WITH their rationale (the because is load-bearing).",
|
|
152
|
+
"- Open objectives, unresolved TODOs, and open questions, each tagged with its original message refs.",
|
|
153
|
+
"",
|
|
154
|
+
"DROP (one line of lesson per failure stays): verbose detail once the exact value is captured, repetition between the summaries, settled exploration, process narration.",
|
|
155
|
+
"",
|
|
156
|
+
"FORMAT:",
|
|
157
|
+
"- Dense bullets, short thematic headers; no narrative prose.",
|
|
158
|
+
"- Keep the [m<ref>] anchors for anything a later step may need to unfold.",
|
|
159
|
+
"- Write the summary in the same language as the input summaries.",
|
|
160
|
+
"- Output ONLY the summary text.",
|
|
161
|
+
].join("\n");
|
|
162
|
+
|
|
163
|
+
/** Render the ladder distillation material for one run of consumed blocks:
|
|
164
|
+
* labeled, block-stamped sections (deterministic — same blocks, same
|
|
165
|
+
* bytes). Capped by the same material ceiling as renderMaterial. */
|
|
166
|
+
export function renderLadderMaterial(blocks) {
|
|
167
|
+
const parts = [];
|
|
168
|
+
let chars = 0;
|
|
169
|
+
let truncated = false;
|
|
170
|
+
for (const block of blocks) {
|
|
171
|
+
const tier = block?.tier === 3 ? 3 : block?.tier === 2 ? 2 : 1;
|
|
172
|
+
const tokens = Number.isFinite(block?.approxTokens) ? Math.round(block.approxTokens) : "?";
|
|
173
|
+
const count = Number.isInteger(block?.count) && block.count > 0 ? block.count : "?";
|
|
174
|
+
const summary = typeof block?.summary === "string" ? block.summary : "";
|
|
175
|
+
const head = `--- [block ${block?.id ?? "?"}] (tier ${tier}, ~${tokens} tokens, ${count} msgs) ---\n`;
|
|
176
|
+
const body = `${summary}\n\n`;
|
|
177
|
+
if (chars + head.length + body.length > MAX_MATERIAL_CHARS) {
|
|
178
|
+
truncated = true;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
parts.push(head + body);
|
|
182
|
+
chars += head.length + body.length;
|
|
183
|
+
}
|
|
184
|
+
if (truncated) parts.push(`--- [...material truncated after ${chars} chars; ${blocks.length - parts.length} blocks dropped from this render...] ---\n`);
|
|
101
185
|
return parts.join("");
|
|
102
186
|
}
|
|
103
187
|
|
|
@@ -127,16 +211,32 @@ export function mechanicalSummary(messages, startIdx, endIdx, refOf) {
|
|
|
127
211
|
return lines.join("\n");
|
|
128
212
|
}
|
|
129
213
|
|
|
214
|
+
/** One-line suffix carrying the adapter's failure detail: DSH terminal
|
|
215
|
+
* finishes ride { reason: { kind, failure: { message, code, status? } } } —
|
|
216
|
+
* without this the decisions.log only ever sees finishKind, and upstream
|
|
217
|
+
* rejections (quota/concurrency/risk) stay invisible. Single-lined, capped. */
|
|
218
|
+
function describeFailure(chunk) {
|
|
219
|
+
const failure = chunk?.reason?.failure ?? chunk?.failure ?? null;
|
|
220
|
+
const raw = typeof failure?.message === "string" && failure.message !== ""
|
|
221
|
+
? failure.message
|
|
222
|
+
: (typeof failure?.code === "string" && failure.code !== "" ? failure.code : "");
|
|
223
|
+
if (raw === "") return "";
|
|
224
|
+
return ` (${raw.replace(/\s+/gu, " ").trim().slice(0, 300)})`;
|
|
225
|
+
}
|
|
226
|
+
|
|
130
227
|
/** Side-model summary. llm: the dsh llm service (receiver-bound methods);
|
|
131
228
|
* route: { provider, model } | null (null → caller skips); materialText:
|
|
132
|
-
* renderMaterial output;
|
|
133
|
-
*
|
|
229
|
+
* renderMaterial output; options.systemPrompt overrides the default
|
|
230
|
+
* compression prompt (the ladder distillation passes its own); deadlineMs:
|
|
231
|
+
* hard budget. Returns the summary text, or null on ANY failure (caller
|
|
232
|
+
* skips the fold cycle). */
|
|
134
233
|
export async function summarizeWithModel(llm, route, materialText, options = {}) {
|
|
135
234
|
const fail = typeof options.onFail === "function" ? options.onFail : null;
|
|
136
235
|
if (route === null || route === undefined) { fail?.("no-route"); return null; }
|
|
137
236
|
if (typeof llm?.prepareCall !== "function") { fail?.("no-prepare-call"); return null; }
|
|
138
237
|
const deadlineMs = Number.isFinite(options.deadlineMs) && options.deadlineMs > 0 ? options.deadlineMs : SUMMARY_TIMEOUT_MS;
|
|
139
238
|
const maxTokens = Number.isFinite(options.maxTokens) && options.maxTokens > 0 ? options.maxTokens : 8_192;
|
|
239
|
+
const systemPrompt = typeof options.systemPrompt === "string" && options.systemPrompt !== "" ? options.systemPrompt : SUMMARY_SYSTEM_PROMPT;
|
|
140
240
|
const controller = new AbortController();
|
|
141
241
|
const timer = setTimeout(() => controller.abort(new Error("trim summary deadline")), deadlineMs);
|
|
142
242
|
try {
|
|
@@ -146,7 +246,7 @@ export async function summarizeWithModel(llm, route, materialText, options = {})
|
|
|
146
246
|
(controls) => controls,
|
|
147
247
|
);
|
|
148
248
|
let text = "";
|
|
149
|
-
|
|
249
|
+
let finishKind = null;
|
|
150
250
|
// stream options MUST carry the prepared call config — the adapter
|
|
151
251
|
// dispatch binds credentials/routing from it (same waterfall shape as
|
|
152
252
|
// the intent classifier; dropping it fails prepare-time integrity).
|
|
@@ -154,7 +254,7 @@ export async function summarizeWithModel(llm, route, materialText, options = {})
|
|
|
154
254
|
// source tag, exactly like the intent classifier's proven shape.
|
|
155
255
|
const streamOptions = {
|
|
156
256
|
...call.config,
|
|
157
|
-
system:
|
|
257
|
+
system: systemPrompt,
|
|
158
258
|
messages: [
|
|
159
259
|
{ id: randomUUID(), role: "user", content: [{ type: "text", text: materialText }], source: { kind: "dsh-switchman-trim-summary" } },
|
|
160
260
|
],
|
|
@@ -163,12 +263,26 @@ export async function summarizeWithModel(llm, route, materialText, options = {})
|
|
|
163
263
|
for await (const chunk of call.stream(streamOptions)) {
|
|
164
264
|
if (chunk?.type === "text-delta" && typeof chunk.text === "string") text += chunk.text;
|
|
165
265
|
else if (chunk?.type === "finish") {
|
|
166
|
-
|
|
167
|
-
|
|
266
|
+
// The translator's clean stop is { type:'finish', reason:{kind:'stop'} } —
|
|
267
|
+
// no top-level kind. Read the nested reason first so clean EOFs log
|
|
268
|
+
// finish=stop (not finish=none), and treat abort/error finishes as
|
|
269
|
+
// explicit failures in either shape.
|
|
270
|
+
const topKind = chunk.kind ?? null;
|
|
271
|
+
finishKind = chunk.reason?.kind ?? topKind;
|
|
272
|
+
if (topKind === "error" || topKind === "aborted" || finishKind === "error" || finishKind === "aborted") {
|
|
273
|
+
fail?.(`stream-${finishKind}${describeFailure(chunk)}`);
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
168
276
|
}
|
|
169
277
|
}
|
|
170
278
|
const summary = text.trim();
|
|
171
279
|
if (summary === "") { fail?.(`empty-output finish=${finishKind ?? "none"}`); return null; }
|
|
280
|
+
// A length/max-tokens finish means the tail of the material never got
|
|
281
|
+
// summarized, yet the text that DID land reads as settled fact — mark it
|
|
282
|
+
// so every later consumer (view rendering, ladder distillation) sees the
|
|
283
|
+
// gap. DSH's FinishReasonMap has no "length" kind: adapters map
|
|
284
|
+
// finish_reason=length to kind "max-tokens" (llm-deepseek translate).
|
|
285
|
+
if (finishKind === "length" || finishKind === "max-tokens") return `${TRUNCATED_SUMMARY_MARK}${summary}`;
|
|
172
286
|
return summary;
|
|
173
287
|
} catch (error) {
|
|
174
288
|
fail?.(`error: ${error?.message ?? error}`);
|