@weniatall/dsh-task-models 0.3.1 → 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 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.1/dsh-task-models-0.3.1.tgz
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.1.tgz
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/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
- function keepCurrent(agent, current, reason) {
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
- // Capture the delegated prompt before agent/request: the Host commits the
950
- // current step's user message only after the request waterfall.
951
- ctx.on('agent/pre-step', async ({ agent, turn, step }, next) => {
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
- 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
- })
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
- // Prepending makes this router the outermost agent/request listener: it
972
- // observes every other listener's result through next() and applies its
973
- // profile last. It only touches delegated children.
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 keepCurrent(agent, current, 'internal-role')
981
- if (planModeActive(agent?.session?.events)) return keepCurrent(agent, current, 'plan-mode')
982
- if (!cfg.routing.enabled) return keepCurrent(agent, current, 'routing-disabled')
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 keepCurrent(agent, current, 'no-profiles')
989
- if (requestHeaderOf(agent) !== undefined) return keepCurrent(agent, current, 'existing-request')
990
- if (cfg.routing.respectExplicitChildModel && childHasExplicitRoute(agent)) return keepCurrent(agent, current, 'explicit-child-model')
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 keepCurrent(agent, current, 'no-task')
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 keepCurrent(agent, current, bypass)
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.1",
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
+ }