dsh-llm-fallbacks 0.2.0 → 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 -15
- package/dist/role-resolution.d.ts +81 -0
- package/dist/roles.d.ts +8 -0
- package/package.json +20 -18
- 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,6 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
4
5
|
import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
5
6
|
//#region .build/host/selectors.js
|
|
6
7
|
var SelectorError = class extends Error {
|
|
@@ -52,7 +53,8 @@ const defaultFallbacksConfig = {
|
|
|
52
53
|
revertPolicy: "cooldown-expiry",
|
|
53
54
|
maxSwitchesPerStep: 8,
|
|
54
55
|
alwaysModeRetryCap: 5,
|
|
55
|
-
presets: "bundled"
|
|
56
|
+
presets: "bundled",
|
|
57
|
+
roleAutoMatch: true
|
|
56
58
|
};
|
|
57
59
|
const INHERIT_ROLE_ID = "inherit";
|
|
58
60
|
const ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
|
|
@@ -138,8 +140,101 @@ const Config = z.object({
|
|
|
138
140
|
revertPolicy: z.union([z.const("cooldown-expiry"), z.const("never")]).default("cooldown-expiry"),
|
|
139
141
|
maxSwitchesPerStep: z.number().default(8),
|
|
140
142
|
alwaysModeRetryCap: z.number().default(5),
|
|
141
|
-
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)
|
|
142
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
|
+
}
|
|
143
238
|
//#endregion
|
|
144
239
|
//#region .build/host/chains.js
|
|
145
240
|
function resolveCandidate(entry, failing, modelExists) {
|
|
@@ -263,6 +358,29 @@ function resolveRole(agent, rules, roleIds, warn = console.warn) {
|
|
|
263
358
|
return INHERIT_ROLE_ID;
|
|
264
359
|
}
|
|
265
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
|
|
266
384
|
//#region .build/host/cooldown.js
|
|
267
385
|
var CooldownStore = class {
|
|
268
386
|
entries = new Map();
|
|
@@ -398,7 +516,8 @@ const CONFIG_KEYS = {
|
|
|
398
516
|
revertPolicy: true,
|
|
399
517
|
maxSwitchesPerStep: true,
|
|
400
518
|
alwaysModeRetryCap: true,
|
|
401
|
-
presets: true
|
|
519
|
+
presets: true,
|
|
520
|
+
roleAutoMatch: true
|
|
402
521
|
};
|
|
403
522
|
const ROLES_KEYS = {
|
|
404
523
|
list: true,
|
|
@@ -598,6 +717,7 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
598
717
|
configMaxSwitches: "单步最大切换",
|
|
599
718
|
configAlwaysCap: "always 上限",
|
|
600
719
|
configPresets: "预置",
|
|
720
|
+
configRoleAutoMatch: "角色自动匹配",
|
|
601
721
|
configEdit: "编辑:~/.dsh/profiles/<profile>/cordis.patch.yml(插件行)或 $DSH_HOME/settings.yaml(fallbacks: 分节)",
|
|
602
722
|
configEditHint: "TUI 无法修改配置——只能编辑文件"
|
|
603
723
|
},
|
|
@@ -635,6 +755,7 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
635
755
|
configMaxSwitches: "Max switches/step",
|
|
636
756
|
configAlwaysCap: "Always-mode cap",
|
|
637
757
|
configPresets: "Presets",
|
|
758
|
+
configRoleAutoMatch: "Auto-match",
|
|
638
759
|
configEdit: "Edit: ~/.dsh/profiles/<profile>/cordis.patch.yml (plugin row) or $DSH_HOME/settings.yaml (fallbacks: section)",
|
|
639
760
|
configEditHint: "TUI cannot change config — edit files only"
|
|
640
761
|
}
|
|
@@ -708,6 +829,7 @@ function fallbacksConfigText(summary, locale = "zh") {
|
|
|
708
829
|
`${t.configMaxSwitches}: ${summary.maxSwitchesPerStep}`,
|
|
709
830
|
`${t.configAlwaysCap}: ${summary.alwaysModeRetryCap}`,
|
|
710
831
|
`${t.configPresets}: ${summary.presets}`,
|
|
832
|
+
`${t.configRoleAutoMatch}: ${summary.roleAutoMatch ? t.configEnabled : t.configDisabled}`,
|
|
711
833
|
"",
|
|
712
834
|
t.configEdit,
|
|
713
835
|
t.configEditHint
|
|
@@ -1122,6 +1244,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1122
1244
|
});
|
|
1123
1245
|
const states = new FallbackStateStore();
|
|
1124
1246
|
stateStores.set(ctx, states);
|
|
1247
|
+
const dispatchInjected = new Set();
|
|
1125
1248
|
async function decide(agent, turn, step, current, reason, state) {
|
|
1126
1249
|
const config = source();
|
|
1127
1250
|
if (state !== void 0) {
|
|
@@ -1159,7 +1282,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1159
1282
|
reason
|
|
1160
1283
|
};
|
|
1161
1284
|
}
|
|
1162
|
-
function commit(
|
|
1285
|
+
function commit(state, pending, turn, step) {
|
|
1163
1286
|
const config = source();
|
|
1164
1287
|
const fromKey = selectorKey(pending.from.provider, pending.from.model);
|
|
1165
1288
|
const until = config.revertPolicy === "never" ? Number.POSITIVE_INFINITY : Date.now() + config.cooldownMs;
|
|
@@ -1168,14 +1291,6 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1168
1291
|
states.suppress(state, fromKey, until);
|
|
1169
1292
|
states.recordFailure(state, fromKey);
|
|
1170
1293
|
states.recordSwitch(state);
|
|
1171
|
-
agent.session.append("fallbacks/switch", {
|
|
1172
|
-
turn,
|
|
1173
|
-
step,
|
|
1174
|
-
from: pending.from,
|
|
1175
|
-
to: pending.to,
|
|
1176
|
-
role: pending.role,
|
|
1177
|
-
reason: pending.reason
|
|
1178
|
-
});
|
|
1179
1294
|
}
|
|
1180
1295
|
ctx.on("agent/request-error", async ({ agent, turn, step, provider, failure }, next) => {
|
|
1181
1296
|
const config = source();
|
|
@@ -1185,7 +1300,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1185
1300
|
try {
|
|
1186
1301
|
const pending = await decide(agent, turn, step, current, "trigger-code", states.peek(agent.id));
|
|
1187
1302
|
if (pending === null) return next();
|
|
1188
|
-
commit(
|
|
1303
|
+
commit(states.get(agent.id), pending, turn, step);
|
|
1189
1304
|
return { kind: "retry" };
|
|
1190
1305
|
} catch (error) {
|
|
1191
1306
|
logger.warn("llm-fallbacks: decision path failed, passing the original failure through: %s", error?.message ?? String(error));
|
|
@@ -1198,6 +1313,30 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1198
1313
|
const applied = state === void 0 ? void 0 : states.applyPending(state, turn, step);
|
|
1199
1314
|
if (applied !== void 0) return overrideConfig(seed, applied.to);
|
|
1200
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
|
+
}
|
|
1201
1340
|
if (hasChains && config.enabled && config.alwaysModeRetryCap > 0 && countRetryEvents(agent.session, turn, step, seed.provider) >= config.alwaysModeRetryCap) {
|
|
1202
1341
|
const decisionState = states.peek(agent.id);
|
|
1203
1342
|
const pending = await decide(agent, turn, step, {
|
|
@@ -1206,7 +1345,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1206
1345
|
}, "always-cap", decisionState);
|
|
1207
1346
|
if (pending !== null) {
|
|
1208
1347
|
const commitState = states.get(agent.id);
|
|
1209
|
-
commit(
|
|
1348
|
+
commit(commitState, pending, turn, step);
|
|
1210
1349
|
const appliedCap = states.applyPending(commitState, turn, step);
|
|
1211
1350
|
if (appliedCap !== void 0) return overrideConfig(seed, appliedCap.to);
|
|
1212
1351
|
}
|
|
@@ -1220,9 +1359,11 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1220
1359
|
});
|
|
1221
1360
|
ctx.on("agent/disposed", ({ agent }) => {
|
|
1222
1361
|
states.delete(agent.id);
|
|
1362
|
+
dispatchInjected.delete(agent.id);
|
|
1223
1363
|
});
|
|
1224
1364
|
ctx.effect(() => () => {
|
|
1225
1365
|
states.clear();
|
|
1366
|
+
dispatchInjected.clear();
|
|
1226
1367
|
}, "llm-fallbacks: clear per-agent state");
|
|
1227
1368
|
const fallbacksCommandController = {
|
|
1228
1369
|
getSnapshot(agent) {
|
|
@@ -1251,7 +1392,8 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1251
1392
|
revertPolicy: config.revertPolicy,
|
|
1252
1393
|
maxSwitchesPerStep: config.maxSwitchesPerStep,
|
|
1253
1394
|
alwaysModeRetryCap: config.alwaysModeRetryCap,
|
|
1254
|
-
presets: config.presets ?? "bundled"
|
|
1395
|
+
presets: config.presets ?? "bundled",
|
|
1396
|
+
roleAutoMatch: config.roleAutoMatch ?? true
|
|
1255
1397
|
};
|
|
1256
1398
|
}
|
|
1257
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,23 +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-
|
|
75
|
-
"@deepseek-ai/dsh-
|
|
76
|
-
"@deepseek-ai/dsh-typert-
|
|
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",
|
|
77
79
|
"react": "^18.2.0",
|
|
78
80
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
79
81
|
},
|