dsh-auto-model-routing 0.5.2 → 0.5.3
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 +4 -0
- package/README.md +3 -3
- package/README.zh.md +3 -3
- package/docs/SCREENSHOTS.md +2 -2
- package/docs/auto-routing.png +0 -0
- package/docs/settings.png +0 -0
- package/lib/client/index.d.ts +7 -1
- package/lib/client/index.js +47 -8
- package/lib/client.js +60 -40
- package/lib/index.js +56 -43
- package/lib/policy-host.d.ts +13 -2
- package/lib/policy-host.js +105 -85
- package/lib/policy-store.d.ts +14 -10
- package/lib/policy-store.js +27 -4
- package/lib/routing.d.ts +33 -9
- package/lib/routing.js +39 -28
- package/lib/settings-host.d.ts +2 -3
- package/lib/settings-host.js +19 -14
- package/lib/strategies/claude.d.ts +0 -5
- package/lib/strategies/claude.js +0 -1
- package/lib/strategies/codex.d.ts +0 -5
- package/lib/strategies/codex.js +0 -1
- package/lib/strategies/index.d.ts +2 -6
- package/lib/strategies/index.js +16 -23
- package/lib/strategies/types.d.ts +1 -2
- package/lib/subagent-tool.d.ts +13 -1
- package/lib/subagent-tool.js +22 -19
- package/package.json +1 -1
- package/screenshots.json +2 -2
- package/skills/subagent-model-routing/SKILL.md +5 -5
- package/docs/auto-routing.gif +0 -0
- package/docs/settings.gif +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.3
|
|
4
|
+
|
|
5
|
+
- Replace both README demo GIFs with representative PNG screenshots (an applied AUTO decision with its distribution; decision service settings with a connection test), because the DSH plugin store does not show GIFs by default. No runtime behavior changes.
|
|
6
|
+
|
|
3
7
|
## 0.5.2
|
|
4
8
|
|
|
5
9
|
- README follows the standard plugin layout: demos side by side, then the architecture diagram, then the introduction; a closing section links the ohmydsh source, the MIT license and the issue tracker.
|
package/README.md
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
English · [简体中文](README.zh.md)
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<img src="docs/auto-routing.
|
|
7
|
-
<img src="docs/settings.
|
|
6
|
+
<img src="docs/auto-routing.png" alt="AUTO applied a higher effort to the main session; the panel shows the returned decision distribution" width="49%">
|
|
7
|
+
<img src="docs/settings.png" alt="Settings: decision service, compatible endpoint and a successful connection test" width="49%">
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
|
-
*Left: AUTO
|
|
10
|
+
*Left: an applied AUTO decision with its effort distribution. Right: decision service settings with a successful connection test. Captured from an isolated DSH host with local mock Jev/model responses and synthetic tasks.*
|
|
11
11
|
|
|
12
12
|

|
|
13
13
|
|
package/README.zh.md
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
[English](README.md) · 简体中文
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<img src="docs/auto-routing.
|
|
7
|
-
<img src="docs/settings.
|
|
6
|
+
<img src="docs/auto-routing.png" alt="AUTO 已为主会话应用更高推理投入,面板显示返回的决策分布" width="49%">
|
|
7
|
+
<img src="docs/settings.png" alt="设置:决策服务、兼容端点与连接测试成功" width="49%">
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
*左:已应用的 AUTO 决策与投入分布;右:决策服务设置与连接测试成功。截图来自隔离 DSH 宿主,使用本地模拟 Jev/模型响应与合成任务。*
|
|
11
11
|
|
|
12
12
|

