dsh-model-switch 0.4.25 → 0.4.27
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 +3 -3
- package/README.md +3 -3
- package/README.zh.md +2 -2
- package/cordis.patch.yml +18 -472
- package/lib/auto-review.js +1 -1
- package/lib/compaction.js +1 -1
- package/lib/index.js +150 -0
- 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/preset-sync.d.ts +64 -0
- 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/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 };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { Entry, Loader } from '@deepseek-ai/cordis-plugin-loader';
|
|
3
|
+
/**
|
|
4
|
+
* Agent presets that ship their own isolated `compaction` group.
|
|
5
|
+
*
|
|
6
|
+
* The Web surface mounts preset-nested rows below a `cordis:group`, and the
|
|
7
|
+
* Loader's patch index cannot address rows inside a preset's `config.plugins`
|
|
8
|
+
* list, so this plugin bridges those rows at runtime instead of shipping copies
|
|
9
|
+
* of the shipped preset definitions: the official (or deployment-edited) preset
|
|
10
|
+
* stays authoritative, and only the nested compaction engine is swapped.
|
|
11
|
+
*/
|
|
12
|
+
export declare const PRESET_ROW_IDS: readonly ["preset-standard", "preset-ptc", "preset-cordis"];
|
|
13
|
+
interface PresetRow {
|
|
14
|
+
readonly id?: unknown;
|
|
15
|
+
readonly name?: unknown;
|
|
16
|
+
readonly group?: unknown;
|
|
17
|
+
readonly disabled?: unknown;
|
|
18
|
+
readonly config?: unknown;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Swap the nested official compaction engine for this plugin's routed engine.
|
|
22
|
+
*
|
|
23
|
+
* Recurses through every nested entry list, so a preset that wraps its
|
|
24
|
+
* compaction group differently is still bridged. A list that carries no
|
|
25
|
+
* official engine is returned untouched. The transform is idempotent: a list
|
|
26
|
+
* whose official row is already disabled and already followed by the routed row
|
|
27
|
+
* reports `changed: false`.
|
|
28
|
+
* @param rows - one plugin list (a preset's `plugins` or a nested group's `config`).
|
|
29
|
+
* @returns The new list and whether anything changed.
|
|
30
|
+
*/
|
|
31
|
+
export declare function bridgePresetPlugins(rows: readonly PresetRow[]): {
|
|
32
|
+
rows: PresetRow[];
|
|
33
|
+
changed: boolean;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Bridge one preset definition's plugin list.
|
|
37
|
+
* @param config - the preset row's stored config (`{ id, order, plugins }`).
|
|
38
|
+
* @returns The bridged config and whether anything changed.
|
|
39
|
+
*/
|
|
40
|
+
export declare function bridgePresetConfig(config: unknown): {
|
|
41
|
+
config: unknown;
|
|
42
|
+
changed: boolean;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Locate one preset row in the loader tree.
|
|
46
|
+
* @param loader - the host loader.
|
|
47
|
+
* @param id - the preset id (`standard`, `ptc`, `cordis`).
|
|
48
|
+
* @returns The declaring entry, or undefined when this profile has no such preset.
|
|
49
|
+
*/
|
|
50
|
+
export declare function presetEntry(loader: Loader | undefined, id: string): Entry | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Bridge every shipped preset in the running composition.
|
|
53
|
+
*
|
|
54
|
+
* Runs at Host startup, before any session can use a preset, so the wrapped
|
|
55
|
+
* engine is in place for the first session. An entry that has not started yet
|
|
56
|
+
* has its raw config replaced (so the first activation already carries the
|
|
57
|
+
* bridge); a started entry is reloaded through the public loader update. A
|
|
58
|
+
* preset without a compaction group is skipped, and a bridge failure is logged
|
|
59
|
+
* without failing the row: the official presets keep working.
|
|
60
|
+
* @param ctx - the Host plugin context.
|
|
61
|
+
* @returns The preset ids that were bridged.
|
|
62
|
+
*/
|
|
63
|
+
export declare function bridgeAgentPresets(ctx: Context): Promise<readonly string[]>;
|
|
64
|
+
export {};
|