dsh-model-switch 0.4.24 → 0.4.26
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.md +25 -0
- package/PRODUCT.md +6 -4
- package/README.md +7 -13
- package/README.zh.md +6 -12
- package/cordis.patch.yml +9 -0
- package/lib/auto-review.js +57 -14
- package/lib/client.js +208 -139
- package/lib/compaction.js +97 -2
- package/lib/index.js +1 -301
- package/lib/{provider-group-CsZi7H29.js → provider-group-CwadImhH.js} +3 -9
- package/lib/{session-route-h9SAqajc.js → session-route-BCQFzqRP.js} +1 -1
- package/lib/session-route-BjNPlliu.js +172 -0
- package/lib/session-title.js +387 -34
- package/lib/subagent-runtime.js +26 -3
- package/lib/types/adapter-registry.d.ts +1 -1
- package/lib/types/auto-review.d.ts +38 -11
- package/lib/types/client/ModelSwitchSettings.d.ts +0 -1
- package/lib/types/client/locales.d.ts +2 -5
- package/lib/types/client-contract.d.ts +0 -1
- package/lib/types/compaction.d.ts +50 -6
- package/lib/types/host-settings.d.ts +0 -2
- package/lib/types/provider-group.d.ts +4 -10
- package/lib/types/session-title.d.ts +194 -14
- package/lib/types/subagent-runtime.d.ts +10 -0
- package/package.json +3 -2
- package/lib/types/client/derived-settings-scope.d.ts +0 -4
- package/lib/types/client/picker/main-default-restore.d.ts +0 -56
- package/lib/types/client/picker/provider-group-watch.d.ts +0 -48
- package/lib/types/client/remote-settings-scope.d.ts +0 -45
- package/lib/types/switch-compact.d.ts +0 -72
package/lib/session-title.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as sessionProviderOf, n as groupRoute, t as deploymentRoute } from "./session-route-BjNPlliu.js";
|
|
2
2
|
//#region lib/types/session-title.js
|
|
3
3
|
/**
|
|
4
4
|
* Model Switch-owned session-naming row.
|
|
@@ -10,16 +10,32 @@ import { n as sessionProviderOf, t as resolveCapabilityRoute } from "./session-r
|
|
|
10
10
|
* saved group is named through that group's pair while every other session keeps
|
|
11
11
|
* following its own model.
|
|
12
12
|
*
|
|
13
|
-
* The call itself stays inside the official implementation: the
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* without a registered provider.
|
|
13
|
+
* The call itself stays inside the official implementation: the auxiliary-LLM
|
|
14
|
+
* package records exactly the route that was called in `session/title-llm-request`,
|
|
15
|
+
* so the durable record and the actual request can never disagree. Nothing is
|
|
16
|
+
* remounted when settings change — the route is read through on every naming
|
|
17
|
+
* request — so a settings save can neither interrupt an in-flight naming request
|
|
18
|
+
* nor leave the profile without a registered provider.
|
|
20
19
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Two official auxiliary-LLM lines are supported, selected at runtime from the
|
|
21
|
+
* module surface, because the 0.2.1 release replaced the shared title call:
|
|
22
|
+
* - Up to DSH 0.2.0 (`generateSessionTitleWithLlm`): the module owns the system
|
|
23
|
+
* prompt, input framing, and output validation; the shared policy carries the
|
|
24
|
+
* title-length targets.
|
|
25
|
+
* - DSH 0.2.1+ (`executeSessionTitleLlm`): the provider owns the system prompt,
|
|
26
|
+
* the input, the reasoning-effort selection, and the output interpretation;
|
|
27
|
+
* the shared policy carries execution controls only. This row mirrors the
|
|
28
|
+
* official provider's wording and interpretation so a configured route keeps
|
|
29
|
+
* the official behavior.
|
|
30
|
+
*
|
|
31
|
+
* A naming request that fails is retried once with a widened output budget
|
|
32
|
+
* (max-token failures only) and then falls through the remaining route
|
|
33
|
+
* candidates — the session group's pair, the deployment-wide pair, the row's own
|
|
34
|
+
* pair, then the session's logged route — so one unavailable model or provider
|
|
35
|
+
* cannot leave the session on its deterministic fallback title forever.
|
|
36
|
+
*
|
|
37
|
+
* `cordis.patch.yml` disables the official rows (the seam allows one registered
|
|
38
|
+
* provider per context), and this row mirrors the cadence a disabled row
|
|
23
39
|
* declared, so a profile that restated the row as all-prompts keeps its cadence.
|
|
24
40
|
*
|
|
25
41
|
* @module dsh-model-switch/session-title
|
|
@@ -54,6 +70,43 @@ const TITLE_CADENCES = Object.freeze([Object.freeze({
|
|
|
54
70
|
automatic: "all-prompts",
|
|
55
71
|
firstPromptOnly: false
|
|
56
72
|
})]);
|
|
73
|
+
/** Execution controls both official auxiliary-LLM lines validate; defaults mirror the shipped official rows. */
|
|
74
|
+
const EXECUTION_DEFAULTS = Object.freeze({
|
|
75
|
+
maxInputBytes: 4096,
|
|
76
|
+
maxOutputTokens: 64,
|
|
77
|
+
timeoutMs: 6e4
|
|
78
|
+
});
|
|
79
|
+
/** Provider-owned title-length targets: required by the pre-0.2.1 line and moved into providers by 0.2.1. */
|
|
80
|
+
const TARGET_DEFAULTS = Object.freeze({
|
|
81
|
+
targetWords: 5,
|
|
82
|
+
targetCjkCharacters: 10
|
|
83
|
+
});
|
|
84
|
+
/** Execution keys accepted by both lines; provider-owned keys are excluded on purpose. */
|
|
85
|
+
const EXECUTION_KEYS = Object.freeze([
|
|
86
|
+
"maxInputBytes",
|
|
87
|
+
"maxOutputTokens",
|
|
88
|
+
"timeoutMs",
|
|
89
|
+
"provider",
|
|
90
|
+
"model"
|
|
91
|
+
]);
|
|
92
|
+
/**
|
|
93
|
+
* Read one call entry point without assuming it exists.
|
|
94
|
+
*
|
|
95
|
+
* The module namespace of a host line that does not export the name can throw on
|
|
96
|
+
* access (mocked namespaces do), so a missing entry point reads as undefined
|
|
97
|
+
* instead of failing the row.
|
|
98
|
+
* @param module - the loaded auxiliary-LLM package.
|
|
99
|
+
* @param key - the call entry point to read.
|
|
100
|
+
* @returns The callable, or undefined when this line does not expose it.
|
|
101
|
+
*/
|
|
102
|
+
function titleCallOf(module, key) {
|
|
103
|
+
try {
|
|
104
|
+
const value = module[key];
|
|
105
|
+
return typeof value === "function" ? value : void 0;
|
|
106
|
+
} catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
57
110
|
/**
|
|
58
111
|
* The cadence one official package name declares.
|
|
59
112
|
* @param pkg - the disabled row's package name.
|
|
@@ -128,42 +181,341 @@ function forwardedConfig(entry) {
|
|
|
128
181
|
*/
|
|
129
182
|
function officialTitlePackage(entry) {
|
|
130
183
|
const pkg = entry.options?.name;
|
|
131
|
-
|
|
184
|
+
if (typeof pkg !== "string" || pkg.trim() === "") return void 0;
|
|
185
|
+
return pkg;
|
|
186
|
+
}
|
|
187
|
+
/** A positive integer, or undefined for anything else (blank, fractional, negative, non-number). */
|
|
188
|
+
function positiveInteger(value) {
|
|
189
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) return void 0;
|
|
190
|
+
return value;
|
|
132
191
|
}
|
|
133
192
|
/**
|
|
134
|
-
*
|
|
193
|
+
* Split one untrusted stored policy into the halves both official lines accept.
|
|
135
194
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
195
|
+
* Execution controls pass through untouched (an invalid value still fails the
|
|
196
|
+
* official validator). Absent execution fields fall back to the shipped
|
|
197
|
+
* official defaults, because a profile that mounts no official row has no
|
|
198
|
+
* policy to forward and this row must still run. Title-length targets are read
|
|
199
|
+
* here for the system instruction: the pre-0.2.1 line wants them in its shared
|
|
200
|
+
* config, while 0.2.1 moved them into each provider.
|
|
201
|
+
* @param forwarded - the disabled official row's stored config.
|
|
202
|
+
* @returns The execution subset and the title-length targets.
|
|
142
203
|
*/
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
204
|
+
function splitTitlePolicy(forwarded) {
|
|
205
|
+
const execution = {};
|
|
206
|
+
for (const key of EXECUTION_KEYS) {
|
|
207
|
+
const value = forwarded[key];
|
|
208
|
+
if (value !== void 0) execution[key] = value;
|
|
148
209
|
}
|
|
210
|
+
for (const [key, fallback] of Object.entries(EXECUTION_DEFAULTS)) if (execution[key] === void 0) execution[key] = fallback;
|
|
211
|
+
return {
|
|
212
|
+
execution,
|
|
213
|
+
targets: {
|
|
214
|
+
targetWords: positiveInteger(forwarded.targetWords) ?? TARGET_DEFAULTS.targetWords,
|
|
215
|
+
targetCjkCharacters: positiveInteger(forwarded.targetCjkCharacters) ?? TARGET_DEFAULTS.targetCjkCharacters
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Detect the 0.2.1+ auxiliary-LLM line.
|
|
221
|
+
* @param module - the loaded auxiliary-LLM package.
|
|
222
|
+
* @returns Whether the module exposes the provider-prepared call.
|
|
223
|
+
*/
|
|
224
|
+
function isModernTitleModule(module) {
|
|
225
|
+
return titleCallOf(module, "executeSessionTitleLlm") !== void 0;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Validate the stored policy through the official validator of the installed line.
|
|
229
|
+
* @param module - the loaded auxiliary-LLM package.
|
|
230
|
+
* @param forwarded - the disabled official row's stored config.
|
|
231
|
+
* @returns The validated execution config and title-length targets.
|
|
232
|
+
* @throws when the official validator rejects the stored policy.
|
|
233
|
+
*/
|
|
234
|
+
function resolveNamingPolicy(module, forwarded) {
|
|
235
|
+
const split = splitTitlePolicy(forwarded);
|
|
236
|
+
const resolve = module.resolveSessionTitleLlmConfig;
|
|
237
|
+
if (isModernTitleModule(module)) return {
|
|
238
|
+
config: resolve(split.execution),
|
|
239
|
+
targets: split.targets
|
|
240
|
+
};
|
|
241
|
+
const config = resolve({
|
|
242
|
+
...split.execution,
|
|
243
|
+
...split.targets
|
|
244
|
+
});
|
|
245
|
+
return {
|
|
246
|
+
config,
|
|
247
|
+
targets: {
|
|
248
|
+
targetWords: config.targetWords ?? split.targets.targetWords,
|
|
249
|
+
targetCjkCharacters: config.targetCjkCharacters ?? split.targets.targetCjkCharacters
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Candidate routes for one naming request, most specific first and deduplicated.
|
|
255
|
+
*
|
|
256
|
+
* The stored session group's pair wins, then the deployment-wide pair, then the
|
|
257
|
+
* row's own configured pair, then the session's logged route. The list is also
|
|
258
|
+
* the fallback order: a failing candidate falls through to the next one.
|
|
259
|
+
* @param settings - live Model Switch settings; undefined skips the stored pairs.
|
|
260
|
+
* @param provider - the delegating session's provider, when one is observable.
|
|
261
|
+
* @param policy - the validated row policy (its own pair is the third candidate).
|
|
262
|
+
* @param requestRoute - the session's logged main-request route, when one exists.
|
|
263
|
+
* @returns The ordered, deduplicated candidates; empty when only the row policy applies.
|
|
264
|
+
*/
|
|
265
|
+
function titleRouteCandidates(settings, provider, policy, requestRoute) {
|
|
266
|
+
const candidates = [];
|
|
267
|
+
const push = (route) => {
|
|
268
|
+
if (route === void 0) return;
|
|
269
|
+
if (candidates.some((candidate) => candidate.provider === route.provider && candidate.model === route.model)) return;
|
|
270
|
+
candidates.push(route);
|
|
271
|
+
};
|
|
272
|
+
push(groupRoute(settings, provider, "title"));
|
|
273
|
+
push(deploymentRoute(settings, "title"));
|
|
274
|
+
if (policy.provider !== void 0 && policy.model !== void 0) push({
|
|
275
|
+
provider: policy.provider,
|
|
276
|
+
model: policy.model
|
|
277
|
+
});
|
|
278
|
+
push(requestRoute);
|
|
279
|
+
return candidates;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Whether a failure may be retried on another candidate.
|
|
283
|
+
* A superseded or disposed request keeps no budget, and a deadline that fired is
|
|
284
|
+
* the caller's bound, not the route's fault: both end the attempt loop.
|
|
285
|
+
* @param error - the failure thrown by the auxiliary call.
|
|
286
|
+
* @param request - the service-owned request whose signal bounds the work.
|
|
287
|
+
* @returns Whether another candidate may be tried.
|
|
288
|
+
*/
|
|
289
|
+
function shouldTryNextCandidate(error, request) {
|
|
290
|
+
if (request.signal.aborted) return false;
|
|
291
|
+
return error?.code !== "SESSION_TITLE_TIMEOUT";
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Whether the failure is the model filling its output budget before finishing.
|
|
295
|
+
* Official lines report this as `title output reached maxOutputTokens`; a widened
|
|
296
|
+
* budget is the only retry that can succeed for it.
|
|
297
|
+
* @param error - the failure thrown by the auxiliary call.
|
|
298
|
+
* @returns Whether one widened-budget retry is worth trying.
|
|
299
|
+
*/
|
|
300
|
+
function isMaxTokensFailure(error) {
|
|
301
|
+
if (!(error instanceof Error)) return false;
|
|
302
|
+
return /maxoutputtokens|max[-_ ]tokens/iu.test(error.message);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* A widened output budget for the same route.
|
|
306
|
+
* @param maxOutputTokens - the policy's configured budget.
|
|
307
|
+
* @returns Four times the budget, capped at 4096 tokens.
|
|
308
|
+
*/
|
|
309
|
+
function widerTitleBudget(maxOutputTokens) {
|
|
310
|
+
return Math.min(maxOutputTokens * 4, 4096);
|
|
311
|
+
}
|
|
312
|
+
/** Asterisk emphasis wrapping a whole line, such as `**Title**`. */
|
|
313
|
+
const EMPHASIS_WRAPPER = /^(?<marker>\*{1,3})(?<inner>\S(?:.*\S)?)\k<marker>$/u;
|
|
314
|
+
/** OSC control sequences, such as a title hyperlink. */
|
|
315
|
+
const OSC_SEQUENCE = /(?:\u001B\]|\u009D)(?:(?!\u0007|\u001B\\)[\s\S])*(?:\u0007|\u001B\\|$)/gu;
|
|
316
|
+
/** Control-sequence-introducer escapes such as SGR color codes. */
|
|
317
|
+
const CSI_SEQUENCE = /(?:\u001B\[|\u009B)[0-?]*[ -/]*[@-~]/gu;
|
|
318
|
+
/** Remaining two-byte ESC control sequences. */
|
|
319
|
+
const ESC_SEQUENCE = /\u001B[@-_]/gu;
|
|
320
|
+
/** Non-whitespace C0/C1 control characters. */
|
|
321
|
+
const CONTROL_CHARACTER = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/gu;
|
|
322
|
+
/** Directional and invisible controls that can make a displayed title deceptive. */
|
|
323
|
+
const DIRECTIONAL_CONTROL = /[\u200B\u200E\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF]/gu;
|
|
324
|
+
/**
|
|
325
|
+
* Normalize one accepted title exactly like the official service.
|
|
326
|
+
* @param input - untrusted title text.
|
|
327
|
+
* @returns A terminal-safe, whitespace-normalized one-line title, possibly empty.
|
|
328
|
+
*/
|
|
329
|
+
function normalizeTitleText(input) {
|
|
330
|
+
return input.replace(OSC_SEQUENCE, "").replace(CSI_SEQUENCE, "").replace(ESC_SEQUENCE, "").replace(CONTROL_CHARACTER, "").replace(DIRECTIONAL_CONTROL, "").replace(/\s+/gu, " ").trim();
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Take the title from model output: the first non-empty line, without asterisk
|
|
334
|
+
* emphasis wrapping that whole line. Models that disregard the one-line
|
|
335
|
+
* instruction put the title first and commentary after it.
|
|
336
|
+
* @param text - joined text blocks of the auxiliary response.
|
|
337
|
+
* @returns The candidate title line.
|
|
338
|
+
*/
|
|
339
|
+
function titleFromOutput(text) {
|
|
340
|
+
const line = text.split(/\r?\n/u).map((item) => item.trim()).find((item) => item.length > 0) ?? "";
|
|
341
|
+
const groups = EMPHASIS_WRAPPER.exec(line)?.groups;
|
|
342
|
+
const marker = groups?.["marker"];
|
|
343
|
+
const inner = groups?.["inner"];
|
|
344
|
+
return marker === void 0 || inner === void 0 || inner.includes(marker) ? line : inner;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Interpret one assembled 0.2.1+ response as a title.
|
|
348
|
+
* Rejects terminal tool-call and max-token outcomes exactly like the official
|
|
349
|
+
* provider, so the row keeps the official acceptance rules.
|
|
350
|
+
* @param response - the assembled auxiliary response.
|
|
351
|
+
* @returns A non-empty normalized title.
|
|
352
|
+
* @throws when the finish reason, a tool-call block, or empty text makes the response unusable.
|
|
353
|
+
*/
|
|
354
|
+
function titleFromResponse(response) {
|
|
355
|
+
switch (response.finish.kind) {
|
|
356
|
+
case "stop": break;
|
|
357
|
+
case "tool-calls": throw new Error("session-title-llm: title output must contain text only");
|
|
358
|
+
case "max-tokens": throw new Error("session-title-llm: title output reached maxOutputTokens");
|
|
359
|
+
default: throw new Error(`session-title-llm: unsupported finish reason "${String(response.finish.kind)}"`);
|
|
360
|
+
}
|
|
361
|
+
if (response.blocks.some((block) => block.type === "tool-call")) throw new Error("session-title-llm: title output must contain text only");
|
|
362
|
+
const title = normalizeTitleText(titleFromOutput(response.blocks.filter((block) => block.type === "text").map((block) => block.text ?? "").join(" ")));
|
|
363
|
+
if (title.length === 0) throw new Error("session-title-llm: title model produced no text");
|
|
364
|
+
return title;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Select the route's least supported reasoning effort for a title call, as the
|
|
368
|
+
* official 0.2.1 provider does: naming is a short auxiliary task, and a full
|
|
369
|
+
* reasoning budget can consume the whole output allowance.
|
|
370
|
+
* @param model - the prepared model metadata.
|
|
371
|
+
* @returns The first declared effort id, or undefined when the model has none.
|
|
372
|
+
*/
|
|
373
|
+
function selectTitleReasoningEffort(model) {
|
|
374
|
+
return model.reasoning?.efforts?.[0]?.id;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* The official 0.2.1+ system instruction, including the optional current-title anchor.
|
|
378
|
+
* @param targets - title-length targets.
|
|
379
|
+
* @param hasCurrentTitle - whether the request is anchored to an existing provider title.
|
|
380
|
+
* @returns The system prompt text.
|
|
381
|
+
*/
|
|
382
|
+
function titleSystemPrompt(targets, hasCurrentTitle) {
|
|
383
|
+
return [
|
|
384
|
+
"Create a concise title for an AI coding-assistant session from the supplied human messages.",
|
|
385
|
+
"Return only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.",
|
|
386
|
+
"Use the language of the messages.",
|
|
387
|
+
`Aim for about ${targets.targetWords} words in non-CJK languages or ${targets.targetCjkCharacters} CJK characters.`,
|
|
388
|
+
"If the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.",
|
|
389
|
+
...hasCurrentTitle ? [
|
|
390
|
+
"An existing title is supplied as currentTitle. If it still accurately describes the main topic or task, return it exactly unchanged.",
|
|
391
|
+
"Follow-up questions, additional details within the same topic, acknowledgements such as \"thanks\", and requests to continue do not by themselves justify a title change.",
|
|
392
|
+
"Do not reword, polish, shorten, or replace synonyms in an adequate title. Keeping its exact wording takes priority over the target length.",
|
|
393
|
+
"Change the title only when the messages materially change or expand the main topic or task so that the existing title is no longer accurate."
|
|
394
|
+
] : []
|
|
395
|
+
].join("\n");
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Frame the exact messages as JSON so user text cannot break structural delimiters.
|
|
399
|
+
* @param messages - the selected human messages.
|
|
400
|
+
* @param currentTitle - the accepted provider title to anchor on, when there is one.
|
|
401
|
+
* @returns The framed user input.
|
|
402
|
+
*/
|
|
403
|
+
function frameTitleInput(messages, currentTitle) {
|
|
404
|
+
if (currentTitle === void 0) return `Generate the session title from this JSON array of human messages:\n${JSON.stringify(messages)}`;
|
|
405
|
+
return `Update the session title from this JSON object:\n${JSON.stringify({
|
|
406
|
+
currentTitle,
|
|
407
|
+
messages
|
|
408
|
+
})}`;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* The accepted provider title of one request, when the service supplied one.
|
|
412
|
+
* Only a provider-sourced title anchors an update; a fallback title does not.
|
|
413
|
+
* @param request - the service-owned naming request.
|
|
414
|
+
* @returns The current title, or undefined when there is nothing to anchor on.
|
|
415
|
+
*/
|
|
416
|
+
function currentProviderTitle(request) {
|
|
417
|
+
const current = request.currentTitle;
|
|
418
|
+
if (current === void 0 || current.source?.kind !== "provider" || typeof current.title !== "string") return void 0;
|
|
419
|
+
return current.title;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Run one candidate through the auxiliary-LLM line the profile installed.
|
|
423
|
+
* @param ctx - this row's context, exposing `llm`.
|
|
424
|
+
* @param module - the loaded auxiliary-LLM package.
|
|
425
|
+
* @param plan - the profile's naming policy and cadence.
|
|
426
|
+
* @param request - service-owned session, message revision, and cancellation.
|
|
427
|
+
* @param attempt - one candidate route with its composed config.
|
|
428
|
+
* @returns The official provider result.
|
|
429
|
+
*/
|
|
430
|
+
async function runTitleCall(ctx, module, plan, request, attempt) {
|
|
431
|
+
const messages = selectTitleMessages(request.messages, plan.cadence.firstPromptOnly);
|
|
432
|
+
const execute = titleCallOf(module, "executeSessionTitleLlm");
|
|
433
|
+
if (execute !== void 0) {
|
|
434
|
+
const anchor = plan.cadence.firstPromptOnly ? void 0 : currentProviderTitle(request);
|
|
435
|
+
const prepared = {
|
|
436
|
+
system: titleSystemPrompt(plan.targets, anchor !== void 0),
|
|
437
|
+
input: frameTitleInput(messages, anchor),
|
|
438
|
+
messageSeqs: messages.map((message) => message.seq),
|
|
439
|
+
selectReasoningEffort: selectTitleReasoningEffort
|
|
440
|
+
};
|
|
441
|
+
const response = await execute(ctx, attempt.config, request, plan.cadence.id, prepared);
|
|
442
|
+
return {
|
|
443
|
+
title: titleFromResponse(response),
|
|
444
|
+
messageSeqs: prepared.messageSeqs,
|
|
445
|
+
model: response.model
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
const legacy = titleCallOf(module, "generateSessionTitleWithLlm");
|
|
449
|
+
if (legacy === void 0) throw new Error(`model-switch: ${TITLE_LLM_PACKAGE} exposes no title call this row understands`);
|
|
450
|
+
return legacy(ctx, {
|
|
451
|
+
...plan.targets,
|
|
452
|
+
...attempt.config
|
|
453
|
+
}, request, messages, plan.cadence.id);
|
|
149
454
|
}
|
|
150
455
|
/**
|
|
151
456
|
* Execute one naming request through the official implementation.
|
|
152
457
|
*
|
|
153
458
|
* The route is resolved from the session being titled — its provider selects the
|
|
154
|
-
* saved group,
|
|
155
|
-
*
|
|
156
|
-
*
|
|
459
|
+
* saved group, the flat settings pair is the deployment default — and every
|
|
460
|
+
* candidate is tried in order until one succeeds. A max-token failure first gets
|
|
461
|
+
* one widened-budget retry on the same route; every other failure moves on.
|
|
157
462
|
* @param ctx - this row's context, exposing `llm`.
|
|
158
463
|
* @param module - the loaded auxiliary-LLM module.
|
|
159
464
|
* @param plan - the profile's fixed naming policy.
|
|
160
465
|
* @param request - service-owned session, message revision, and cancellation.
|
|
161
466
|
* @returns The official provider result.
|
|
467
|
+
* @throws The last candidate failure when every candidate fails.
|
|
162
468
|
*/
|
|
163
469
|
async function generateTitle(ctx, module, plan, request) {
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
|
|
470
|
+
const candidates = titleRouteCandidates(currentSettings(ctx), sessionProviderOf(ctx, request.session), plan.config, request.route);
|
|
471
|
+
const attempts = candidates.length === 0 ? [{
|
|
472
|
+
route: void 0,
|
|
473
|
+
config: plan.config
|
|
474
|
+
}] : candidates.map((route) => ({
|
|
475
|
+
route,
|
|
476
|
+
config: composeTitleRoute(plan.config, route)
|
|
477
|
+
}));
|
|
478
|
+
let lastError;
|
|
479
|
+
for (const attempt of attempts) try {
|
|
480
|
+
return await runTitleCall(ctx, module, plan, request, attempt);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
lastError = error;
|
|
483
|
+
if (!shouldTryNextCandidate(error, request)) break;
|
|
484
|
+
if (!isMaxTokensFailure(error)) continue;
|
|
485
|
+
const widened = {
|
|
486
|
+
route: attempt.route,
|
|
487
|
+
config: {
|
|
488
|
+
...attempt.config,
|
|
489
|
+
maxOutputTokens: widerTitleBudget(attempt.config.maxOutputTokens)
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
try {
|
|
493
|
+
return await runTitleCall(ctx, module, plan, request, widened);
|
|
494
|
+
} catch (retryError) {
|
|
495
|
+
lastError = retryError;
|
|
496
|
+
if (!shouldTryNextCandidate(retryError, request)) break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const tried = attempts.map((attempt) => attempt.route === void 0 ? "the row policy" : `${attempt.route.provider}/${attempt.route.model}`).join(" → ");
|
|
500
|
+
if (!request.signal.aborted) ctx.logger.warn(`model-switch: session title generation failed on every candidate (${tried}): ${String(lastError)}`);
|
|
501
|
+
throw lastError;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Read live Model Switch settings without requiring the host row.
|
|
505
|
+
*
|
|
506
|
+
* The host is read structurally instead of injected so this row keeps mirroring
|
|
507
|
+
* the official naming behavior when the `model-switch` row is disabled: an
|
|
508
|
+
* unreachable host resolves to the row's own policy rather than to a pinned
|
|
509
|
+
* route.
|
|
510
|
+
* @param ctx - this row's context.
|
|
511
|
+
* @returns The settings, or undefined when the host is unavailable.
|
|
512
|
+
*/
|
|
513
|
+
function currentSettings(ctx) {
|
|
514
|
+
try {
|
|
515
|
+
return ctx.get("modelSwitch", false)?.currentSettings();
|
|
516
|
+
} catch {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
167
519
|
}
|
|
168
520
|
/**
|
|
169
521
|
* Mount the Model Switch session-naming row for this profile.
|
|
@@ -180,7 +532,7 @@ async function apply(ctx) {
|
|
|
180
532
|
const cadence = titleCadence(entry === void 0 ? void 0 : officialTitlePackage(entry)) ?? TITLE_CADENCES[0];
|
|
181
533
|
if (cadence === void 0) return;
|
|
182
534
|
if (entry === void 0) ctx.logger.info("model-switch: no instantiated official session-title row to mirror; naming uses the first-prompt cadence");
|
|
183
|
-
/** The displaced row's naming policy, or the
|
|
535
|
+
/** The displaced row's naming policy, or the shipped defaults when it is absent. */
|
|
184
536
|
const forwarded = entry === void 0 ? {} : forwardedConfig(entry);
|
|
185
537
|
let module;
|
|
186
538
|
try {
|
|
@@ -190,9 +542,9 @@ async function apply(ctx) {
|
|
|
190
542
|
ctx.logger.error(cause);
|
|
191
543
|
return;
|
|
192
544
|
}
|
|
193
|
-
let
|
|
545
|
+
let policy;
|
|
194
546
|
try {
|
|
195
|
-
|
|
547
|
+
policy = resolveNamingPolicy(module, forwarded);
|
|
196
548
|
} catch (cause) {
|
|
197
549
|
ctx.logger.error("model-switch: the official session-title row has no valid naming policy; naming keeps the deterministic fallback");
|
|
198
550
|
ctx.logger.error(cause);
|
|
@@ -200,7 +552,8 @@ async function apply(ctx) {
|
|
|
200
552
|
}
|
|
201
553
|
const plan = {
|
|
202
554
|
cadence,
|
|
203
|
-
config
|
|
555
|
+
config: policy.config,
|
|
556
|
+
targets: policy.targets
|
|
204
557
|
};
|
|
205
558
|
try {
|
|
206
559
|
ctx.sessionTitle.register({
|
|
@@ -214,4 +567,4 @@ async function apply(ctx) {
|
|
|
214
567
|
}
|
|
215
568
|
}
|
|
216
569
|
//#endregion
|
|
217
|
-
export { TITLE_CADENCES, apply, composeTitleRoute, forwardedConfig, inject, name, officialTitleEntry, officialTitlePackage, selectTitleMessages, titleCadence };
|
|
570
|
+
export { TITLE_CADENCES, apply, composeTitleRoute, forwardedConfig, frameTitleInput, inject, isMaxTokensFailure, isModernTitleModule, name, normalizeTitleText, officialTitleEntry, officialTitlePackage, resolveNamingPolicy, selectTitleMessages, selectTitleReasoningEffort, shouldTryNextCandidate, splitTitlePolicy, titleCadence, titleFromOutput, titleFromResponse, titleRouteCandidates, titleSystemPrompt, widerTitleBudget };
|
package/lib/subagent-runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as sessionSubagentRouteOf } from "./session-route-BjNPlliu.js";
|
|
2
2
|
import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
3
3
|
import OfficialSubagentRuntime from "@deepseek-ai/dsh-subagent";
|
|
4
4
|
//#region lib/types/subagent-runtime.js
|
|
@@ -144,9 +144,32 @@ async function mountWithStartupFallback(candidate, fallback) {
|
|
|
144
144
|
startupFailure(error, await ledger.disposeAll());
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Operator-facing notice for a Host whose Subagent runtime cannot be routed.
|
|
149
|
+
*
|
|
150
|
+
* DSH 0.2.1 replaced `start()`/`startContinuable()` with the activation API, so
|
|
151
|
+
* this adapter's override point is gone; the row then mounts the untouched
|
|
152
|
+
* official runtime instead of guessing a new seam.
|
|
153
|
+
* @param error - the typed startup incompatibility that rejected the candidate.
|
|
154
|
+
* @returns The notice logged once at startup.
|
|
155
|
+
*/
|
|
156
|
+
function routedRuntimeUnavailableNotice(error) {
|
|
157
|
+
return `model-switch: the routed Subagent runtime is unavailable on this DSH Host (${error.message}); delegation follows the official route instead`;
|
|
158
|
+
}
|
|
159
|
+
/** Log one notice without letting a missing logger service fail the startup. */
|
|
160
|
+
function reportNotice(ctx, message) {
|
|
161
|
+
try {
|
|
162
|
+
ctx.logger.warn(message);
|
|
163
|
+
} catch {}
|
|
164
|
+
}
|
|
147
165
|
async function mountProfileRuntime(ctx) {
|
|
148
166
|
return (await mountWithStartupFallback(async (track) => {
|
|
149
|
-
|
|
167
|
+
try {
|
|
168
|
+
assertOfficialRuntimeSurface();
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof StartupIncompatibilityError) reportNotice(ctx, routedRuntimeUnavailableNotice(error));
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
150
173
|
assertRoutingSurface(ctx);
|
|
151
174
|
const candidate = ctx.plugin(ModelSwitchSubagentRuntime);
|
|
152
175
|
track(() => candidate.dispose());
|
|
@@ -186,4 +209,4 @@ var ModelSwitchSubagentRuntime = class extends OfficialSubagentRuntime {
|
|
|
186
209
|
}
|
|
187
210
|
};
|
|
188
211
|
//#endregion
|
|
189
|
-
export { ModelSwitchSubagentRuntime, StartupIncompatibilityError, profileSubagentRuntime as default, profileSubagentRuntime, mountWithStartupFallback, routeSubagentRequest };
|
|
212
|
+
export { ModelSwitchSubagentRuntime, StartupIncompatibilityError, profileSubagentRuntime as default, profileSubagentRuntime, mountWithStartupFallback, routeSubagentRequest, routedRuntimeUnavailableNotice };
|
|
@@ -41,7 +41,7 @@ export interface ModelSwitchImageAdapter {
|
|
|
41
41
|
}
|
|
42
42
|
export interface ModelSwitchProviderAdapters {
|
|
43
43
|
readonly provider: string;
|
|
44
|
-
/**
|
|
44
|
+
/** Optional runtime role declared by the provider integration. */
|
|
45
45
|
readonly role?: 'llm' | 'agent';
|
|
46
46
|
readonly search?: ModelSwitchSearchAdapter;
|
|
47
47
|
readonly image?: ModelSwitchImageAdapter;
|
|
@@ -5,17 +5,20 @@
|
|
|
5
5
|
* reviewer to the session's own request route (`header.config`). This row runs
|
|
6
6
|
* the official `apply` under a scoped child context whose `llm` service is
|
|
7
7
|
* shadowed by a wrapper that rewrites only `stream()`'s provider/model — and
|
|
8
|
-
* only
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* only when the reviewing session's derived route (its provider group's
|
|
9
|
+
* reviewer pair, else the deployment-wide reviewer card) pins one. With no
|
|
10
|
+
* route pinned the wrapper passes options through by identity, so the reviewer
|
|
11
|
+
* follows the session model exactly.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
* its
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
13
|
+
* Although the official gate omits `sessionId` and `purpose` from its frozen
|
|
14
|
+
* `GenerateOptions`, its `tools/pre-execute` waterfall listener receives
|
|
15
|
+
* `exec.agent` (`exec.agent.session`). After running `official.apply(scope)`,
|
|
16
|
+
* this module registers a `{ prepend: true }` `tools/pre-execute` listener on
|
|
17
|
+
* the same scope — which Cordis places ahead of the official gate's listener in
|
|
18
|
+
* the waterfall — and binds `exec.agent?.session` in an `AsyncLocalStorage`
|
|
19
|
+
* scope around `next()`. Every `scope.llm.stream()` call from `classifyRisk`
|
|
20
|
+
* therefore resolves the exact session being reviewed, with zero cross-session
|
|
21
|
+
* interference when multiple sessions review tool calls concurrently.
|
|
19
22
|
*
|
|
20
23
|
* The official gate stays an opt-in capability: the row mounts it only when
|
|
21
24
|
* the profile actually started with the experimental-auto-review bundle.
|
|
@@ -24,8 +27,10 @@
|
|
|
24
27
|
*
|
|
25
28
|
* @module dsh-model-switch/auto-review
|
|
26
29
|
*/
|
|
30
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
27
31
|
import type { Context } from '@deepseek-ai/cordis';
|
|
28
32
|
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
33
|
+
import type { Session } from '@deepseek-ai/dsh-session';
|
|
29
34
|
/** Cordis plugin name used by loader diagnostics. */
|
|
30
35
|
export declare const name = "model-switch-auto-review";
|
|
31
36
|
/** The official gate's host services plus Model Switch's settings owner. */
|
|
@@ -60,12 +65,34 @@ export declare function autoReviewRouteOf(settings: {
|
|
|
60
65
|
* @returns A structurally compatible llm face for the scoped context.
|
|
61
66
|
*/
|
|
62
67
|
export declare function reviewLlm(real: ReviewLlm, route: () => AutoReviewRoute | undefined): ReviewLlm;
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the reviewer route for one session (or deployment-wide when no session is bound).
|
|
70
|
+
*
|
|
71
|
+
* @param ctx - host context whose modelSwitch settings and sessionProjections derive the route.
|
|
72
|
+
* @param session - the session whose tool call is being reviewed, when known.
|
|
73
|
+
* @returns The derived reviewer route, or undefined to follow the session model.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveSessionAutoReviewRoute(ctx: Context, session?: Session): AutoReviewRoute | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Install the outer `tools/pre-execute` correlation listener ahead of the official gate.
|
|
78
|
+
*
|
|
79
|
+
* Because Cordis `on(..., { prepend: true })` unshifts onto the event hook list,
|
|
80
|
+
* registering this listener AFTER `official.apply(scope)` places it at index 0
|
|
81
|
+
* — directly ahead of the official gate's own prepended listener — so
|
|
82
|
+
* `activeSession.run(exec.agent?.session, next)` wraps the entire official
|
|
83
|
+
* review execution.
|
|
84
|
+
*
|
|
85
|
+
* @param scope - the review scope context where the gate is mounted.
|
|
86
|
+
* @param activeSession - AsyncLocalStorage holding the session under review.
|
|
87
|
+
*/
|
|
88
|
+
export declare function bindAutoReviewSessionCorrelation(scope: Context, activeSession: AsyncLocalStorage<Session | undefined>): void;
|
|
63
89
|
/**
|
|
64
90
|
* Build the scoped context the official gate runs under.
|
|
65
91
|
* @param ctx - host context whose modelSwitch settings pin the reviewer route.
|
|
92
|
+
* @param activeSession - optional AsyncLocalStorage correlating each `tools/pre-execute` call to its session.
|
|
66
93
|
* @returns A child context shadowing only the llm service.
|
|
67
94
|
*/
|
|
68
|
-
export declare function reviewScope(ctx: Context): Context;
|
|
95
|
+
export declare function reviewScope(ctx: Context, activeSession?: AsyncLocalStorage<Session | undefined>): Context;
|
|
69
96
|
/**
|
|
70
97
|
* Mount the official gate under the reviewer-route scope.
|
|
71
98
|
*
|
|
@@ -26,7 +26,6 @@ export interface ModelSwitchSettingsFace {
|
|
|
26
26
|
*/
|
|
27
27
|
saveSubagent: (ops: readonly ConfigViewOp<SubagentSettingsView>[]) => Promise<void>;
|
|
28
28
|
saveCapability: (route: 'search' | 'image' | 'autoReview' | 'compaction' | 'title', ops: readonly ConfigViewOp<CapabilityRouteView>[]) => Promise<void>;
|
|
29
|
-
setCompactOnSwitch: (value: boolean) => Promise<void>;
|
|
30
29
|
/** Replace or remove (undefined) one provider's saved capability group. */
|
|
31
30
|
saveProviderGroup: (provider: string, group: ProviderGroupConfig | undefined) => Promise<void>;
|
|
32
31
|
loadCatalog: () => Promise<readonly ModelProviderGroup[]>;
|
|
@@ -4,12 +4,9 @@ export declare const zh: {
|
|
|
4
4
|
readonly subtitle: "为子代理与能力路由设置固定模型;当前会话模型用输入框旁的选择器直接切换。";
|
|
5
5
|
readonly conversationRoutes: "对话路由";
|
|
6
6
|
readonly capabilityRoutes: "能力路由";
|
|
7
|
-
readonly sendProtection: "发送保护";
|
|
8
7
|
readonly settingsSynced: "设置已同步";
|
|
9
8
|
readonly remoteSettingsUnavailable: "官方 DSH 在非本机浏览器禁用配置读取与写入;请在 Host 的 127.0.0.1 页面打开设置。";
|
|
10
9
|
readonly cancel: "取消";
|
|
11
|
-
readonly compactOnSwitch: "切换时压缩";
|
|
12
|
-
readonly compactOnSwitchHelp: "发送且换了模型时,先按目标窗口检查上下文;超了就用切换前的模型压一次历史,再继续请求。只选模型、不发送,不会压缩。官方自动压缩和 /compact 不受影响。压缩会产生额外模型用量,摘要可能省略细节。";
|
|
13
10
|
readonly provider: "提供商";
|
|
14
11
|
readonly model: "模型";
|
|
15
12
|
readonly providerDefault: "使用提供商默认值";
|
|
@@ -46,8 +43,8 @@ export declare const zh: {
|
|
|
46
43
|
readonly providerGroups: "供应商配置组";
|
|
47
44
|
readonly providerGroupsCard: "按供应商预设路由";
|
|
48
45
|
readonly providerGroupsSummary: "使用该供应商的会话自动采用对应组";
|
|
49
|
-
readonly providerGroupsHelp: "
|
|
50
|
-
readonly providerGroupsBodyHelp: "
|
|
46
|
+
readonly providerGroupsHelp: "为压缩、自动审查、会话命名与子代理按供应商分别保存路由。";
|
|
47
|
+
readonly providerGroupsBodyHelp: "每个供应商保存一组压缩、自动审查、会话命名与子代理路由。会话自身的供应商与该组一致时,这四项能力按组实时解析,未固定项回落到上方卡片里的全局设置。切换某个会话的模型不会写入任何全局设置,因此不同供应商的多个会话并行时互不影响,会话中切换供应商后下一次调用也会立即跟随新供应商的配置组。";
|
|
51
48
|
readonly providerGroupPick: "选择供应商以编辑其配置组。";
|
|
52
49
|
readonly providerGroupDelete: "删除该组";
|
|
53
50
|
readonly saveTimeout: "保存超时,请重试";
|