|
|
13
13
|
|
package/docs/SCREENSHOTS.md
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
|
|
3
3
|
| file | kind | source | date | reviewer | verdict | note |
|
|
4
4
|
| --- | --- | --- | --- | --- | --- | --- |
|
|
5
|
-
| auto-routing.
|
|
6
|
-
| settings.
|
|
5
|
+
| auto-routing.png | screenshot | frame 5 of the 2026-10-28 recording (isolated DSH_HOME under /tmp, port 3199; current plugin UI with local loopback mock Jev and model streaming responses; Playwright/CDP lossless 2x screenshots), exported losslessly from the global-palette GIF | 2026-10-29 | lead | pass (reviewed frame: synthetic task/reply and decision panel; no real credentials or user data) | 1840x1600, 245191 bytes; applied same-model medium → high decision with the returned effort distribution; replaces auto-routing.gif because the plugin store does not show GIFs by default |
|
|
6
|
+
| settings.png | screenshot | frame 3 of the same 2026-10-28 recording (lossless 2x screenshots box-filtered to 1137x1070), exported losslessly from the GIF | 2026-10-29 | lead | pass (reviewed frame: settings dialog, empty key input, synthetic loopback endpoint; no real credentials) | 198719 bytes; decision service, decision model, compatible endpoint and a successful mock connection test; replaces settings.gif |
|
|
Binary file
|
|
Binary file
|
package/lib/client/index.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ interface AutoSupport {
|
|
|
7
7
|
model?: string;
|
|
8
8
|
}
|
|
9
9
|
interface View {
|
|
10
|
+
childNotice?: {
|
|
11
|
+
id: number;
|
|
12
|
+
childId: string;
|
|
13
|
+
reason: string;
|
|
14
|
+
at: number;
|
|
15
|
+
};
|
|
10
16
|
mode: 'AUTO' | 'MANUAL';
|
|
11
17
|
depth?: string;
|
|
12
18
|
effort?: string;
|
|
@@ -38,6 +44,6 @@ export declare function RoutingChip({ sessionId, rpc, initialView, initiallyOpen
|
|
|
38
44
|
onMouseLeave: () => void;
|
|
39
45
|
onFocus: () => void;
|
|
40
46
|
onBlur: () => void;
|
|
41
|
-
}, HTMLSpanElement
|
|
47
|
+
}, HTMLSpanElement> | null;
|
|
42
48
|
export declare function apply(ctx: Context): void;
|
|
43
49
|
export {};
|
package/lib/client/index.js
CHANGED
|
@@ -39,11 +39,13 @@ const CSS = `
|
|
|
39
39
|
function SwapIcon() {
|
|
40
40
|
return h('svg', { className: 'amr-icon amr-swap', width: 14, height: 14, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true }, h('path', { d: 'M3 5.5h9.5M10 3l2.5 2.5L10 8' }), h('path', { d: 'M13 10.5H3.5M6 8l-2.5 2.5L6 13' }));
|
|
41
41
|
}
|
|
42
|
+
const noticeText = (notice) => `最近子任务未路由:${notice.reason}(按 DSH 默认运行)`;
|
|
42
43
|
export function RoutingChip({ sessionId, rpc, initialView, initiallyOpen = false, initialToast, subscribeSelection }) {
|
|
43
44
|
const [view, setView] = useState(initialView);
|
|
44
45
|
const [open, setOpen] = useState(initiallyOpen);
|
|
45
46
|
const [busy, setBusy] = useState(false);
|
|
46
47
|
const [error, setError] = useState('');
|
|
48
|
+
const [notApplicable, setNotApplicable] = useState(false);
|
|
47
49
|
const timer = useRef(undefined);
|
|
48
50
|
const [toast, setToast] = useState(initialToast);
|
|
49
51
|
// The composer hides the right-side controls while the agent runs (when Jev decides), so the hint is
|
|
@@ -79,6 +81,25 @@ export function RoutingChip({ sessionId, rpc, initialView, initiallyOpen = false
|
|
|
79
81
|
remember();
|
|
80
82
|
setToast(next);
|
|
81
83
|
}, [view, sessionId]);
|
|
84
|
+
const seenNotice = useRef(undefined);
|
|
85
|
+
const noticePrimed = useRef(false);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!view || viewSession.current !== sessionId)
|
|
88
|
+
return;
|
|
89
|
+
const notice = view.childNotice;
|
|
90
|
+
if (!noticePrimed.current) {
|
|
91
|
+
noticePrimed.current = true;
|
|
92
|
+
seenNotice.current = notice?.id;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!notice || notice.id === seenNotice.current)
|
|
96
|
+
return;
|
|
97
|
+
seenNotice.current = notice.id;
|
|
98
|
+
if (!view.decisionToast)
|
|
99
|
+
return;
|
|
100
|
+
remember();
|
|
101
|
+
setToast({ id: -notice.id, lines: [noticeText(notice)] });
|
|
102
|
+
}, [view, sessionId]);
|
|
82
103
|
// Hiding depends only on the toast shown, so later state changes cannot cancel it.
|
|
83
104
|
useEffect(() => {
|
|
84
105
|
if (!toast)
|
|
@@ -111,19 +132,34 @@ export function RoutingChip({ sessionId, rpc, initialView, initiallyOpen = false
|
|
|
111
132
|
useEffect(() => {
|
|
112
133
|
let active = true;
|
|
113
134
|
primed.current = false;
|
|
135
|
+
noticePrimed.current = false;
|
|
114
136
|
seen.current = undefined;
|
|
115
137
|
viewSession.current = undefined;
|
|
116
138
|
setToast(undefined);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
139
|
+
let poll;
|
|
140
|
+
const refresh = () => {
|
|
141
|
+
void callRouting(rpc, sessionId).then(value => {
|
|
142
|
+
if (!active)
|
|
143
|
+
return;
|
|
144
|
+
// Child sessions have no AUTO chip; their origin never changes, so stop polling for good.
|
|
145
|
+
if (value?.applicable === false) {
|
|
146
|
+
setNotApplicable(true);
|
|
147
|
+
clearInterval(poll);
|
|
148
|
+
subscription.current?.stop();
|
|
149
|
+
subscription.current = undefined;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
viewSession.current = sessionId;
|
|
153
|
+
setView(value);
|
|
154
|
+
setError('');
|
|
155
|
+
ensureSubscription();
|
|
156
|
+
}, reason => { if (active)
|
|
157
|
+
setError(String(reason)); });
|
|
158
|
+
};
|
|
124
159
|
refreshRef.current = refresh;
|
|
160
|
+
setNotApplicable(false);
|
|
125
161
|
refresh();
|
|
126
|
-
|
|
162
|
+
poll = setInterval(refresh, 3000);
|
|
127
163
|
return () => {
|
|
128
164
|
active = false;
|
|
129
165
|
clearInterval(poll);
|
|
@@ -180,8 +216,11 @@ export function RoutingChip({ sessionId, rpc, initialView, initiallyOpen = false
|
|
|
180
216
|
h('p', { key: 'depth', className: 'amr-muted' }, `AUTO · ${view?.depth ?? '尚无判断'};主模型不变,推理投入按请求应用;子任务自动选配。`),
|
|
181
217
|
off && h('p', { key: 'disabled', role: 'status', className: 'amr-error' }, `Jev 停用(${off.category}):${hint}。${new Date(off.at).toLocaleString()}。点击 AUTO 重试。`),
|
|
182
218
|
h('p', { key: 'data', className: 'amr-muted' }, 'AUTO 会把脱敏任务文本发送给设置中所选的决策服务判断。'),
|
|
219
|
+
view?.childNotice && h('p', { key: 'notice', role: 'status', className: 'amr-muted' }, noticeText(view.childNotice)),
|
|
183
220
|
error && h('p', { key: 'error', role: 'alert', className: 'amr-error' }, error),
|
|
184
221
|
];
|
|
222
|
+
if (notApplicable)
|
|
223
|
+
return null;
|
|
185
224
|
return h('span', { ref: (el) => { rootRef.current = el; remember(); }, className: 'amr-root', onMouseEnter: on('enter'), onMouseLeave: on('leave'), onFocus: on('focus'), onBlur: on('blur') }, h('style', null, CSS), h('button', {
|
|
186
225
|
type: 'button', className: 'amr-trigger', disabled: busy || !view, onClick: () => { if (locked)
|
|
187
226
|
return; void toggle(); },
|
package/lib/client.js
CHANGED
|
@@ -312,11 +312,6 @@ window.__ModuleLoader__.load({
|
|
|
312
312
|
id: "claude",
|
|
313
313
|
label: "Claude",
|
|
314
314
|
modelPrefixes: ["claude-"],
|
|
315
|
-
transports: {
|
|
316
|
-
claude: "",
|
|
317
|
-
"dsh-magpie-connect": "claude/",
|
|
318
|
-
"dsh-magpie-connector": "claude/"
|
|
319
|
-
},
|
|
320
315
|
effortStarts: {
|
|
321
316
|
"claude-opus-5-5": "medium",
|
|
322
317
|
"claude-sonnet-5-5": "medium",
|
|
@@ -335,11 +330,6 @@ window.__ModuleLoader__.load({
|
|
|
335
330
|
id: "codex",
|
|
336
331
|
label: "GPT",
|
|
337
332
|
modelPrefixes: ["gpt-"],
|
|
338
|
-
transports: {
|
|
339
|
-
codex: "",
|
|
340
|
-
"dsh-magpie-connect": "codex/",
|
|
341
|
-
"dsh-magpie-connector": "codex/"
|
|
342
|
-
},
|
|
343
333
|
effortStarts: {
|
|
344
334
|
"gpt-6.1-sol": "medium",
|
|
345
335
|
"gpt-6-luna": "high",
|
|
@@ -364,6 +354,14 @@ window.__ModuleLoader__.load({
|
|
|
364
354
|
"xhigh"
|
|
365
355
|
];
|
|
366
356
|
const bare = (model) => model.slice(model.lastIndexOf("/") + 1);
|
|
357
|
+
const prefixOf = (model) => model.slice(0, Math.max(0, model.lastIndexOf("/")));
|
|
358
|
+
const KEYS = /* @__PURE__ */ new Set([
|
|
359
|
+
"id",
|
|
360
|
+
"label",
|
|
361
|
+
"modelPrefixes",
|
|
362
|
+
"effortStarts",
|
|
363
|
+
"childModels"
|
|
364
|
+
]);
|
|
367
365
|
const overlaps = (a, b) => a.startsWith(b) || b.startsWith(a);
|
|
368
366
|
/** Validates the whole list up front; any conflict throws and no partial registry exists. */
|
|
369
367
|
function createRegistry(input) {
|
|
@@ -371,9 +369,9 @@ window.__ModuleLoader__.load({
|
|
|
371
369
|
const ids = /* @__PURE__ */ new Set();
|
|
372
370
|
const starts = /* @__PURE__ */ new Map();
|
|
373
371
|
const prefixes = [];
|
|
374
|
-
const childPrefixes = /* @__PURE__ */ new Map();
|
|
375
372
|
for (const s of strategies) {
|
|
376
373
|
if (!s.id) throw new Error("策略缺少 id");
|
|
374
|
+
for (const key of Object.keys(s)) if (!KEYS.has(key)) throw new Error(`策略 ${s.id} 含未知键:${key}`);
|
|
377
375
|
if (ids.has(s.id)) throw new Error(`策略 id 重复:${s.id}`);
|
|
378
376
|
ids.add(s.id);
|
|
379
377
|
for (const prefix of s.modelPrefixes) {
|
|
@@ -391,18 +389,10 @@ window.__ModuleLoader__.load({
|
|
|
391
389
|
if (starts.has(model)) throw new Error(`起点键重复:${model}(${starts.get(model)} 与 ${s.id})`);
|
|
392
390
|
starts.set(model, s.id);
|
|
393
391
|
}
|
|
394
|
-
for (const capability of CAPABILITIES)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (clash) {
|
|
399
|
-
if (clash.prefix === "" || prefix === "") throw new Error(`provider ${provider} 的空前缀只能属于一个策略(${clash.id} 与 ${s.id} 冲突)`);
|
|
400
|
-
throw new Error(`provider ${provider} 下子模型前缀重叠:${prefix}(${s.id})与 ${clash.prefix}(${clash.id})`);
|
|
401
|
-
}
|
|
402
|
-
childPrefixes.set(provider, [...existing, {
|
|
403
|
-
prefix,
|
|
404
|
-
id: s.id
|
|
405
|
-
}]);
|
|
392
|
+
for (const capability of CAPABILITIES) {
|
|
393
|
+
const slot = s.childModels?.[capability];
|
|
394
|
+
if (typeof slot !== "string" || !slot) throw new Error(`策略 ${s.id} 缺少能力槽:${capability}`);
|
|
395
|
+
if (slot.includes("/")) throw new Error(`策略 ${s.id} 的能力槽不能含 /:${slot}`);
|
|
406
396
|
}
|
|
407
397
|
}
|
|
408
398
|
const byId = new Map(strategies.map((s) => [s.id, s]));
|
|
@@ -413,23 +403,17 @@ window.__ModuleLoader__.load({
|
|
|
413
403
|
const name = bare(model).toLowerCase();
|
|
414
404
|
return strategies.find((s) => s.modelPrefixes.some((p) => name.startsWith(p.toLowerCase())));
|
|
415
405
|
},
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
childModel: (familyId, provider, capability) => {
|
|
422
|
-
const s = byId.get(familyId);
|
|
423
|
-
const prefix = s?.transports[provider];
|
|
424
|
-
return s && prefix !== void 0 ? `${prefix}${s.childModels[capability]}` : void 0;
|
|
406
|
+
resolveSlot: (familyId, capability, parentModel, models) => {
|
|
407
|
+
const slot = byId.get(familyId)?.childModels[capability]?.toLowerCase();
|
|
408
|
+
if (!slot) return void 0;
|
|
409
|
+
const hits = models.filter((m) => bare(m).toLowerCase() === slot && prefixOf(m) === prefixOf(parentModel));
|
|
410
|
+
return hits.length === 1 ? hits[0] : void 0;
|
|
425
411
|
},
|
|
426
412
|
effortStart: (model) => {
|
|
427
413
|
const name = bare(model);
|
|
428
414
|
for (const s of strategies) if (Object.hasOwn(s.effortStarts, name)) return s.effortStarts[name];
|
|
429
415
|
},
|
|
430
|
-
hasProvider: (provider) => childPrefixes.has(provider),
|
|
431
416
|
get: (familyId) => byId.get(familyId),
|
|
432
|
-
providers: () => [...childPrefixes.keys()],
|
|
433
417
|
ids: () => strategies.map((s) => s.id),
|
|
434
418
|
labels: () => strategies.map((s) => s.label)
|
|
435
419
|
});
|
|
@@ -842,11 +826,13 @@ window.__ModuleLoader__.load({
|
|
|
842
826
|
"aria-hidden": true
|
|
843
827
|
}, (0, react.createElement)("path", { d: "M3 5.5h9.5M10 3l2.5 2.5L10 8" }), (0, react.createElement)("path", { d: "M13 10.5H3.5M6 8l-2.5 2.5L6 13" }));
|
|
844
828
|
}
|
|
829
|
+
const noticeText = (notice) => `最近子任务未路由:${notice.reason}(按 DSH 默认运行)`;
|
|
845
830
|
function RoutingChip({ sessionId, rpc, initialView, initiallyOpen = false, initialToast, subscribeSelection }) {
|
|
846
831
|
const [view, setView] = (0, react.useState)(initialView);
|
|
847
832
|
const [open, setOpen] = (0, react.useState)(initiallyOpen);
|
|
848
833
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
849
834
|
const [error, setError] = (0, react.useState)("");
|
|
835
|
+
const [notApplicable, setNotApplicable] = (0, react.useState)(false);
|
|
850
836
|
const timer = (0, react.useRef)(void 0);
|
|
851
837
|
const [toast, setToast] = (0, react.useState)(initialToast);
|
|
852
838
|
const rootRef = (0, react.useRef)(null);
|
|
@@ -876,6 +862,25 @@ window.__ModuleLoader__.load({
|
|
|
876
862
|
remember();
|
|
877
863
|
setToast(next);
|
|
878
864
|
}, [view, sessionId]);
|
|
865
|
+
const seenNotice = (0, react.useRef)(void 0);
|
|
866
|
+
const noticePrimed = (0, react.useRef)(false);
|
|
867
|
+
(0, react.useEffect)(() => {
|
|
868
|
+
if (!view || viewSession.current !== sessionId) return;
|
|
869
|
+
const notice = view.childNotice;
|
|
870
|
+
if (!noticePrimed.current) {
|
|
871
|
+
noticePrimed.current = true;
|
|
872
|
+
seenNotice.current = notice?.id;
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (!notice || notice.id === seenNotice.current) return;
|
|
876
|
+
seenNotice.current = notice.id;
|
|
877
|
+
if (!view.decisionToast) return;
|
|
878
|
+
remember();
|
|
879
|
+
setToast({
|
|
880
|
+
id: -notice.id,
|
|
881
|
+
lines: [noticeText(notice)]
|
|
882
|
+
});
|
|
883
|
+
}, [view, sessionId]);
|
|
879
884
|
(0, react.useEffect)(() => {
|
|
880
885
|
if (!toast) return;
|
|
881
886
|
const hide = setTimeout(() => setToast(void 0), TOAST_MS);
|
|
@@ -910,24 +915,33 @@ window.__ModuleLoader__.load({
|
|
|
910
915
|
(0, react.useEffect)(() => {
|
|
911
916
|
let active = true;
|
|
912
917
|
primed.current = false;
|
|
918
|
+
noticePrimed.current = false;
|
|
913
919
|
seen.current = void 0;
|
|
914
920
|
viewSession.current = void 0;
|
|
915
921
|
setToast(void 0);
|
|
922
|
+
let poll;
|
|
916
923
|
const refresh = () => {
|
|
917
924
|
callRouting(rpc, sessionId).then((value) => {
|
|
918
|
-
if (active)
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
925
|
+
if (!active) return;
|
|
926
|
+
if (value?.applicable === false) {
|
|
927
|
+
setNotApplicable(true);
|
|
928
|
+
clearInterval(poll);
|
|
929
|
+
subscription.current?.stop();
|
|
930
|
+
subscription.current = void 0;
|
|
931
|
+
return;
|
|
923
932
|
}
|
|
933
|
+
viewSession.current = sessionId;
|
|
934
|
+
setView(value);
|
|
935
|
+
setError("");
|
|
936
|
+
ensureSubscription();
|
|
924
937
|
}, (reason) => {
|
|
925
938
|
if (active) setError(String(reason));
|
|
926
939
|
});
|
|
927
940
|
};
|
|
928
941
|
refreshRef.current = refresh;
|
|
942
|
+
setNotApplicable(false);
|
|
929
943
|
refresh();
|
|
930
|
-
|
|
944
|
+
poll = setInterval(refresh, 3e3);
|
|
931
945
|
return () => {
|
|
932
946
|
active = false;
|
|
933
947
|
clearInterval(poll);
|
|
@@ -1006,12 +1020,18 @@ window.__ModuleLoader__.load({
|
|
|
1006
1020
|
key: "data",
|
|
1007
1021
|
className: "amr-muted"
|
|
1008
1022
|
}, "AUTO 会把脱敏任务文本发送给设置中所选的决策服务判断。"),
|
|
1023
|
+
view?.childNotice && (0, react.createElement)("p", {
|
|
1024
|
+
key: "notice",
|
|
1025
|
+
role: "status",
|
|
1026
|
+
className: "amr-muted"
|
|
1027
|
+
}, noticeText(view.childNotice)),
|
|
1009
1028
|
error && (0, react.createElement)("p", {
|
|
1010
1029
|
key: "error",
|
|
1011
1030
|
role: "alert",
|
|
1012
1031
|
className: "amr-error"
|
|
1013
1032
|
}, error)
|
|
1014
1033
|
];
|
|
1034
|
+
if (notApplicable) return null;
|
|
1015
1035
|
return (0, react.createElement)("span", {
|
|
1016
1036
|
ref: (el) => {
|
|
1017
1037
|
rootRef.current = el;
|
package/lib/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { policyHost } from './policy-host.js';
|
|
|
11
11
|
import { PolicyStore } from './policy-store.js';
|
|
12
12
|
import { currentMainSelection, mainSupported } from './support.js';
|
|
13
13
|
import { createRegistry, registry as builtin } from './strategies/index.js';
|
|
14
|
+
const SCOPE_NOTE = '按官方工具 ALS 上下文、同父与 startup 绑定;不能区分同窗口插件旁支,旁支可能一并受管。';
|
|
14
15
|
export const name = 'dsh-auto-model-routing';
|
|
15
16
|
/** Child decision records. The historical directory name is kept on purpose: records are not migrated. */
|
|
16
17
|
export const CHILD_DECISIONS_DIR = 'state/auto-model-routing/policy-v2';
|
|
@@ -19,7 +20,7 @@ export function apply(ctx, config = {}) {
|
|
|
19
20
|
const home = config.home ?? process.env.DSH_HOME ?? resolve(homedir(), '.dsh');
|
|
20
21
|
const registry = config.strategies ? createRegistry(config.strategies) : builtin;
|
|
21
22
|
const settings = new SettingsStore(resolve(home, 'plugins/dsh-auto-model-routing/config.json'));
|
|
22
|
-
const hostSettings = settingsHost(ctx, settings
|
|
23
|
+
const hostSettings = settingsHost(ctx, settings);
|
|
23
24
|
const jev = new JevClient({ get: hostSettings.credential }, config.jevBase, config.jevTimeoutMs ?? (() => settings.read().settings.jevTimeoutMs), config.jevBase ? undefined : () => settings.read().settings.decisionService);
|
|
24
25
|
const stateFiles = new FileStateStore(resolve(home, 'state/auto-model-routing/sessions'));
|
|
25
26
|
const state = new RoutingState(stateFiles);
|
|
@@ -27,7 +28,7 @@ export function apply(ctx, config = {}) {
|
|
|
27
28
|
// Official writer is available only while the session controller is mounted.
|
|
28
29
|
let selectModel;
|
|
29
30
|
const policy = policyHost(ctx, { feedback, jev, state, selectModel: async (request) => { if (!selectModel)
|
|
30
|
-
throw new Error('session controller is unavailable'); await selectModel(request); }, store: new PolicyStore(resolve(home, CHILD_DECISIONS_DIR), registry), settings: () => settings.read().settings, catalog:
|
|
31
|
+
throw new Error('session controller is unavailable'); await selectModel(request); }, store: new PolicyStore(resolve(home, CHILD_DECISIONS_DIR), registry), settings: () => settings.read().settings, catalog: provider => hostSettings.catalog(provider), registry });
|
|
31
32
|
const toastOn = () => settings.read().settings.decisionToast;
|
|
32
33
|
ctx.effect(() => ctx.tools.register(routeSubagentTool(jev, { agents: ctx.agents, state, readSettings: () => settings.read().settings, catalog: hostSettings.catalog, registry })));
|
|
33
34
|
ctx.on('agent/created', ({ agent, source }) => {
|
|
@@ -57,6 +58,57 @@ export function apply(ctx, config = {}) {
|
|
|
57
58
|
child.effect(() => () => { selectModel = undefined; });
|
|
58
59
|
});
|
|
59
60
|
ctx.inject(['connection', 'sessionController'], child => {
|
|
61
|
+
const modeRequest = async (operation, payload) => {
|
|
62
|
+
const resolveAgent = async (id) => {
|
|
63
|
+
const result = await child.sessionController.resolveAgent(id);
|
|
64
|
+
return 'agent' in result ? ref(result.agent) : undefined;
|
|
65
|
+
};
|
|
66
|
+
// Child sessions have no AUTO chip: answer before any support check, lock or decoration.
|
|
67
|
+
if (operation === 'state' && typeof payload?.sessionId === 'string') {
|
|
68
|
+
const target = await resolveAgent(payload.sessionId);
|
|
69
|
+
if (target?.origin === 'subagent')
|
|
70
|
+
return Response.json({ applicable: false });
|
|
71
|
+
}
|
|
72
|
+
let manualDecision;
|
|
73
|
+
if (operation === 'setMode' && typeof payload?.sessionId === 'string') {
|
|
74
|
+
const id = payload.sessionId, mode = payload.mode;
|
|
75
|
+
if ((mode === 'AUTO' || mode === 'MANUAL') && state.read(id).mode !== mode && await resolveAgent(id)) {
|
|
76
|
+
const decisionId = policy.appliedDecision(id);
|
|
77
|
+
if (mode === 'MANUAL' && decisionId !== undefined)
|
|
78
|
+
manualDecision = { sessionId: id, decisionId };
|
|
79
|
+
policy.invalidate(id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const response = await endpoint(operation, payload, resolveAgent, state);
|
|
83
|
+
if (!response.ok)
|
|
84
|
+
return response;
|
|
85
|
+
if (manualDecision) {
|
|
86
|
+
try {
|
|
87
|
+
feedback.fact(manualDecision.sessionId, manualDecision.decisionId, 'manual-switch');
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
ctx.logger.warn('auto-model-routing: could not record MANUAL switch');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const sessionId = payload.sessionId;
|
|
94
|
+
const resolved = await child.sessionController.resolveAgent(sessionId).catch(() => undefined);
|
|
95
|
+
const selection = resolved && 'agent' in resolved ? currentMainSelection(ctx, resolved.agent) : undefined;
|
|
96
|
+
const supported = mainSupported(selection, registry);
|
|
97
|
+
if (!supported && state.read(sessionId).mode === 'AUTO') {
|
|
98
|
+
state.setMode(sessionId, 'MANUAL');
|
|
99
|
+
policy.invalidate(sessionId);
|
|
100
|
+
}
|
|
101
|
+
const { previous: _previous, ...persisted } = await response.json();
|
|
102
|
+
return Response.json({ ...persisted, mode: state.read(sessionId).mode, ...policy.mainStatus(sessionId), ...(policy.childNotice(sessionId) ? { childNotice: policy.childNotice(sessionId) } : {}), ...(toastOn() ? { decisionToast: true } : {}), autoSupport: { supported, ...(selection ? { provider: selection.provider, model: selection.model } : {}) } });
|
|
103
|
+
};
|
|
104
|
+
child.provide('autoModelRouting', { call: async (operation, payload) => {
|
|
105
|
+
if (operation !== 'state' && operation !== 'setMode')
|
|
106
|
+
throw new Error('Unsupported routing operation');
|
|
107
|
+
const response = await modeRequest(operation, payload);
|
|
108
|
+
if (!response.ok)
|
|
109
|
+
throw new Error('Routing mode request rejected');
|
|
110
|
+
return response.json();
|
|
111
|
+
} });
|
|
60
112
|
for (const operation of ['state', 'setMode', 'label', 'observe', 'settings', 'saveSettings', 'setKey', 'testDecision', 'childRouting']) {
|
|
61
113
|
child.effect(() => child.connection.fetch.register({
|
|
62
114
|
path: `/api/auto-model-routing.${operation}`,
|
|
@@ -77,54 +129,15 @@ export function apply(ctx, config = {}) {
|
|
|
77
129
|
const result = await child.sessionController.resolveAgent(id);
|
|
78
130
|
return 'agent' in result ? ref(result.agent) : undefined;
|
|
79
131
|
};
|
|
80
|
-
const resolveSessionAgent = async (id) => {
|
|
81
|
-
const result = await child.sessionController.resolveAgent(id);
|
|
82
|
-
return 'agent' in result ? result.agent : undefined;
|
|
83
|
-
};
|
|
84
132
|
const payload = enveloped ? wire.payload : body;
|
|
85
|
-
let manualDecision;
|
|
86
|
-
if (operation === 'setMode' && typeof payload?.sessionId === 'string') {
|
|
87
|
-
const id = payload.sessionId;
|
|
88
|
-
const mode = payload.mode;
|
|
89
|
-
if ((mode === 'AUTO' || mode === 'MANUAL') && state.read(id).mode !== mode && await resolveAgent(id)) {
|
|
90
|
-
const decisionId = policy.appliedDecision(id);
|
|
91
|
-
if (mode === 'MANUAL' && decisionId !== undefined)
|
|
92
|
-
manualDecision = { sessionId: id, decisionId };
|
|
93
|
-
policy.invalidate(id);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
133
|
const response = operation === 'childRouting'
|
|
97
134
|
? (typeof payload?.sessionId === 'string'
|
|
98
|
-
? Response.json(
|
|
135
|
+
? Response.json((record => record ? { ...record, scopeNote: SCOPE_NOTE } : { status: 'unsupported', reason: '没有已绑定的子任务决策' })(policy.status(payload.sessionId)))
|
|
99
136
|
: Response.json({ error: 'invalid request' }, { status: 400 }))
|
|
100
137
|
: operation === 'settings' || operation === 'saveSettings' || operation === 'setKey' || operation === 'testDecision'
|
|
101
138
|
? await hostSettings.endpoint(operation, payload)
|
|
102
139
|
: operation === 'observe' ? await observeEndpoint(payload, resolveAgent, feedback, () => ctx.logger.warn('auto-model-routing: could not record exposure'))
|
|
103
|
-
: operation === 'label' ? await labelEndpoint(payload, resolveAgent, feedback) : await
|
|
104
|
-
if (manualDecision && response.ok) {
|
|
105
|
-
try {
|
|
106
|
-
feedback.fact(manualDecision.sessionId, manualDecision.decisionId, 'manual-switch');
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
ctx.logger.warn('auto-model-routing: could not record MANUAL switch');
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if ((operation === 'state' || operation === 'setMode') && response.ok) {
|
|
113
|
-
const sessionId = payload.sessionId;
|
|
114
|
-
const agent = await resolveSessionAgent(sessionId).catch(() => undefined);
|
|
115
|
-
const selection = agent ? currentMainSelection(ctx, agent) : undefined;
|
|
116
|
-
const supported = mainSupported(selection, registry);
|
|
117
|
-
// Leaving AUTO is persisted: a pick outside the registered families must not keep judging turns.
|
|
118
|
-
if (!supported && state.read(sessionId).mode === 'AUTO') {
|
|
119
|
-
state.setMode(sessionId, 'MANUAL');
|
|
120
|
-
policy.invalidate(sessionId);
|
|
121
|
-
}
|
|
122
|
-
const value = await response.json();
|
|
123
|
-
const main = policy.mainStatus(sessionId);
|
|
124
|
-
const { previous: _previous, ...persisted } = value;
|
|
125
|
-
const result = { ...persisted, mode: state.read(sessionId).mode, ...main, ...(toastOn() ? { decisionToast: true } : {}), autoSupport: { supported, ...(selection ? { provider: selection.provider, model: selection.model } : {}) } };
|
|
126
|
-
return Response.json(enveloped ? { type: 'server-response', rpcId: wire.rpcId, result: { ok: true, value: result } } : result);
|
|
127
|
-
}
|
|
140
|
+
: operation === 'label' ? await labelEndpoint(payload, resolveAgent, feedback) : await modeRequest(operation, payload);
|
|
128
141
|
if (!enveloped)
|
|
129
142
|
return response;
|
|
130
143
|
const value = await response.json();
|
package/lib/policy-host.d.ts
CHANGED
|
@@ -17,16 +17,22 @@ export declare function policyHost(ctx: Context, deps: {
|
|
|
17
17
|
state: RoutingState;
|
|
18
18
|
store: PolicyStore;
|
|
19
19
|
settings: () => Settings;
|
|
20
|
-
catalog: () => Promise<RoutingCatalog>;
|
|
20
|
+
catalog: (provider?: string) => Promise<RoutingCatalog>;
|
|
21
21
|
registry?: Registry;
|
|
22
22
|
}): {
|
|
23
23
|
appliedDecision: (id: string) => number | undefined;
|
|
24
24
|
invalidate: (id: string) => void;
|
|
25
|
+
childNotice: (rootId: string) => {
|
|
26
|
+
id: number;
|
|
27
|
+
childId: string;
|
|
28
|
+
reason: string;
|
|
29
|
+
at: number;
|
|
30
|
+
} | undefined;
|
|
25
31
|
status: (id: string) => {
|
|
26
32
|
childId: string;
|
|
27
33
|
parentId: string;
|
|
28
34
|
rootId: string;
|
|
29
|
-
status: "failed" | "recommended" | "
|
|
35
|
+
status: "failed" | "recommended" | "needs-context" | "unsupported" | "applied";
|
|
30
36
|
path: "ordinary" | "fork" | "teammate";
|
|
31
37
|
explicit: {
|
|
32
38
|
provider: string;
|
|
@@ -34,6 +40,11 @@ export declare function policyHost(ctx: Context, deps: {
|
|
|
34
40
|
reasoningEffort?: string | undefined;
|
|
35
41
|
};
|
|
36
42
|
family?: string | undefined;
|
|
43
|
+
parentSelection?: {
|
|
44
|
+
provider: string;
|
|
45
|
+
model?: string | undefined;
|
|
46
|
+
reasoningEffort?: string | undefined;
|
|
47
|
+
} | undefined;
|
|
37
48
|
decision?: {
|
|
38
49
|
id: number;
|
|
39
50
|
source: "main" | "subagent";
|