@weniatall/dsh-task-models 0.3.0 → 0.3.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/LICENSE +21 -0
- package/README.md +5 -2
- package/lib/client.js +3 -3
- package/lib/index.js +79 -25
- package/package.json +5 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Veildawn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ dsh plugin --profile web add @weniatall/dsh-task-models
|
|
|
34
34
|
或锁定版本,从 GitHub Release 安装 tarball:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
dsh plugin --profile web add https://github.com/veildawn/dsh-plugins/releases/download/dsh-task-models@v0.3.
|
|
37
|
+
dsh plugin --profile web add https://github.com/veildawn/dsh-plugins/releases/download/dsh-task-models@v0.3.2/dsh-task-models-0.3.2.tgz
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
> 本包已发布到 npm scope `@weniatall/dsh-task-models`,后续版本由 CI 通过 Trusted
|
|
@@ -47,7 +47,7 @@ dsh plugin --profile web add https://github.com/veildawn/dsh-plugins/releases/do
|
|
|
47
47
|
cd plugins/dsh-task-models
|
|
48
48
|
npm test
|
|
49
49
|
npm pack
|
|
50
|
-
dsh plugin --profile web add ./weniatall-dsh-task-models-0.3.
|
|
50
|
+
dsh plugin --profile web add ./weniatall-dsh-task-models-0.3.2.tgz
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
> 本仓库的 `AGENTS.md` 规定:除非用户明确要求,不在本机部署插件或重启服务。开发验证请只运行
|
|
@@ -163,6 +163,9 @@ task-models:
|
|
|
163
163
|
## 与 DSH 的边界
|
|
164
164
|
|
|
165
165
|
- 不 patch `ctx.subagents.start`;只在公开的 `agent/pre-step` / `agent/request` 上工作。
|
|
166
|
+
子代理的模型改写发生在 `agent/pre-step`(通过 `agent.model.select()`):DSH 0.2.2 起
|
|
167
|
+
`agent/request` 已废弃且会在瀑布之后强校验模型一致性,从那里改模型会直接抛错。
|
|
168
|
+
`agent/request` 只用于记录子代理实际使用的模型。
|
|
166
169
|
- 不新增子代理工具,不修改 `subagent` / `workflow` 的工具 schema。
|
|
167
170
|
- 不处理 ACP / Codex / Claude Code 等外部进程子代理的模型选择;这些由各自外部进程决定。
|
|
168
171
|
- 主任务自动分类、识图委派、顾问复核不在本插件职责内;由宿主或相应插件负责。本插件只处理
|
package/lib/client.js
CHANGED
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
};
|
|
40
40
|
const INTRO_TEXT = "为不同类型的子任务选择不同模型;未命中时保持当前路由。";
|
|
41
41
|
const css = `
|
|
42
|
-
.tm-page{display:flex;flex-direction:column;gap:14px;width:100%;max-width:820px;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family)
|
|
42
|
+
.tm-page{display:flex;flex-direction:column;gap:14px;width:100%;max-width:820px;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family)}
|
|
43
43
|
.tm-page h2,.tm-page h3,.tm-page p{margin:0}.tm-page h2{font:var(--dsw-font-l-20)}.tm-intro{color:var(--dsw-alias-label-tertiary);font:var(--dsw-font-s-14);line-height:20px}
|
|
44
44
|
.tm-list{display:flex;flex-direction:column;gap:10px}.tm-card{display:flex;flex-direction:column;gap:10px;padding:14px;border:1px solid var(--dsw-alias-border-subtle,var(--dsw-alias-border-l1));border-radius:var(--dsw-radius-l,12px);background:var(--dsw-alias-background-base,var(--dsw-alias-bg-module-platform))}
|
|
45
|
-
.tm-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.tm-title{font:var(--dsw-font-
|
|
45
|
+
.tm-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.tm-title{font:var(--dsw-font-base-strong-16,600 16px/24px var(--dsw-font-family));font-weight:600}.tm-detail{margin-top:3px!important;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
|
|
46
46
|
.tm-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.tm-field{display:flex;flex-direction:column;gap:5px}.tm-label{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500}
|
|
47
47
|
.tm-select,.tm-input{box-sizing:border-box;width:100%;height:36px;padding:0 10px;border:1px solid var(--dsw-alias-border-default,var(--dsw-alias-border-l2));border-radius:8px;outline:none;background:var(--dsw-alias-background-base,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary);font:var(--dsw-font-s-14)}
|
|
48
48
|
.tm-select:focus-visible,.tm-input:focus-visible{border-color:var(--dsw-alias-brand-primary,#4d6bfe);box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-brand-primary,#4d6bfe) 18%,transparent)}.tm-select:disabled,.tm-input:disabled{opacity:.55}
|
|
49
|
-
.tm-actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:8px}.tm-button{display:inline-flex;align-items:center;justify-content:center;height:36px;padding:0 14px;border:1px solid var(--dsw-alias-border-default,var(--dsw-alias-border-l2));border-radius:18px;background:transparent;color:var(--dsw-alias-label-primary);font:var(--dsw-font-s-14);cursor:pointer}.tm-button:disabled{cursor:default;opacity:.45}.tm-primary{border-color:transparent;background:var(--dsw-alias-brand-primary,#4d6bfe);color
|
|
49
|
+
.tm-actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:8px}.tm-button{display:inline-flex;align-items:center;justify-content:center;height:36px;padding:0 14px;border:1px solid var(--dsw-alias-border-default,var(--dsw-alias-border-l2));border-radius:18px;background:transparent;color:var(--dsw-alias-label-primary);font:var(--dsw-font-s-14);cursor:pointer}.tm-button:disabled{cursor:default;opacity:.45}.tm-primary{border-color:transparent;background:var(--dsw-alias-brand-primary,#4d6bfe);color:var(--dsw-alias-label-primary-foreground,#fff)}.tm-danger{color:var(--dsw-alias-state-error-primary)}
|
|
50
50
|
.tm-feedback{padding:9px 11px;border-radius:8px;font-size:12px;line-height:18px}.tm-feedback.tm-error{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#d84848) 10%,transparent);color:var(--dsw-alias-state-error-primary,#d84848)}.tm-feedback.tm-warn{background:color-mix(in srgb,var(--dsw-alias-state-warning-primary,#d98e00) 12%,transparent);color:var(--dsw-alias-state-warning-primary,#d98e00);display:flex;align-items:center;justify-content:space-between;gap:8px}.tm-status{min-height:18px;color:var(--dsw-alias-label-tertiary);font-size:12px}
|
|
51
51
|
.tm-preview-result{padding:10px 12px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-label-secondary) 8%,transparent);font-size:12px;line-height:20px;display:flex;flex-direction:column;gap:4px}
|
|
52
52
|
.tm-textarea{box-sizing:border-box;width:100%;min-height:72px;padding:8px 10px;border:1px solid var(--dsw-alias-border-default,var(--dsw-alias-border-l2));border-radius:8px;outline:none;background:var(--dsw-alias-background-base,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary);font:var(--dsw-font-s-14);resize:vertical}
|
package/lib/index.js
CHANGED
|
@@ -434,6 +434,18 @@ function toRoute(profile) {
|
|
|
434
434
|
}
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
+
/** 把任意携带 provider/model 的对象收敛成路由配置;缺失时返回 undefined。 */
|
|
438
|
+
function toRouteConfig(entry) {
|
|
439
|
+
if (!entry?.provider || !entry?.model) return undefined
|
|
440
|
+
return {
|
|
441
|
+
provider: entry.provider,
|
|
442
|
+
model: entry.model,
|
|
443
|
+
...(entry.reasoningEffort === undefined || entry.reasoningEffort === null || entry.reasoningEffort === ''
|
|
444
|
+
? {}
|
|
445
|
+
: { reasoningEffort: String(entry.reasoningEffort) }),
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
437
449
|
function applyRoute(config, route) {
|
|
438
450
|
const { reasoningEffort: _previousEffort, ...rest } = config
|
|
439
451
|
return {
|
|
@@ -554,7 +566,8 @@ export function apply(ctx, config = {}) {
|
|
|
554
566
|
}))
|
|
555
567
|
}
|
|
556
568
|
|
|
557
|
-
|
|
569
|
+
/** 记录「保持当前模型」的原因,并原样返回候选配置。 */
|
|
570
|
+
function applyKeep(agent, current, reason) {
|
|
558
571
|
rememberRoutingDetail(agent, { status: 'current', source: 'current', reason, durationMs: 0 })
|
|
559
572
|
return current
|
|
560
573
|
}
|
|
@@ -946,52 +959,69 @@ export function apply(ctx, config = {}) {
|
|
|
946
959
|
)
|
|
947
960
|
})
|
|
948
961
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
962
|
+
ctx.on('session/event', (session, event) => {
|
|
963
|
+
if (event?.type === 'subagent/descriptor' && typeof event.data?.label === 'string') {
|
|
964
|
+
subagentLabels.set(session, event.data.label)
|
|
965
|
+
}
|
|
966
|
+
if (event?.type === 'request/header') {
|
|
967
|
+
rememberChildModel({ session }, event.data?.header?.config ?? event.data?.config, true)
|
|
968
|
+
}
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
// DSH 0.2.2 起 `agent/request` 已废弃,且宿主会在瀑布之后强校验模型一致性:
|
|
972
|
+
// 改了 provider/model/reasoningEffort 会直接抛错。因此子代理的路由改写移到
|
|
973
|
+
// 步骤准备期,通过 `agent.model.select()` 落地,agent/request 只负责记录结果。
|
|
974
|
+
ctx.on('agent/pre-step', async ({ agent, turn, step, signal, modelSelection }, next) => {
|
|
952
975
|
const decision = await next()
|
|
953
976
|
if (decision.kind !== 'enter') return decision
|
|
977
|
+
// 宿主只在请求瀑布之后才提交本步骤的用户消息,所以委派提示词要在这里抓取。
|
|
954
978
|
if (isSubagent(agent) && turn === 1 && step === 1
|
|
955
979
|
&& agent?.options?.modelRole === undefined && !firstStepText.has(agent)) {
|
|
956
980
|
const text = promptTextOf(decision.messages)
|
|
957
981
|
if (text !== '') firstStepText.set(agent, text.slice(-6_000))
|
|
958
982
|
}
|
|
983
|
+
|
|
984
|
+
// 决定本步骤的模型:宿主会在 pre-step 返回后把 agent.model.current() 固定为本步骤选择,
|
|
985
|
+
// 之后再从 agent/request 改模型会触发宿主的强校验错误,所以必须在这里写入。
|
|
986
|
+
const current = routeCandidateOf(agent, modelSelection)
|
|
987
|
+
const routed = await routeChildRequest(agent, turn, current, signal)
|
|
988
|
+
if (routed !== current) applyChildRoute(agent, routed)
|
|
989
|
+
signal?.throwIfAborted?.()
|
|
959
990
|
return decision
|
|
960
991
|
}, { prepend: true })
|
|
961
992
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
993
|
+
/** 读取本步骤为子代理准备的路由候选(未经过步骤准备期时退回请求头/options)。 */
|
|
994
|
+
function routeCandidateOf(agent, modelSelection) {
|
|
995
|
+
const fromSelection = toRouteConfig(modelSelection)
|
|
996
|
+
if (fromSelection !== undefined) return fromSelection
|
|
997
|
+
const header = requestHeaderOf(agent)
|
|
998
|
+
const fromHeader = toRouteConfig(header?.config)
|
|
999
|
+
if (fromHeader !== undefined) return fromHeader
|
|
1000
|
+
return toRouteConfig(agent?.options)
|
|
1001
|
+
}
|
|
970
1002
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
ctx.on('agent/request', async ({ agent, signal }, next) => {
|
|
975
|
-
const current = await next()
|
|
1003
|
+
/** 决定子代理本回合要用的模型;返回与 `current` 相同的对象表示保持现状。 */
|
|
1004
|
+
async function routeChildRequest(agent, turn, current, signal) {
|
|
1005
|
+
if (current === undefined) return current
|
|
976
1006
|
if (!isSubagent(agent)) return current
|
|
977
1007
|
throwIfAborted(signal)
|
|
978
1008
|
rememberChildModel(agent, current)
|
|
979
1009
|
const cfg = routingConfig
|
|
980
|
-
if (agent?.options?.modelRole !== undefined) return
|
|
981
|
-
if (planModeActive(agent?.session?.events)) return
|
|
982
|
-
if (!cfg.routing.enabled) return
|
|
1010
|
+
if (agent?.options?.modelRole !== undefined) return applyKeep(agent, current, 'internal-role')
|
|
1011
|
+
if (planModeActive(agent?.session?.events)) return applyKeep(agent, current, 'plan-mode')
|
|
1012
|
+
if (!cfg.routing.enabled) return applyKeep(agent, current, 'routing-disabled')
|
|
983
1013
|
|
|
984
1014
|
// An adopted snapshot survives edits, disablement and removal of its profile.
|
|
985
1015
|
// Only the explicit global switch and the existing bypasses suspend replay.
|
|
986
1016
|
let cached = activeDecision(agent)
|
|
987
1017
|
if (cached === undefined) {
|
|
988
|
-
if (![...cfg.profiles.values()].some((profile) => profile.enabled)) return
|
|
989
|
-
if (requestHeaderOf(agent) !== undefined) return
|
|
990
|
-
if (cfg.routing.respectExplicitChildModel && childHasExplicitRoute(agent)) return
|
|
1018
|
+
if (![...cfg.profiles.values()].some((profile) => profile.enabled)) return applyKeep(agent, current, 'no-profiles')
|
|
1019
|
+
if (requestHeaderOf(agent) !== undefined) return applyKeep(agent, current, 'existing-request')
|
|
1020
|
+
if (cfg.routing.respectExplicitChildModel && childHasExplicitRoute(agent)) return applyKeep(agent, current, 'explicit-child-model')
|
|
991
1021
|
const label = labelOf(agent)
|
|
992
1022
|
const captured = firstStepText.get(agent)
|
|
993
1023
|
const text = typeof captured === 'string' && captured !== '' ? captured : taskTextOf(agent)
|
|
994
|
-
if ((label ?? '') === '' && (text ?? '') === '') return
|
|
1024
|
+
if ((label ?? '') === '' && (text ?? '') === '') return applyKeep(agent, current, 'no-task')
|
|
995
1025
|
decisionFor(agent, cfg, label, text, signal)
|
|
996
1026
|
cached = taskDecisions.get(agent)
|
|
997
1027
|
}
|
|
@@ -1009,7 +1039,7 @@ export function apply(ctx, config = {}) {
|
|
|
1009
1039
|
ruleAnalysis: decision.ruleAnalysis, durationMs: decision.durationMs,
|
|
1010
1040
|
})
|
|
1011
1041
|
}
|
|
1012
|
-
return
|
|
1042
|
+
return applyKeep(agent, current, bypass)
|
|
1013
1043
|
}
|
|
1014
1044
|
rememberRoutingDetail(agent, decision)
|
|
1015
1045
|
if (decision.route === undefined) return current
|
|
@@ -1017,5 +1047,29 @@ export function apply(ctx, config = {}) {
|
|
|
1017
1047
|
cached.applied = true
|
|
1018
1048
|
rememberChildModel(agent, routed)
|
|
1019
1049
|
return routed
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* 在步骤准备期写入下一次请求的模型选择。
|
|
1054
|
+
* 宿主没有 agent.model 时(旧版本)退回追加 model/selection 事件。
|
|
1055
|
+
*/
|
|
1056
|
+
function applyChildRoute(agent, routed) {
|
|
1057
|
+
const selection = {
|
|
1058
|
+
provider: routed.provider,
|
|
1059
|
+
model: routed.model,
|
|
1060
|
+
...(routed.reasoningEffort === undefined ? {} : { reasoningEffort: routed.reasoningEffort }),
|
|
1061
|
+
}
|
|
1062
|
+
if (typeof agent?.model?.select === 'function') {
|
|
1063
|
+
agent.model.select(selection)
|
|
1064
|
+
return
|
|
1065
|
+
}
|
|
1066
|
+
agent?.session?.append?.('model/selection', selection)
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// agent/request 只做记录:子代理实际使用的模型已经由步骤准备期决定。
|
|
1070
|
+
ctx.on('agent/request', async ({ agent }, next) => {
|
|
1071
|
+
const current = await next()
|
|
1072
|
+
if (isSubagent(agent) && current?.provider && current?.model) rememberChildModel(agent, current)
|
|
1073
|
+
return current
|
|
1020
1074
|
}, { prepend: true })
|
|
1021
1075
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weniatall/dsh-task-models",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "子任务模型路由插件:按任务类型为子代理选择模型,显式标签优先、规则试运行与路由解释,可选 TypeSafe Jev 决策与轻量分类回退,并区分预选与请求头确认的 workflow 成员模型。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -10,9 +10,6 @@
|
|
|
10
10
|
"./client": "./lib/client.js",
|
|
11
11
|
"./package.json": "./package.json"
|
|
12
12
|
},
|
|
13
|
-
"scripts": {
|
|
14
|
-
"test": "node --test test/*.test.mjs && node ../../scripts/check-host-settings.mjs plugins/dsh-task-models task-models"
|
|
15
|
-
},
|
|
16
13
|
"files": [
|
|
17
14
|
"lib/index.js",
|
|
18
15
|
"lib/core.js",
|
|
@@ -74,5 +71,8 @@
|
|
|
74
71
|
"homepage": "https://github.com/veildawn/dsh-plugins/tree/main/plugins/dsh-task-models#readme",
|
|
75
72
|
"bugs": {
|
|
76
73
|
"url": "https://github.com/veildawn/dsh-plugins/issues"
|
|
74
|
+
},
|
|
75
|
+
"scripts": {
|
|
76
|
+
"test": "node --test test/*.test.mjs && node ../../scripts/check-host-settings.mjs plugins/dsh-task-models task-models"
|
|
77
77
|
}
|
|
78
|
-
}
|
|
78
|
+
}
|