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
|
@@ -4,14 +4,10 @@ export interface Registry {
|
|
|
4
4
|
readonly strategies: readonly FamilyStrategy[];
|
|
5
5
|
/** Main-model support decision; undefined means AUTO does not manage this model. */
|
|
6
6
|
familyOfMainModel(model: string | undefined): FamilyStrategy | undefined;
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
/** Child model id for a family on a transport: transport prefix + capability slot. */
|
|
10
|
-
childModel(familyId: string, provider: string, capability: Capability): string | undefined;
|
|
7
|
+
/** Catalog entry for a capability slot: same bare name (case-insensitive) and the parent's exact prefix, unique or none. */
|
|
8
|
+
resolveSlot(familyId: string, capability: Capability, parentModel: string, models: readonly string[]): string | undefined;
|
|
11
9
|
effortStart(model: string): StartEffort | undefined;
|
|
12
|
-
hasProvider(provider: string): boolean;
|
|
13
10
|
get(familyId: string): FamilyStrategy | undefined;
|
|
14
|
-
providers(): string[];
|
|
15
11
|
ids(): string[];
|
|
16
12
|
labels(): string[];
|
|
17
13
|
}
|
package/lib/strategies/index.js
CHANGED
|
@@ -3,6 +3,8 @@ import { codex } from './codex.js';
|
|
|
3
3
|
const CAPABILITIES = ['bounded', 'analytical', 'frontier'];
|
|
4
4
|
const START_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
|
|
5
5
|
const bare = (model) => model.slice(model.lastIndexOf('/') + 1);
|
|
6
|
+
const prefixOf = (model) => model.slice(0, Math.max(0, model.lastIndexOf('/')));
|
|
7
|
+
const KEYS = new Set(['id', 'label', 'modelPrefixes', 'effortStarts', 'childModels']);
|
|
6
8
|
const overlaps = (a, b) => a.startsWith(b) || b.startsWith(a);
|
|
7
9
|
/** Validates the whole list up front; any conflict throws and no partial registry exists. */
|
|
8
10
|
export function createRegistry(input) {
|
|
@@ -10,10 +12,12 @@ export function createRegistry(input) {
|
|
|
10
12
|
const ids = new Set();
|
|
11
13
|
const starts = new Map();
|
|
12
14
|
const prefixes = [];
|
|
13
|
-
const childPrefixes = new Map();
|
|
14
15
|
for (const s of strategies) {
|
|
15
16
|
if (!s.id)
|
|
16
17
|
throw new Error('策略缺少 id');
|
|
18
|
+
for (const key of Object.keys(s))
|
|
19
|
+
if (!KEYS.has(key))
|
|
20
|
+
throw new Error(`策略 ${s.id} 含未知键:${key}`);
|
|
17
21
|
if (ids.has(s.id))
|
|
18
22
|
throw new Error(`策略 id 重复:${s.id}`);
|
|
19
23
|
ids.add(s.id);
|
|
@@ -33,18 +37,12 @@ export function createRegistry(input) {
|
|
|
33
37
|
throw new Error(`起点键重复:${model}(${starts.get(model)} 与 ${s.id})`);
|
|
34
38
|
starts.set(model, s.id);
|
|
35
39
|
}
|
|
36
|
-
for (const capability of CAPABILITIES)
|
|
37
|
-
|
|
40
|
+
for (const capability of CAPABILITIES) {
|
|
41
|
+
const slot = s.childModels?.[capability];
|
|
42
|
+
if (typeof slot !== 'string' || !slot)
|
|
38
43
|
throw new Error(`策略 ${s.id} 缺少能力槽:${capability}`);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const clash = existing.find(p => p.prefix === '' || prefix === '' ? true : overlaps(p.prefix, prefix));
|
|
42
|
-
if (clash) {
|
|
43
|
-
if (clash.prefix === '' || prefix === '')
|
|
44
|
-
throw new Error(`provider ${provider} 的空前缀只能属于一个策略(${clash.id} 与 ${s.id} 冲突)`);
|
|
45
|
-
throw new Error(`provider ${provider} 下子模型前缀重叠:${prefix}(${s.id})与 ${clash.prefix}(${clash.id})`);
|
|
46
|
-
}
|
|
47
|
-
childPrefixes.set(provider, [...existing, { prefix, id: s.id }]);
|
|
44
|
+
if (slot.includes('/'))
|
|
45
|
+
throw new Error(`策略 ${s.id} 的能力槽不能含 /:${slot}`);
|
|
48
46
|
}
|
|
49
47
|
}
|
|
50
48
|
const byId = new Map(strategies.map(s => [s.id, s]));
|
|
@@ -56,15 +54,12 @@ export function createRegistry(input) {
|
|
|
56
54
|
const name = bare(model).toLowerCase();
|
|
57
55
|
return strategies.find(s => s.modelPrefixes.some(p => name.startsWith(p.toLowerCase())));
|
|
58
56
|
},
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const s = byId.get(familyId);
|
|
66
|
-
const prefix = s?.transports[provider];
|
|
67
|
-
return s && prefix !== undefined ? `${prefix}${s.childModels[capability]}` : undefined;
|
|
57
|
+
resolveSlot: (familyId, capability, parentModel, models) => {
|
|
58
|
+
const slot = byId.get(familyId)?.childModels[capability]?.toLowerCase();
|
|
59
|
+
if (!slot)
|
|
60
|
+
return undefined;
|
|
61
|
+
const hits = models.filter(m => bare(m).toLowerCase() === slot && prefixOf(m) === prefixOf(parentModel));
|
|
62
|
+
return hits.length === 1 ? hits[0] : undefined;
|
|
68
63
|
},
|
|
69
64
|
effortStart: (model) => {
|
|
70
65
|
const name = bare(model);
|
|
@@ -73,9 +68,7 @@ export function createRegistry(input) {
|
|
|
73
68
|
return s.effortStarts[name];
|
|
74
69
|
return undefined;
|
|
75
70
|
},
|
|
76
|
-
hasProvider: (provider) => childPrefixes.has(provider),
|
|
77
71
|
get: (familyId) => byId.get(familyId),
|
|
78
|
-
providers: () => [...childPrefixes.keys()],
|
|
79
72
|
ids: () => strategies.map(s => s.id),
|
|
80
73
|
labels: () => strategies.map(s => s.label),
|
|
81
74
|
});
|
|
@@ -6,9 +6,8 @@ export interface FamilyStrategy {
|
|
|
6
6
|
readonly label: string;
|
|
7
7
|
/** Main-model support: matched case-insensitively against the id after its last `/`. */
|
|
8
8
|
readonly modelPrefixes: readonly string[];
|
|
9
|
-
/** Transport provider → model-id prefix for this family's child models on that transport. */
|
|
10
|
-
readonly transports: Readonly<Record<string, string>>;
|
|
11
9
|
/** Exact bare model id → recommended starting effort. */
|
|
12
10
|
readonly effortStarts: Readonly<Record<string, StartEffort>>;
|
|
11
|
+
/** Bare child model names; the provider's live catalog supplies the full id. */
|
|
13
12
|
readonly childModels: Readonly<Record<Capability, string>>;
|
|
14
13
|
}
|
package/lib/subagent-tool.d.ts
CHANGED
|
@@ -9,6 +9,18 @@ interface SessionLike {
|
|
|
9
9
|
origin?: string;
|
|
10
10
|
parentSession?: string;
|
|
11
11
|
};
|
|
12
|
+
requestHeader?: () => {
|
|
13
|
+
config?: {
|
|
14
|
+
provider?: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
reasoningEffort?: string;
|
|
17
|
+
};
|
|
18
|
+
} | undefined;
|
|
19
|
+
};
|
|
20
|
+
options?: {
|
|
21
|
+
provider?: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
reasoningEffort?: string;
|
|
12
24
|
};
|
|
13
25
|
}
|
|
14
26
|
export interface ToolDeps {
|
|
@@ -18,7 +30,7 @@ export interface ToolDeps {
|
|
|
18
30
|
};
|
|
19
31
|
state: RoutingState;
|
|
20
32
|
readSettings: () => Settings;
|
|
21
|
-
catalog: () => RoutingCatalog | Promise<RoutingCatalog>;
|
|
33
|
+
catalog: (provider?: string) => RoutingCatalog | Promise<RoutingCatalog>;
|
|
22
34
|
registry?: Registry;
|
|
23
35
|
}
|
|
24
36
|
/** Preview only: the same Jev judgment AUTO uses, while the root session is AUTO. Writes no state. */
|
package/lib/subagent-tool.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
2
|
import { buildState } from './context.js';
|
|
3
|
-
import {
|
|
3
|
+
import { chooseChildRoute } from './routing.js';
|
|
4
4
|
import { registry as builtin } from './strategies/index.js';
|
|
5
5
|
/** The ordinary session a caller belongs to; undefined when any link is no longer live (treated as MANUAL). */
|
|
6
6
|
function rootOf(caller, agents) {
|
|
@@ -19,15 +19,14 @@ export function routeSubagentTool(jev, deps) {
|
|
|
19
19
|
const registry = deps.registry ?? builtin;
|
|
20
20
|
return defineTool({
|
|
21
21
|
name: 'route_subagent',
|
|
22
|
-
description: 'Optional preview of the AUTO sub-agent route: returns a recommendation only, never an applied route. Automatic routing happens when the official fresh creation tool creates the exact child, before its first request; use that tool directly and do not turn this recommendation into an explicit user constraint. Fork is
|
|
22
|
+
description: 'Optional preview of the AUTO sub-agent route: returns a recommendation only, never an applied route. Automatic routing happens when the official fresh creation tool creates the exact child, before its first request; use that tool directly and do not turn this recommendation into an explicit user constraint. Fork copies the parent and is not routed. Dispatch does not require calling this first. Sends the task text (redacted, up to 8000 characters) to the decision service chosen in settings only while the main session is AUTO; otherwise returns routed=false: delegate with the official defaults. If the user or lead already chose the model or effort, pass it: only the missing dimension is judged. status fallback: delegate with official defaults and tell the user why; needs-context: add goal and scope first; on error (including cancellation): pause delegation and ask the user; never guess or switch models.',
|
|
23
23
|
timeoutMs: 10000,
|
|
24
24
|
parameters: {
|
|
25
25
|
task: { type: 'string', required: true, description: 'The task the sub-agent will do.' },
|
|
26
|
-
provider: { type: 'string', required: true,
|
|
26
|
+
provider: { type: 'string', required: true, description: 'Exact provider of the sub-agent (any provider; normally the current session’s).' },
|
|
27
27
|
model: { type: 'string', description: 'Sub-agent model already chosen by the user or lead; when given, only reasoning_effort is judged.' },
|
|
28
28
|
reasoning_effort: { type: 'string', description: 'Effort already chosen by the user or lead; preserved and validated, never silently replaced.' },
|
|
29
|
-
|
|
30
|
-
path: { type: 'string', enum: ['ordinary', 'fork', 'teammate'], description: 'Creation path; fork is unsupported.' },
|
|
29
|
+
path: { type: 'string', enum: ['ordinary', 'fork', 'teammate'], description: 'Creation path; fork is not routed.' },
|
|
31
30
|
},
|
|
32
31
|
output: {
|
|
33
32
|
schema: {
|
|
@@ -46,8 +45,10 @@ export function routeSubagentTool(jev, deps) {
|
|
|
46
45
|
},
|
|
47
46
|
render: (_args, v) => [{ type: 'text', text: v.status === 'recommended'
|
|
48
47
|
? `Recommended only (not applied): ${v.provider}/${v.model}/${v.reasoning_effort}. Use the official creation tool; AUTO judges and binds the exact child before its first request. Do not copy this recommendation as a new explicit user constraint. Query the child routing status for applied evidence.`
|
|
49
|
-
: v.status ?
|
|
50
|
-
:
|
|
48
|
+
: v.status === 'fallback' ? `fallback: ${v.reason}. 按官方默认委派,并向用户简要说明原因。`
|
|
49
|
+
: v.status === 'needs-context' ? `needs-context: ${v.reason}. 补充目标与范围后再派发。`
|
|
50
|
+
: v.status ? `${v.status}: ${v.reason}. fork 与父会话保持一致,AUTO 不路由。`
|
|
51
|
+
: MANUAL_TEXT }],
|
|
51
52
|
},
|
|
52
53
|
execute: async (args, exec) => {
|
|
53
54
|
const root = rootOf(exec.agent, deps.agents);
|
|
@@ -55,23 +56,25 @@ export function routeSubagentTool(jev, deps) {
|
|
|
55
56
|
if (!root || deps.state.read(root.id).mode !== 'AUTO')
|
|
56
57
|
return { routed: false, reason: 'manual' };
|
|
57
58
|
if (args.path === 'fork')
|
|
58
|
-
return { routed: false, status: 'unsupported', reason: 'fork
|
|
59
|
+
return { routed: false, status: 'unsupported', reason: 'fork' };
|
|
60
|
+
if (!args.provider)
|
|
61
|
+
throw new Error('provider 为空;暂停委派并询问用户');
|
|
59
62
|
const explicit = { ...(args.model ? { model: args.model } : {}), ...(args.reasoning_effort ? { reasoningEffort: args.reasoning_effort } : {}) };
|
|
60
63
|
const result = explicit.model && explicit.reasoningEffort ? { kind: 'ok', depth: 'baseline', capability: 'bounded' }
|
|
61
64
|
: await jev.judgePolicy(buildState({ text: args.task }), exec.signal, 'child', explicit.model ? 'depth' : explicit.reasoningEffort ? 'capability' : 'both');
|
|
62
|
-
if (result.kind
|
|
63
|
-
throw new Error('
|
|
65
|
+
if (result.kind === 'cancelled' || exec.signal.aborted)
|
|
66
|
+
throw new Error('子任务判断已取消;暂停委派并询问用户');
|
|
67
|
+
if (result.kind === 'failed')
|
|
68
|
+
return { routed: false, status: 'fallback', reason: `jev-${result.category}` };
|
|
64
69
|
if (result.capability === 'insufficient_context')
|
|
65
70
|
return { routed: false, status: 'needs-context', reason: '请补充任务目标与范围' };
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
throw new Error(reason.includes('暂停委派并询问用户') ? reason : `${reason};暂停委派并询问用户`);
|
|
74
|
-
}
|
|
71
|
+
const caller = exec.agent;
|
|
72
|
+
const config = caller?.session.requestHeader?.()?.config;
|
|
73
|
+
const parent = { model: config?.model ?? caller?.options?.model, reasoningEffort: config?.reasoningEffort ?? caller?.options?.reasoningEffort };
|
|
74
|
+
const chosen = chooseChildRoute({ provider: args.provider, parent, explicit, capability: result.capability ?? 'bounded', depth: result.depth, catalog: await deps.catalog(args.provider), cap: deps.readSettings().effortCap }, registry);
|
|
75
|
+
if ('fallback' in chosen)
|
|
76
|
+
return { routed: false, status: 'fallback', reason: chosen.fallback };
|
|
77
|
+
const route = chosen.route;
|
|
75
78
|
return { status: 'recommended', ...(result.capability ? { capability: result.capability } : {}), depth: result.depth, provider: route.provider, model: route.model, reasoning_effort: route.reasoningEffort };
|
|
76
79
|
},
|
|
77
80
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-auto-model-routing",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "DeepSeek Harness plugin: independent Jev capability and effort decisions, enforced fresh-child routes, decision visibility and allocation feedback",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "prgrmrwy",
|
package/screenshots.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: subagent-model-routing
|
|
3
|
-
description: 创建或复用子智能体前使用:通过官方 fresh 创建工具委派,照实传递用户或 lead 指定的模型与 effort
|
|
3
|
+
description: 创建或复用子智能体前使用:通过官方 fresh 创建工具委派,照实传递用户或 lead 指定的模型与 effort;AUTO 在创建时自行选配,route_subagent 只是可选预览。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 子会话模型选择
|
|
@@ -11,19 +11,19 @@ AUTO 的子任务路由由插件在**官方 fresh 创建工具**创建的精确
|
|
|
11
11
|
|
|
12
12
|
1. **使用官方 fresh 创建工具**(`subagent` 或 fresh `spawn_teammate`),给出清楚的目标与范围。目标或范围不足时,插件会返回 needs-context,请补充描述后重新创建。
|
|
13
13
|
2. **显式指定的值照实传递**:用户或 lead 指定了模型或 effort 时,原样传给官方工具。只指定一个维度时,插件只判断另一个;两者都指定时不调用 Jev,只做校验。
|
|
14
|
-
3. **`route_subagent`
|
|
14
|
+
3. **`route_subagent` 只是可选预览**:派发不需要先预览,路由只在创建时判断一次。它返回的 `status: recommended` 不等于已应用;不要把推荐结果当成显式参数传给官方工具,让插件在创建时自行选配。
|
|
15
15
|
4. **主会话为 MANUAL**:`route_subagent` 返回 `routed: false`。此时按官方默认委派,只传用户或 lead 指定的值,不传其它 provider/model/reasoning_effort,也不猜档位。
|
|
16
16
|
5. **标注**:在 `description` 末尾以 `[model·effort]` 标注实际使用的配置,例如 `检查鉴权边界 [<model>·<effort>]`;不确定时先查第 7 条的应用证据。
|
|
17
|
-
6. **fork
|
|
17
|
+
6. **fork 与父会话保持一致**:AUTO 不路由 fork,按 DSH 原生行为继承父会话配置。需要按任务选配时改用 fresh。
|
|
18
18
|
7. **应用证据**:通过 `childRouting` 状态接口查看对应子会话的 recommended / applied / unsupported / needs-context / failed。applied 来自实际请求头,而不是工具推荐;未绑定的创建路径不算已路由。
|
|
19
19
|
|
|
20
20
|
## 失败
|
|
21
21
|
|
|
22
|
-
8.
|
|
22
|
+
8. 若调用了预览:返回 `status: fallback`(目录或允许列表中没有可用路由、判断服务失败等)时,按官方默认委派并向用户简要说明原因;返回 `needs-context` 时补充目标与范围后再派发;返回错误(含判断被取消)时**暂停委派并询问用户**,把错误原文告诉用户,不得猜测档位、换用其它模型、改由主会话自己完成或自行重试。创建时路由不可用会自动退化为 DSH 默认行为,并在主会话面板中告警。
|
|
23
23
|
9. 工具不存在(插件未启用):告知用户“子会话 Jev 选模未启用”,然后按官方默认委派,只传用户或 lead 指定的值。
|
|
24
24
|
|
|
25
25
|
## 复用
|
|
26
26
|
|
|
27
27
|
10. 向已有子会话发送新任务前,先比较它的 `[model·effort]` 标注与新任务的需求。需要更高投入、不同模型家族,或无法确认原配置时,**新建**子会话。已创建的子会话收到后续消息时不会重新选模;创建后不动态修改其模型或 effort。
|
|
28
28
|
|
|
29
|
-
此 skill 是使用指引,不能强制 Agent
|
|
29
|
+
此 skill 是使用指引,不能强制 Agent 每次委派前都执行某个工具。与环境级指令「子会话派发」一致:直接用官方 fresh 工具派发,预览可选。
|
package/docs/auto-routing.gif
DELETED
|
Binary file
|
package/docs/settings.gif
DELETED
|
Binary file
|