dsh-llm-fallbacks 0.2.1 → 0.2.2
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.md +17 -2
- package/README.zh-CN.md +17 -2
- package/dist/automatch.d.ts +98 -0
- package/dist/client/FallbacksCard.d.ts +4 -3
- package/dist/client/fallbacks-store.d.ts +12 -6
- package/dist/client/index.d.ts +9 -7
- package/dist/client/index.js +138 -108
- package/dist/client/locales.d.ts +24 -11
- package/dist/commands.d.ts +4 -0
- package/dist/config.d.ts +13 -0
- package/dist/events.d.ts +19 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +157 -32
- package/dist/role-resolution.d.ts +81 -0
- package/dist/roles.d.ts +8 -0
- package/package.json +20 -19
- package/scripts/repair-fallbacks-switch-logs.ts +374 -0
package/dist/events.d.ts
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `fallbacks/switch` session event vocabulary (spec §5 table; plan Task 3).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Writing is stopped (issue #52): the plugin no longer appends this event —
|
|
5
|
+
* the apply()-time event-type registration was proven ineffective (module
|
|
6
|
+
* instance mismatch), and a session containing the event refused to load
|
|
7
|
+
* after a dsh restart. The event now exists only in history: old logs are
|
|
8
|
+
* repaired by `scripts/repair-fallbacks-switch-logs.ts`, which marks legacy
|
|
9
|
+
* events ignorable so affected sessions load again.
|
|
6
10
|
* The module is type-only — the augmentation is erased at runtime; the
|
|
7
11
|
* plugin's runtime behavior lives in `src/index.ts`.
|
|
8
12
|
*
|
|
9
13
|
* @module dsh-llm-fallbacks/events
|
|
10
14
|
*/
|
|
11
|
-
/**
|
|
12
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Why a switch was decided (spec §5.1 `PendingSwitch.reason`).
|
|
17
|
+
*
|
|
18
|
+
* `role-inject` is the dispatch-time reason (plan fallbacks-role-automatch
|
|
19
|
+
* Task 4): a subagent-origin agent's first request was re-routed to its
|
|
20
|
+
* resolved role's chain head. It is ADDITIVE to the failure-time reasons —
|
|
21
|
+
* the event shape stays a superset of the pre-feature payload, and existing
|
|
22
|
+
* renderers show unknown reasons raw (AC-5). Unlike `trigger-code` /
|
|
23
|
+
* `always-cap`, a `role-inject` event is NOT a failure decision: it carries
|
|
24
|
+
* no pending switch / cooldown / failure bookkeeping (no `commit()`).
|
|
25
|
+
*/
|
|
26
|
+
export type FallbackSwitchReason = 'trigger-code' | 'always-cap' | 'role-inject';
|
|
13
27
|
/** Durable payload of one provider/model switch (spec §5 table). */
|
|
14
28
|
export interface FallbacksSwitchEventData {
|
|
15
29
|
turn: number;
|
|
@@ -30,7 +44,7 @@ export interface FallbacksSwitchEventData {
|
|
|
30
44
|
}
|
|
31
45
|
declare module '@deepseek-ai/dsh-session/types' {
|
|
32
46
|
interface SessionEventMap {
|
|
33
|
-
/**
|
|
47
|
+
/** Historical, append-only record of one provider/model switch decided by dsh-llm-fallbacks (no longer written — issue #52 stop-write). */
|
|
34
48
|
'fallbacks/switch': FallbacksSwitchEventData;
|
|
35
49
|
}
|
|
36
50
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* (**always mode included**) → `next()`; otherwise resolve role + chain,
|
|
14
14
|
* and when a candidate survives the filter (current / cooldown /
|
|
15
15
|
* step-failed / `provider/*`-missing-id) write the pending switch +
|
|
16
|
-
* cooldown + failure bookkeeping
|
|
16
|
+
* cooldown + failure bookkeeping, then return
|
|
17
17
|
* `{ kind: 'retry' }` (own recovery, no `next()`).
|
|
18
18
|
* - `agent/request` waterfall: apply a pending switch after `await next()`
|
|
19
19
|
* (provider/model override, inherited `reasoningEffort` dropped — the
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
3
|
-
import * as dshSession from "@deepseek-ai/dsh-session";
|
|
4
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
5
|
import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
6
6
|
//#region .build/host/selectors.js
|
|
7
7
|
var SelectorError = class extends Error {
|
|
@@ -53,7 +53,8 @@ const defaultFallbacksConfig = {
|
|
|
53
53
|
revertPolicy: "cooldown-expiry",
|
|
54
54
|
maxSwitchesPerStep: 8,
|
|
55
55
|
alwaysModeRetryCap: 5,
|
|
56
|
-
presets: "bundled"
|
|
56
|
+
presets: "bundled",
|
|
57
|
+
roleAutoMatch: true
|
|
57
58
|
};
|
|
58
59
|
const INHERIT_ROLE_ID = "inherit";
|
|
59
60
|
const ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
|
|
@@ -139,8 +140,101 @@ const Config = z.object({
|
|
|
139
140
|
revertPolicy: z.union([z.const("cooldown-expiry"), z.const("never")]).default("cooldown-expiry"),
|
|
140
141
|
maxSwitchesPerStep: z.number().default(8),
|
|
141
142
|
alwaysModeRetryCap: z.number().default(5),
|
|
142
|
-
presets: z.union([z.const("bundled"), z.const("none")]).default("bundled")
|
|
143
|
+
presets: z.union([z.const("bundled"), z.const("none")]).default("bundled"),
|
|
144
|
+
roleAutoMatch: z.boolean().default(true)
|
|
143
145
|
});
|
|
146
|
+
function buildAutomatchPrompt(roles, agent) {
|
|
147
|
+
const lines = [
|
|
148
|
+
"You are assigning a subagent to its best-fit role from a declared taxonomy.",
|
|
149
|
+
"",
|
|
150
|
+
"Declared roles:",
|
|
151
|
+
...roles.map((role) => `- ${role.id.trim()}: ${role.persona}`),
|
|
152
|
+
"",
|
|
153
|
+
`Agent context — origin: ${agent.session?.header?.origin ?? "root"}`
|
|
154
|
+
];
|
|
155
|
+
const preset = agent.session?.header?.agentPreset?.trim();
|
|
156
|
+
if (preset !== void 0 && preset !== "") lines.push(`agentPreset: ${preset}`);
|
|
157
|
+
lines.push("", "Reply with EXACTLY ONE of the declared role ids above, or the literal \"none\" when no role fits.", "Do not add any explanation, punctuation, or other text.");
|
|
158
|
+
return lines.join("\n");
|
|
159
|
+
}
|
|
160
|
+
function parseAutomatchAnswer(answer, roleIds) {
|
|
161
|
+
const cleaned = answer.replace(/^[^a-zA-Z0-9-]+/, "").replace(/[^a-zA-Z0-9-]+$/, "");
|
|
162
|
+
if (cleaned === "") return null;
|
|
163
|
+
const key = cleaned.toLowerCase();
|
|
164
|
+
if (key === "none") return null;
|
|
165
|
+
return roleIds.get(key) ?? null;
|
|
166
|
+
}
|
|
167
|
+
async function pickRoleByLlm(ctx, roles, agent, opts) {
|
|
168
|
+
try {
|
|
169
|
+
const list = Array.isArray(roles.list) ? roles.list : [];
|
|
170
|
+
if (list.length === 0) return null;
|
|
171
|
+
const llm = ctx.get("llm");
|
|
172
|
+
if (llm === void 0) return null;
|
|
173
|
+
const route = pickJudgmentRoute(list, agent);
|
|
174
|
+
if (route === null) return null;
|
|
175
|
+
const roleIds = new Map(list.map((role) => [role.id.trim(), role.id]));
|
|
176
|
+
const answer = await streamRoleAnswer(llm, route, buildAutomatchPrompt(list, agent), opts);
|
|
177
|
+
if (answer === null) return null;
|
|
178
|
+
return parseAutomatchAnswer(answer, roleIds);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
opts.warn(`llm-fallbacks: auto-match failed — falling back to "inherit": ${describeError(error)}`);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function pickJudgmentRoute(roles, agent) {
|
|
185
|
+
for (const role of roles) for (const entry of role.chain ?? []) try {
|
|
186
|
+
const selector = parseSelector(entry);
|
|
187
|
+
if (selector.model !== void 0) return {
|
|
188
|
+
provider: selector.provider,
|
|
189
|
+
model: selector.model
|
|
190
|
+
};
|
|
191
|
+
} catch {}
|
|
192
|
+
const provider = agent.options?.provider;
|
|
193
|
+
const model = agent.options?.model;
|
|
194
|
+
if (provider !== void 0 && model !== void 0) return {
|
|
195
|
+
provider,
|
|
196
|
+
model
|
|
197
|
+
};
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
async function streamRoleAnswer(llm, route, prompt, opts) {
|
|
201
|
+
const controller = new AbortController();
|
|
202
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 5e3);
|
|
203
|
+
let text = "";
|
|
204
|
+
let finishKind;
|
|
205
|
+
try {
|
|
206
|
+
for await (const chunk of llm.stream({
|
|
207
|
+
provider: route.provider,
|
|
208
|
+
model: route.model,
|
|
209
|
+
system: prompt,
|
|
210
|
+
messages: [createUserMessage({
|
|
211
|
+
content: [{
|
|
212
|
+
type: "text",
|
|
213
|
+
text: "Pick the best-fit role for this agent."
|
|
214
|
+
}],
|
|
215
|
+
source: { kind: "user" }
|
|
216
|
+
})],
|
|
217
|
+
maxTokens: 32,
|
|
218
|
+
signal: controller.signal
|
|
219
|
+
})) if (chunk.type === "text-delta") text += chunk.text;
|
|
220
|
+
else if (chunk.type === "finish") {
|
|
221
|
+
finishKind = chunk.reason.kind;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
if (controller.signal.aborted) return null;
|
|
225
|
+
if (finishKind === "error" || finishKind === "aborted") return null;
|
|
226
|
+
return text;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (controller.signal.aborted) opts.warn("llm-fallbacks: auto-match timed out — falling back to \"inherit\"");
|
|
229
|
+
else opts.warn(`llm-fallbacks: auto-match call failed — falling back to "inherit": ${describeError(error)}`);
|
|
230
|
+
return null;
|
|
231
|
+
} finally {
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function describeError(error) {
|
|
236
|
+
return error instanceof Error ? error.message : String(error);
|
|
237
|
+
}
|
|
144
238
|
//#endregion
|
|
145
239
|
//#region .build/host/chains.js
|
|
146
240
|
function resolveCandidate(entry, failing, modelExists) {
|
|
@@ -264,6 +358,29 @@ function resolveRole(agent, rules, roleIds, warn = console.warn) {
|
|
|
264
358
|
return INHERIT_ROLE_ID;
|
|
265
359
|
}
|
|
266
360
|
//#endregion
|
|
361
|
+
//#region .build/host/role-resolution.js
|
|
362
|
+
async function resolveRoleAtDispatch(agent, rules, roleIds, opts) {
|
|
363
|
+
const preset = agent.session?.header?.agentPreset?.trim();
|
|
364
|
+
if (preset !== void 0 && preset !== "" && preset !== "inherit") {
|
|
365
|
+
const declared = roleIds.get(preset);
|
|
366
|
+
if (declared !== void 0) return declared;
|
|
367
|
+
}
|
|
368
|
+
const role = resolveRole(agent, rules, roleIds, opts.warn);
|
|
369
|
+
if (role !== "inherit") return role;
|
|
370
|
+
if (opts.automatchEnabled && opts.automatch !== void 0) {
|
|
371
|
+
const picked = await opts.automatch(agent);
|
|
372
|
+
if (picked !== null && picked !== void 0) {
|
|
373
|
+
const declared = roleIds.get(picked.trim());
|
|
374
|
+
if (declared !== void 0) return declared;
|
|
375
|
+
opts.warn(`llm-fallbacks: auto-match returned undeclared role "${picked}" — falling back to "inherit"`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return INHERIT_ROLE_ID;
|
|
379
|
+
}
|
|
380
|
+
function firstExactCandidate(all, wildcard) {
|
|
381
|
+
for (let index = 0; index < all.length; index += 1) if (!wildcard[index]) return all[index];
|
|
382
|
+
}
|
|
383
|
+
//#endregion
|
|
267
384
|
//#region .build/host/cooldown.js
|
|
268
385
|
var CooldownStore = class {
|
|
269
386
|
entries = new Map();
|
|
@@ -399,7 +516,8 @@ const CONFIG_KEYS = {
|
|
|
399
516
|
revertPolicy: true,
|
|
400
517
|
maxSwitchesPerStep: true,
|
|
401
518
|
alwaysModeRetryCap: true,
|
|
402
|
-
presets: true
|
|
519
|
+
presets: true,
|
|
520
|
+
roleAutoMatch: true
|
|
403
521
|
};
|
|
404
522
|
const ROLES_KEYS = {
|
|
405
523
|
list: true,
|
|
@@ -599,6 +717,7 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
599
717
|
configMaxSwitches: "单步最大切换",
|
|
600
718
|
configAlwaysCap: "always 上限",
|
|
601
719
|
configPresets: "预置",
|
|
720
|
+
configRoleAutoMatch: "角色自动匹配",
|
|
602
721
|
configEdit: "编辑:~/.dsh/profiles/<profile>/cordis.patch.yml(插件行)或 $DSH_HOME/settings.yaml(fallbacks: 分节)",
|
|
603
722
|
configEditHint: "TUI 无法修改配置——只能编辑文件"
|
|
604
723
|
},
|
|
@@ -636,6 +755,7 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
636
755
|
configMaxSwitches: "Max switches/step",
|
|
637
756
|
configAlwaysCap: "Always-mode cap",
|
|
638
757
|
configPresets: "Presets",
|
|
758
|
+
configRoleAutoMatch: "Auto-match",
|
|
639
759
|
configEdit: "Edit: ~/.dsh/profiles/<profile>/cordis.patch.yml (plugin row) or $DSH_HOME/settings.yaml (fallbacks: section)",
|
|
640
760
|
configEditHint: "TUI cannot change config — edit files only"
|
|
641
761
|
}
|
|
@@ -709,6 +829,7 @@ function fallbacksConfigText(summary, locale = "zh") {
|
|
|
709
829
|
`${t.configMaxSwitches}: ${summary.maxSwitchesPerStep}`,
|
|
710
830
|
`${t.configAlwaysCap}: ${summary.alwaysModeRetryCap}`,
|
|
711
831
|
`${t.configPresets}: ${summary.presets}`,
|
|
832
|
+
`${t.configRoleAutoMatch}: ${summary.roleAutoMatch ? t.configEnabled : t.configDisabled}`,
|
|
712
833
|
"",
|
|
713
834
|
t.configEdit,
|
|
714
835
|
t.configEditHint
|
|
@@ -1052,15 +1173,6 @@ function overrideConfig(seed, to) {
|
|
|
1052
1173
|
}
|
|
1053
1174
|
function apply(ctx, config = defaultFallbacksConfig) {
|
|
1054
1175
|
const logger = ctx.logger("llm-fallbacks");
|
|
1055
|
-
const known = dshSession.KNOWN_SESSION_EVENT_TYPES;
|
|
1056
|
-
let sessionEventRegistered = false;
|
|
1057
|
-
if (known instanceof Set) try {
|
|
1058
|
-
known.add("fallbacks/switch");
|
|
1059
|
-
sessionEventRegistered = known.has("fallbacks/switch");
|
|
1060
|
-
} catch {
|
|
1061
|
-
sessionEventRegistered = false;
|
|
1062
|
-
}
|
|
1063
|
-
let sessionEventWarned = false;
|
|
1064
1176
|
const entry = Config(config);
|
|
1065
1177
|
const seeds = new FallbacksSeedManager(logger);
|
|
1066
1178
|
const seedsSettingsUnavailable = "llm-fallbacks: seeds: settings service is unavailable — seed roles cannot be written";
|
|
@@ -1132,6 +1244,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1132
1244
|
});
|
|
1133
1245
|
const states = new FallbackStateStore();
|
|
1134
1246
|
stateStores.set(ctx, states);
|
|
1247
|
+
const dispatchInjected = new Set();
|
|
1135
1248
|
async function decide(agent, turn, step, current, reason, state) {
|
|
1136
1249
|
const config = source();
|
|
1137
1250
|
if (state !== void 0) {
|
|
@@ -1169,7 +1282,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1169
1282
|
reason
|
|
1170
1283
|
};
|
|
1171
1284
|
}
|
|
1172
|
-
function commit(
|
|
1285
|
+
function commit(state, pending, turn, step) {
|
|
1173
1286
|
const config = source();
|
|
1174
1287
|
const fromKey = selectorKey(pending.from.provider, pending.from.model);
|
|
1175
1288
|
const until = config.revertPolicy === "never" ? Number.POSITIVE_INFINITY : Date.now() + config.cooldownMs;
|
|
@@ -1178,21 +1291,6 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1178
1291
|
states.suppress(state, fromKey, until);
|
|
1179
1292
|
states.recordFailure(state, fromKey);
|
|
1180
1293
|
states.recordSwitch(state);
|
|
1181
|
-
if (!sessionEventRegistered) {
|
|
1182
|
-
if (!sessionEventWarned) {
|
|
1183
|
-
sessionEventWarned = true;
|
|
1184
|
-
logger.warn("llm-fallbacks: session event type \"fallbacks/switch\" is not registered with this harness — skipping the durable event (the switch itself is applied)");
|
|
1185
|
-
}
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
agent.session.append("fallbacks/switch", {
|
|
1189
|
-
turn,
|
|
1190
|
-
step,
|
|
1191
|
-
from: pending.from,
|
|
1192
|
-
to: pending.to,
|
|
1193
|
-
role: pending.role,
|
|
1194
|
-
reason: pending.reason
|
|
1195
|
-
});
|
|
1196
1294
|
}
|
|
1197
1295
|
ctx.on("agent/request-error", async ({ agent, turn, step, provider, failure }, next) => {
|
|
1198
1296
|
const config = source();
|
|
@@ -1202,7 +1300,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1202
1300
|
try {
|
|
1203
1301
|
const pending = await decide(agent, turn, step, current, "trigger-code", states.peek(agent.id));
|
|
1204
1302
|
if (pending === null) return next();
|
|
1205
|
-
commit(
|
|
1303
|
+
commit(states.get(agent.id), pending, turn, step);
|
|
1206
1304
|
return { kind: "retry" };
|
|
1207
1305
|
} catch (error) {
|
|
1208
1306
|
logger.warn("llm-fallbacks: decision path failed, passing the original failure through: %s", error?.message ?? String(error));
|
|
@@ -1215,6 +1313,30 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1215
1313
|
const applied = state === void 0 ? void 0 : states.applyPending(state, turn, step);
|
|
1216
1314
|
if (applied !== void 0) return overrideConfig(seed, applied.to);
|
|
1217
1315
|
const config = source();
|
|
1316
|
+
if (config.enabled && hasChains && agent.session?.header?.origin === "subagent" && !dispatchInjected.has(agent.id)) {
|
|
1317
|
+
dispatchInjected.add(agent.id);
|
|
1318
|
+
try {
|
|
1319
|
+
const role = await resolveRoleAtDispatch(agent, config.roles.rules, roleIds, {
|
|
1320
|
+
automatchEnabled: config.roleAutoMatch ?? true,
|
|
1321
|
+
automatch: (agent) => pickRoleByLlm(ctx, config.roles, agent, { warn: logger.warn }),
|
|
1322
|
+
warn: logger.warn
|
|
1323
|
+
});
|
|
1324
|
+
if (role !== "inherit") {
|
|
1325
|
+
const { all, wildcard } = resolveChainViews(config.roles.list, config.rootChain, role, seed.provider, seed.model, logger.warn);
|
|
1326
|
+
const head = firstExactCandidate(all, wildcard);
|
|
1327
|
+
if (head !== void 0 && head.model !== void 0 && !(head.provider === seed.provider && head.model === seed.model)) {
|
|
1328
|
+
const to = {
|
|
1329
|
+
provider: head.provider,
|
|
1330
|
+
model: head.model
|
|
1331
|
+
};
|
|
1332
|
+
logger.info("llm-fallbacks: agent \"%s\" role-inject role=%s model=%s/%s", agent.id, role, head.provider, head.model);
|
|
1333
|
+
return overrideConfig(seed, to);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
logger.warn("llm-fallbacks: dispatch role-injection failed, proceeding with the request unchanged: %s", error?.message ?? String(error));
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1218
1340
|
if (hasChains && config.enabled && config.alwaysModeRetryCap > 0 && countRetryEvents(agent.session, turn, step, seed.provider) >= config.alwaysModeRetryCap) {
|
|
1219
1341
|
const decisionState = states.peek(agent.id);
|
|
1220
1342
|
const pending = await decide(agent, turn, step, {
|
|
@@ -1223,7 +1345,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1223
1345
|
}, "always-cap", decisionState);
|
|
1224
1346
|
if (pending !== null) {
|
|
1225
1347
|
const commitState = states.get(agent.id);
|
|
1226
|
-
commit(
|
|
1348
|
+
commit(commitState, pending, turn, step);
|
|
1227
1349
|
const appliedCap = states.applyPending(commitState, turn, step);
|
|
1228
1350
|
if (appliedCap !== void 0) return overrideConfig(seed, appliedCap.to);
|
|
1229
1351
|
}
|
|
@@ -1237,9 +1359,11 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1237
1359
|
});
|
|
1238
1360
|
ctx.on("agent/disposed", ({ agent }) => {
|
|
1239
1361
|
states.delete(agent.id);
|
|
1362
|
+
dispatchInjected.delete(agent.id);
|
|
1240
1363
|
});
|
|
1241
1364
|
ctx.effect(() => () => {
|
|
1242
1365
|
states.clear();
|
|
1366
|
+
dispatchInjected.clear();
|
|
1243
1367
|
}, "llm-fallbacks: clear per-agent state");
|
|
1244
1368
|
const fallbacksCommandController = {
|
|
1245
1369
|
getSnapshot(agent) {
|
|
@@ -1268,7 +1392,8 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1268
1392
|
revertPolicy: config.revertPolicy,
|
|
1269
1393
|
maxSwitchesPerStep: config.maxSwitchesPerStep,
|
|
1270
1394
|
alwaysModeRetryCap: config.alwaysModeRetryCap,
|
|
1271
|
-
presets: config.presets ?? "bundled"
|
|
1395
|
+
presets: config.presets ?? "bundled",
|
|
1396
|
+
roleAutoMatch: config.roleAutoMatch ?? true
|
|
1272
1397
|
};
|
|
1273
1398
|
}
|
|
1274
1399
|
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatch-time three-stage role resolution (plan fallbacks-role-automatch
|
|
3
|
+
* Task 2).
|
|
4
|
+
*
|
|
5
|
+
* Precedence (first hit wins):
|
|
6
|
+
* 1. explicit — `session.header.agentPreset` trimmed matches a declared role
|
|
7
|
+
* id → that role's DECLARED RAW id (via the `roleIds` trimmed-id map, the
|
|
8
|
+
* same canonicalization the rules path uses); the reserved `'inherit'`
|
|
9
|
+
* and undeclared presets are NEVER dispatchable explicit roles and fall
|
|
10
|
+
* through to rules;
|
|
11
|
+
* 2. rules — the existing `resolveRole` passthrough, unchanged (a declared
|
|
12
|
+
* role wins; no-rule-match / an explicit rule targeting `'inherit'`
|
|
13
|
+
* resolve to `'inherit'`);
|
|
14
|
+
* 3. auto-match — only when stage 2 resolved to `'inherit'` AND
|
|
15
|
+
* `opts.automatchEnabled`: call `opts.automatch` with the agent. The hook
|
|
16
|
+
* (Task 3 supplies the bounded LLM caller) MUST return a declared raw id
|
|
17
|
+
* or `null`; the resolver defensively validates the return against
|
|
18
|
+
* `roleIds` — an unknown id warns and resolves to `'inherit'`, never an
|
|
19
|
+
* undeclared role. When `automatch` is undefined or `automatchEnabled`
|
|
20
|
+
* is false the hook is skipped.
|
|
21
|
+
*
|
|
22
|
+
* `'inherit'` is the single "no specific role" outcome — reached by
|
|
23
|
+
* no-rule-match OR an explicit rule targeting `'inherit'` — and both are
|
|
24
|
+
* auto-match eligible when enabled. An operator who wants a class to never
|
|
25
|
+
* auto-match sets `roleAutoMatch: false` (→ `automatchEnabled: false`).
|
|
26
|
+
*
|
|
27
|
+
* Pure ordering logic: no config object and no LLM service inside this
|
|
28
|
+
* module — the LLM is an injectable hook owned by Task 3/4.
|
|
29
|
+
*
|
|
30
|
+
* @module dsh-llm-fallbacks/role-resolution
|
|
31
|
+
*/
|
|
32
|
+
import { type FallbacksRoleRule } from './config.ts';
|
|
33
|
+
import { type AgentLike } from './roles.ts';
|
|
34
|
+
import type { Selector } from './selectors.ts';
|
|
35
|
+
export type { FallbacksRoleRule } from './config.ts';
|
|
36
|
+
/** Options for {@link resolveRoleAtDispatch}. */
|
|
37
|
+
export interface ResolveRoleAtDispatchOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Whether the auto-match stage may run (the `roleAutoMatch` config switch).
|
|
40
|
+
* When `false`, stage 3 never runs and the outcome is identical to the
|
|
41
|
+
* rules stage alone.
|
|
42
|
+
*/
|
|
43
|
+
automatchEnabled: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Auto-match hook — Task 3 supplies the bounded LLM caller; the resolver
|
|
46
|
+
* itself contains no LLM. Contract: given the agent being resolved, return
|
|
47
|
+
* a declared raw role id or `null`. The resolver defensively validates the
|
|
48
|
+
* return against `roleIds` — an unknown id warns and resolves to
|
|
49
|
+
* `'inherit'`, never an undeclared role. Skipped when undefined.
|
|
50
|
+
*/
|
|
51
|
+
automatch?: (agent: AgentLike) => Promise<string | null>;
|
|
52
|
+
/** Warning sink — the decision path injects the plugin logger. */
|
|
53
|
+
warn: (message: string) => void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the dispatch-time role for an agent across three ordered stages
|
|
57
|
+
* (explicit → rules → auto-match hook). Never throws on its own inputs; the
|
|
58
|
+
* hook's own rejection is deliberately NOT caught here (the Task-3/4 wiring
|
|
59
|
+
* bounds the LLM call so it never throws out of the request path).
|
|
60
|
+
*
|
|
61
|
+
* `roleIds` is the canonical trimmed-id map (`trimmed id → declared raw id`,
|
|
62
|
+
* built in `src/index.ts` apply()): every stage canonicalizes its reference
|
|
63
|
+
* by trim and returns the DECLARED RAW id, so roleDef lookups in
|
|
64
|
+
* `chains.ts` / `commands.ts` find the stored `roles.list[]` entry exactly
|
|
65
|
+
* (qc2 F-001 — no silent role inertness on padded YAML ids).
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveRoleAtDispatch(agent: AgentLike, rules: FallbacksRoleRule[], roleIds: ReadonlyMap<string, string>, opts: ResolveRoleAtDispatchOptions): Promise<string>;
|
|
68
|
+
/**
|
|
69
|
+
* The dispatch-time chain head (plan fallbacks-role-automatch Task 4): the
|
|
70
|
+
* FIRST candidate of a `resolveChainViews` pass whose `wildcard` flag is
|
|
71
|
+
* false — an exact `provider/model` entry. `provider/*` entries are NOT
|
|
72
|
+
* dispatch injection targets (no failing model to anchor them; a guessed
|
|
73
|
+
* injection risks a hard failure whose code may sit outside `triggerCodes`),
|
|
74
|
+
* so a wildcard-only chain yields `undefined` → no injection (today's
|
|
75
|
+
* behavior). No cooldown/failed filtering and no existence probe run here —
|
|
76
|
+
* a fresh subagent's first request has no failure-scoped state (see the Task
|
|
77
|
+
* 4 decision) and exact heads are never probed. Pairs with
|
|
78
|
+
* {@link resolveChainViews}'s parallel `wildcard` provenance (`src/chains.ts`);
|
|
79
|
+
* the caller resolves the role's concatenated chain first.
|
|
80
|
+
*/
|
|
81
|
+
export declare function firstExactCandidate(all: readonly Selector[], wildcard: readonly boolean[]): Selector | undefined;
|
package/dist/roles.d.ts
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* Role resolution for fallback chains (spec §7.1; plan fallbacks-role-runtime
|
|
3
3
|
* Task 1).
|
|
4
4
|
*
|
|
5
|
+
* This module is the RULES stage. Dispatch-time role resolution
|
|
6
|
+
* (plan fallbacks-role-automatch) is the three-stage resolver in
|
|
7
|
+
* `role-resolution.ts` — explicit (`session.header.agentPreset`) → this
|
|
8
|
+
* rules stage → an optional auto-match hook; `AgentLike` carries the additive
|
|
9
|
+
* `session.header.agentPreset?` carrier for that explicit stage.
|
|
10
|
+
*
|
|
5
11
|
* Precedence (first hit wins):
|
|
6
12
|
* 1. the first `rules` entry whose specified origin/provider/model patterns
|
|
7
13
|
* all match the agent;
|
|
@@ -47,6 +53,8 @@ export interface AgentLike {
|
|
|
47
53
|
session?: {
|
|
48
54
|
header?: {
|
|
49
55
|
origin?: Origin;
|
|
56
|
+
/** Dispatch-time explicit role carrier (plan fallbacks-role-automatch Task 2). */
|
|
57
|
+
agentPreset?: string;
|
|
50
58
|
};
|
|
51
59
|
};
|
|
52
60
|
}
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"dsh",
|
|
6
6
|
"dsh-plugin"
|
|
7
7
|
],
|
|
8
|
-
"version": "0.2.
|
|
8
|
+
"version": "0.2.2",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"main": "./dist/index.js",
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"build-client": "tsx scripts/build-client.ts",
|
|
49
49
|
"release:prepare": "tsx scripts/prepare-release.ts",
|
|
50
50
|
"release:validate": "tsx scripts/validate-release-version.ts",
|
|
51
|
+
"repair:fallbacks-switch-logs": "tsx scripts/repair-fallbacks-switch-logs.ts",
|
|
51
52
|
"test": "vitest run",
|
|
52
53
|
"typecheck": "tsc -p tsconfig.build.json --noEmit && tsc --noEmit",
|
|
53
54
|
"prepare": "pnpm run build"
|
|
@@ -57,24 +58,24 @@
|
|
|
57
58
|
},
|
|
58
59
|
"peerDependencies": {
|
|
59
60
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
60
|
-
"@deepseek-ai/dsh-agent": "^0.1.0-rc.
|
|
61
|
-
"@deepseek-ai/dsh-api-gateway": "^0.1.0-rc.
|
|
62
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.
|
|
63
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.
|
|
64
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.
|
|
65
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.
|
|
66
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.0-rc.
|
|
67
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.
|
|
68
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.
|
|
69
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.
|
|
70
|
-
"@deepseek-ai/dsh-client-web-react": "^0.1.0-rc.
|
|
71
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.
|
|
72
|
-
"@deepseek-ai/dsh-commands": "^0.1.0-rc.
|
|
73
|
-
"@deepseek-ai/dsh-llm": "^0.1.0-rc.
|
|
74
|
-
"@deepseek-ai/dsh-session": "^0.1.0-rc.
|
|
75
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.
|
|
76
|
-
"@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.
|
|
77
|
-
"@deepseek-ai/dsh-typert-registry": "^0.1.0-rc.
|
|
61
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.7",
|
|
62
|
+
"@deepseek-ai/dsh-api-gateway": "^0.1.0-rc.7",
|
|
63
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.7",
|
|
64
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
|
|
65
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
|
|
66
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.0-rc.7",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.7",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
|
|
71
|
+
"@deepseek-ai/dsh-client-web-react": "^0.1.0-rc.7",
|
|
72
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
|
|
73
|
+
"@deepseek-ai/dsh-commands": "^0.1.0-rc.7",
|
|
74
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.7",
|
|
75
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.7",
|
|
76
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
|
|
77
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.7",
|
|
78
|
+
"@deepseek-ai/dsh-typert-registry": "^0.1.0-rc.7",
|
|
78
79
|
"react": "^18.2.0",
|
|
79
80
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
80
81
|
},
|