dsh-auto-model-routing 0.4.2 → 0.5.1

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.
Files changed (78) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +10 -15
  3. package/README.zh.md +10 -15
  4. package/docs/SCREENSHOTS.md +2 -2
  5. package/docs/auto-routing.gif +0 -0
  6. package/docs/feedback-evaluation.md +4 -4
  7. package/docs/host-capability-report.md +2 -0
  8. package/docs/jev-policy-v2-validation.md +3 -1
  9. package/docs/magpie-live-acceptance.md +2 -0
  10. package/docs/policy-v2-development.md +5 -3
  11. package/docs/policy-v2-real-evaluation-plan.md +2 -0
  12. package/docs/settings.gif +0 -0
  13. package/lib/client/control.d.ts +2 -2
  14. package/lib/client/control.js +10 -20
  15. package/lib/client/index.d.ts +0 -1
  16. package/lib/client/index.js +9 -6
  17. package/lib/client/label.d.ts +7 -10
  18. package/lib/client/label.js +17 -30
  19. package/lib/client/settings-section.d.ts +2 -2
  20. package/lib/client/settings-section.js +7 -69
  21. package/lib/client/strategy-details.js +3 -4
  22. package/lib/client.js +165 -481
  23. package/lib/context.d.ts +1 -3
  24. package/lib/context.js +3 -4
  25. package/lib/effort.d.ts +2 -10
  26. package/lib/effort.js +1 -31
  27. package/lib/endpoints.d.ts +11 -10
  28. package/lib/endpoints.js +29 -46
  29. package/lib/feedback.d.ts +60 -7
  30. package/lib/feedback.js +15 -5
  31. package/lib/index.d.ts +4 -0
  32. package/lib/index.js +17 -86
  33. package/lib/jev.d.ts +17 -29
  34. package/lib/jev.js +6 -34
  35. package/lib/policy-host.d.ts +6 -8
  36. package/lib/policy-host.js +39 -38
  37. package/lib/policy-store.d.ts +9 -20
  38. package/lib/policy-store.js +28 -16
  39. package/lib/routing.d.ts +20 -0
  40. package/lib/routing.js +51 -0
  41. package/lib/rubric.json +15 -6
  42. package/lib/settings-contract.d.ts +5 -21
  43. package/lib/settings-contract.js +3 -2
  44. package/lib/settings-host.d.ts +4 -8
  45. package/lib/settings-host.js +9 -10
  46. package/lib/settings-validation.d.ts +1 -7
  47. package/lib/settings-validation.js +0 -51
  48. package/lib/settings.d.ts +3 -8
  49. package/lib/settings.js +18 -177
  50. package/lib/state.d.ts +2 -4
  51. package/lib/state.js +2 -7
  52. package/lib/strategies/claude.d.ts +21 -0
  53. package/lib/strategies/claude.js +8 -0
  54. package/lib/strategies/codex.d.ts +21 -0
  55. package/lib/strategies/codex.js +9 -0
  56. package/lib/strategies/index.d.ts +22 -0
  57. package/lib/strategies/index.js +85 -0
  58. package/lib/strategies/types.d.ts +14 -0
  59. package/lib/strategies/types.js +1 -0
  60. package/lib/subagent-tool.d.ts +7 -7
  61. package/lib/subagent-tool.js +33 -76
  62. package/lib/support.d.ts +7 -8
  63. package/lib/support.js +9 -13
  64. package/lib/usage-report.d.ts +2 -2
  65. package/lib/usage-report.js +25 -17
  66. package/package.json +3 -4
  67. package/skills/auto-model-routing-report/SKILL.md +4 -3
  68. package/skills/subagent-model-routing/SKILL.md +14 -30
  69. package/lib/client/route-picker.d.ts +0 -21
  70. package/lib/client/route-picker.js +0 -75
  71. package/lib/judgements.d.ts +0 -41
  72. package/lib/judgements.js +0 -55
  73. package/lib/policy-v2-rubric.json +0 -21
  74. package/lib/policy-v2.d.ts +0 -21
  75. package/lib/policy-v2.js +0 -54
  76. package/lib/router.d.ts +0 -68
  77. package/lib/router.js +0 -153
  78. package/scripts/label-report.mjs +0 -50
package/lib/jev.d.ts CHANGED
@@ -8,7 +8,6 @@ export interface PolicyMeasure {
8
8
  capabilityConfidence?: number;
9
9
  capabilityProbabilities?: Record<string, number>;
10
10
  }
11
- import { type Tier } from './effort.js';
12
11
  export type Depth = 'lower' | 'baseline' | 'higher';
13
12
  export type Capability = 'bounded' | 'analytical' | 'frontier';
14
13
  export type PolicyResult = ({
@@ -21,22 +20,8 @@ export type PolicyResult = ({
21
20
  } | {
22
21
  kind: 'cancelled';
23
22
  };
24
- export declare const policyVersion: string;
25
23
  import type { JevCategory } from './hints.js';
26
24
  export { CATEGORY_HINTS, type JevCategory } from './hints.js';
27
- export type Distribution = Partial<Record<Tier, number>>;
28
- export type JevResult = {
29
- kind: 'ok';
30
- tier: Tier;
31
- confidence?: number;
32
- probabilities?: Distribution;
33
- ms: number;
34
- } | {
35
- kind: 'failed';
36
- category: JevCategory;
37
- } | {
38
- kind: 'cancelled';
39
- };
40
25
  export interface CredentialSource {
41
26
  get(name: string): {
42
27
  value: string;
@@ -44,32 +29,35 @@ export interface CredentialSource {
44
29
  value: string;
45
30
  } | undefined>;
46
31
  }
47
- /** Shared by the main session and `route_subagent`; only `state` differs (design D4). Single source: rubric.json. */
48
- export declare const TIER_QUESTIONS: {
49
- tier: {
32
+ /** The only rule file: main and child questions are taken from it verbatim. */
33
+ export declare const QUESTIONS: {
34
+ capability: {
35
+ type: string;
36
+ instructions: string;
37
+ criteria: {
38
+ bounded: string;
39
+ analytical: string;
40
+ frontier: string;
41
+ insufficient_context: string;
42
+ };
43
+ };
44
+ depth: {
50
45
  type: string;
51
46
  instructions: string;
52
47
  criteria: {
53
- simple: string;
54
- standard: string;
55
- complex: string;
56
- escalated: string;
48
+ lower: string;
49
+ baseline: string;
50
+ higher: string;
57
51
  };
58
52
  };
59
53
  };
60
- /** Content hash of the rubric, stored with each judgement so labels can be compared per rubric. */
61
- export declare const rubricVersion: string;
62
- /** One tier up (`escalated` stays): the only tier arithmetic in code; it never reads message text. */
63
- export declare function tierAbove(tier: Tier): Tier;
64
54
  export declare class JevClient {
65
55
  private readonly env;
66
56
  private readonly timeoutMs;
67
57
  private readonly service?;
68
58
  readonly url: string;
69
59
  constructor(env: CredentialSource, base?: string, timeoutMs?: number | (() => number), service?: (() => DecisionConfig | undefined) | undefined);
70
- /** One request, no retry. Never throws; never returns the body or the key. */
71
- /** `state` is assembled and redacted by the caller (`buildState`). */
72
- judge(state: string, signal: AbortSignal): Promise<JevResult>;
60
+ /** One request, no retry. Never throws; never returns the body or the key. `state` comes from `buildState`. */
73
61
  judgePolicy(state: string, signal: AbortSignal, scope: 'main' | 'child', dimensions?: 'both' | 'capability' | 'depth'): Promise<PolicyResult>;
74
62
  private decide;
75
63
  }
package/lib/jev.js CHANGED
@@ -1,19 +1,11 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { resolveDecisionService } from './decision-service.js';
3
2
  const probability = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1;
4
3
  const distribution = (value) => Object.fromEntries(Object.entries(value ?? {}).filter(([, v]) => probability(v)));
5
- import { tiers } from './effort.js';
6
- import rubric from './rubric.json' with { type: 'json' };
7
- import policyQuestions from './policy-v2-rubric.json' with { type: 'json' };
8
- export const policyVersion = `policy-v2-${createHash('sha256').update(JSON.stringify(policyQuestions)).digest('hex').slice(0, 8)}`;
4
+ import policyQuestions from './rubric.json' with { type: 'json' };
9
5
  export { CATEGORY_HINTS } from './hints.js';
10
6
  const MAX_BODY = 64 * 1024;
11
- /** Shared by the main session and `route_subagent`; only `state` differs (design D4). Single source: rubric.json. */
12
- export const TIER_QUESTIONS = rubric;
13
- /** Content hash of the rubric, stored with each judgement so labels can be compared per rubric. */
14
- export const rubricVersion = createHash('sha256').update(JSON.stringify(TIER_QUESTIONS)).digest('hex').slice(0, 8);
15
- /** One tier up (`escalated` stays): the only tier arithmetic in code; it never reads message text. */
16
- export function tierAbove(tier) { return tiers[Math.min(tiers.indexOf(tier) + 1, tiers.length - 1)]; }
7
+ /** The only rule file: main and child questions are taken from it verbatim. */
8
+ export const QUESTIONS = policyQuestions;
17
9
  function byStatus(status) {
18
10
  if (status === 402)
19
11
  return 'insufficient-credits';
@@ -32,33 +24,13 @@ export class JevClient {
32
24
  timeoutMs;
33
25
  service;
34
26
  url;
35
- constructor(env, base = 'https://openrouter.ai', timeoutMs = 3000, service) {
27
+ constructor(env, base = 'https://openrouter.ai', timeoutMs = 5000, service) {
36
28
  this.env = env;
37
29
  this.timeoutMs = timeoutMs;
38
30
  this.service = service;
39
31
  this.url = `${base}/api/alpha/decisions`;
40
32
  }
41
- /** One request, no retry. Never throws; never returns the body or the key. */
42
- /** `state` is assembled and redacted by the caller (`buildState`). */
43
- async judge(state, signal) {
44
- const result = await this.decide(state, signal, TIER_QUESTIONS);
45
- if (result.kind !== 'ok')
46
- return result;
47
- const answer = result.answers?.tier;
48
- const choice = answer?.choice;
49
- if (!tiers.includes(choice))
50
- return { kind: 'failed', category: 'invalid-response' };
51
- const unit = (v) => typeof v === 'number' && v >= 0 && v <= 1;
52
- const probabilities = {};
53
- for (const t of tiers) {
54
- const v = answer?.probabilities?.[t];
55
- if (unit(v))
56
- probabilities[t] = v;
57
- }
58
- return { kind: 'ok', tier: choice, ms: result.ms,
59
- ...(unit(answer?.confidence) ? { confidence: answer.confidence } : {}),
60
- ...(Object.keys(probabilities).length ? { probabilities } : {}) };
61
- }
33
+ /** One request, no retry. Never throws; never returns the body or the key. `state` comes from `buildState`. */
62
34
  async judgePolicy(state, signal, scope, dimensions = 'both') {
63
35
  const questions = scope === 'main' || dimensions === 'depth' ? { depth: policyQuestions.depth }
64
36
  : dimensions === 'capability' ? { capability: policyQuestions.capability } : policyQuestions;
@@ -82,7 +54,7 @@ export class JevClient {
82
54
  return { kind: 'cancelled' };
83
55
  const configuredTimeout = typeof this.timeoutMs === 'function' ? this.timeoutMs() : this.timeoutMs;
84
56
  // Settings callbacks use the public range; numeric constructor overrides remain available to tests.
85
- const timeout = typeof this.timeoutMs === 'function' && (!Number.isInteger(configuredTimeout) || configuredTimeout < 1000 || configuredTimeout > 4500) ? 3000 : configuredTimeout;
57
+ const timeout = typeof this.timeoutMs === 'function' && (!Number.isInteger(configuredTimeout) || configuredTimeout < 1000 || configuredTimeout > 8000) ? 5000 : configuredTimeout;
86
58
  const deadline = AbortSignal.timeout(timeout);
87
59
  const started = performance.now();
88
60
  try {
@@ -1,6 +1,7 @@
1
1
  import type { FeedbackBook } from './feedback.js';
2
2
  import type { Context } from '@deepseek-ai/cordis';
3
3
  import type { JevClient, Depth } from './jev.js';
4
+ import { type Registry } from './strategies/index.js';
4
5
  import { PolicyStore, type ChildDecision } from './policy-store.js';
5
6
  import type { RoutingState } from './state.js';
6
7
  import type { RoutingCatalog, Settings } from './settings-contract.js';
@@ -17,24 +18,22 @@ export declare function policyHost(ctx: Context, deps: {
17
18
  store: PolicyStore;
18
19
  settings: () => Settings;
19
20
  catalog: () => Promise<RoutingCatalog>;
21
+ registry?: Registry;
20
22
  }): {
21
23
  appliedDecision: (id: string) => number | undefined;
22
24
  invalidate: (id: string) => void;
23
25
  status: (id: string) => {
24
- version: 1;
25
26
  childId: string;
26
27
  parentId: string;
27
28
  rootId: string;
28
- policyVersion: string;
29
- mappingVersion: string;
30
29
  status: "failed" | "recommended" | "unsupported" | "needs-context" | "applied";
31
30
  path: "ordinary" | "fork" | "teammate";
32
31
  explicit: {
33
- provider: "claude" | "codex" | "dsh-magpie-connect" | "dsh-magpie-connector";
32
+ provider: string;
34
33
  model?: string | undefined;
35
34
  reasoningEffort?: string | undefined;
36
35
  };
37
- family?: "claude" | "codex" | undefined;
36
+ family?: string | undefined;
38
37
  decision?: {
39
38
  id: number;
40
39
  source: "main" | "subagent";
@@ -54,12 +53,12 @@ export declare function policyHost(ctx: Context, deps: {
54
53
  capabilityProbabilities?: Record<string, number> | undefined;
55
54
  } | undefined;
56
55
  route?: {
57
- provider: "claude" | "codex" | "dsh-magpie-connect" | "dsh-magpie-connector";
56
+ provider: string;
58
57
  model: string;
59
58
  reasoningEffort: "low" | "medium" | "high" | "xhigh";
60
59
  } | undefined;
61
60
  actual?: {
62
- provider: "claude" | "codex" | "dsh-magpie-connect" | "dsh-magpie-connector";
61
+ provider: string;
63
62
  model: string;
64
63
  reasoningEffort: "low" | "medium" | "high" | "xhigh";
65
64
  } | undefined;
@@ -67,7 +66,6 @@ export declare function policyHost(ctx: Context, deps: {
67
66
  } | undefined;
68
67
  mainStatus: (id: string) => {
69
68
  decision?: NonNullable<ChildDecision["decision"]>;
70
- policyVersion?: string;
71
69
  depth?: Depth;
72
70
  effort?: string;
73
71
  reason?: string;
@@ -1,12 +1,15 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
2
  import { SessionId } from '@deepseek-ai/dsh-session';
3
- import { policyVersion, CATEGORY_HINTS } from './jev.js';
4
- import { choosePolicyRoute, mappingVersion, policyEffort, modelFamily, policyProvider, validatePolicyRoute } from './policy-v2.js';
3
+ import { CATEGORY_HINTS } from './jev.js';
4
+ import { choosePolicyRoute, policyEffort, modelFamily, validatePolicyRoute } from './routing.js';
5
+ import { registry as builtin } from './strategies/index.js';
5
6
  import { userInput } from './input.js';
6
7
  import { buildState, previousTurn } from './context.js';
7
8
  import { isRecord } from './settings-validation.js';
8
9
  import { autoSupportedModel, currentMainSelection, mainSupported } from './support.js';
9
10
  export function policyHost(ctx, deps) {
11
+ const registry = deps.registry ?? builtin;
12
+ const familyOf = (model) => registry.familyOfMainModel(model)?.id;
10
13
  const delegation = new AsyncLocalStorage();
11
14
  const rootOf = (agent) => {
12
15
  let current = agent;
@@ -18,7 +21,7 @@ export function policyHost(ctx, deps) {
18
21
  };
19
22
  const epochs = new Map();
20
23
  const epoch = (id) => epochs.get(id) ?? 0;
21
- const enabled = (rootId) => deps.settings().policy === 'policy-v2' && deps.state.read(rootId).mode === 'AUTO';
24
+ const enabled = (rootId) => deps.state.read(rootId).mode === 'AUTO';
22
25
  const pending = new Map();
23
26
  const main = new WeakMap();
24
27
  ctx.effect(() => () => { pending.clear(); delegation.disable(); });
@@ -28,7 +31,7 @@ export function policyHost(ctx, deps) {
28
31
  const decidedAt = new Map();
29
32
  const applied = new Map();
30
33
  const latest = new Map();
31
- /** Fall back to MANUAL when the main model left AUTO's Claude/GPT scope. */
34
+ /** Fall back to MANUAL when the main model left AUTO's registered families. */
32
35
  const lockUnsupportedMain = (agent) => {
33
36
  deps.state.setMode(agent.id, 'MANUAL');
34
37
  mainStatus.delete(agent.id);
@@ -44,12 +47,12 @@ export function policyHost(ctx, deps) {
44
47
  const args = isRecord(exec.arguments) ? exec.arguments : {};
45
48
  const path = exec.name === 'subagent_fork' || (exec.name === 'spawn_teammate' && args.context === 'fork') ? 'fork' : exec.name === 'spawn_teammate' ? 'teammate' : 'ordinary';
46
49
  if (path === 'fork')
47
- throw new Error('policy-v2 unsupported:fork 的真实历史兼容尚未验证;请暂停,改用 fresh 或由用户选择关闭试验策略后继承。');
50
+ throw new Error('unsupported:fork 的真实历史兼容尚未验证;请暂停,改用 fresh 创建,或由用户切到 MANUAL 后继承。');
48
51
  const provider = args.provider ?? exec.agent.session.requestHeader()?.config.provider ?? exec.agent.options.provider;
49
- if (typeof provider !== 'string' || !policyProvider(provider))
50
- throw new Error('policy-v2 unsupported:无法验证 provider;暂停委派并询问用户');
52
+ if (typeof provider !== 'string' || !registry.hasProvider(provider))
53
+ throw new Error('unsupported:无法验证 provider;暂停委派并询问用户');
51
54
  const parentModel = exec.agent.session.requestHeader()?.config.model ?? exec.agent.options.model;
52
- const family = modelFamily(provider, typeof args.model === 'string' ? args.model : parentModel ?? '');
55
+ const family = modelFamily(provider, typeof args.model === 'string' ? args.model : parentModel ?? '', registry);
53
56
  const explicit = { provider,
54
57
  ...(typeof args.model === 'string' ? { model: args.model } : {}),
55
58
  ...(typeof args.reasoning_effort === 'string' ? { reasoningEffort: args.reasoning_effort } : {}) };
@@ -62,7 +65,7 @@ export function policyHost(ctx, deps) {
62
65
  if (!binding || binding.parentId !== agent.session.header.parentSession)
63
66
  return;
64
67
  // Creation is precisely inside this official tool's async lifetime, never matched by prompt.
65
- const record = { version: 1, childId: agent.id, ...binding, policyVersion, mappingVersion, status: 'recommended' };
68
+ const record = { childId: agent.id, ...binding, status: 'recommended' };
66
69
  deps.store.set(record);
67
70
  deps.store.bind(agent.id);
68
71
  pending.set(agent.id, record);
@@ -70,7 +73,7 @@ export function policyHost(ctx, deps) {
70
73
  });
71
74
  const requireBindingRecord = (agent, record) => {
72
75
  if (deps.store.wasBound(agent.id) && !record)
73
- throw new Error('policy-v2 已绑定子会话的决策记录缺失;停止请求,不恢复继承配置');
76
+ throw new Error('已绑定子会话的决策记录缺失,无法恢复;停止请求,不恢复继承配置');
74
77
  };
75
78
  ctx.on('agent/disposed', ({ agent }) => { pending.delete(agent.id); mainStatus.delete(agent.id); return undefined; });
76
79
  ctx.on('agent/pre-step', async (payload, next) => {
@@ -88,8 +91,8 @@ export function policyHost(ctx, deps) {
88
91
  main.set(agent, { ...old, turn: payload.turn });
89
92
  return decision;
90
93
  }
91
- // The main model left AUTO's Claude/GPT scope: judge nothing, rewrite nothing.
92
- if (!mainSupported(currentMainSelection(ctx, agent))) {
94
+ // The main model left AUTO's registered families: judge nothing, rewrite nothing.
95
+ if (!mainSupported(currentMainSelection(ctx, agent), registry)) {
93
96
  lockUnsupportedMain(agent);
94
97
  return decision;
95
98
  }
@@ -114,7 +117,7 @@ export function policyHost(ctx, deps) {
114
117
  main.set(agent, { turn: payload.turn, depth: result.depth, active: true, epoch: startedEpoch });
115
118
  const { kind: _kind, ...measurement } = result;
116
119
  latest.delete(agent.id);
117
- mainStatus.set(agent.id, { policyVersion, depth: result.depth, reason: 'jev', decision: { ...measurement, id: ++decisionId, source: 'main', tier: result.depth, effort: '待应用', status: 'recommended' } });
120
+ mainStatus.set(agent.id, { depth: result.depth, reason: 'jev', decision: { ...measurement, id: ++decisionId, source: 'main', tier: result.depth, effort: '待应用', status: 'recommended' } });
118
121
  decidedAt.set(decisionId, Date.now());
119
122
  if (decidedAt.size > 200)
120
123
  decidedAt.delete(decidedAt.keys().next().value);
@@ -127,23 +130,21 @@ export function policyHost(ctx, deps) {
127
130
  if (!record) {
128
131
  const root = rootOf(agent);
129
132
  if (root && enabled(root.id))
130
- throw new Error('policy-v2 unsupported:创建没有官方工具身份绑定;该子请求未路由。请暂停并选择受支持创建路径或关闭试验后继承。');
133
+ throw new Error('unsupported:创建没有官方工具身份绑定;该子请求未路由。请暂停并改用受支持的创建路径,或由用户切到 MANUAL 后继承。');
131
134
  return decision;
132
135
  }
133
136
  if (record.parentId !== agent.session.header.parentSession)
134
- throw new Error('policy-v2 子会话身份不匹配');
137
+ throw new Error('子会话身份不匹配');
135
138
  if (!record.route && !enabled(record.rootId)) {
136
139
  pending.delete(agent.id);
137
- throw new Error('policy-v2 创建期间策略或模式已改变;请重新创建,不静默继承');
140
+ throw new Error('创建期间模式已改变;请重新创建,不静默继承');
138
141
  }
139
142
  if (!record.route && enabled(record.rootId)) {
140
143
  if (!pending.has(agent.id))
141
- throw new Error('policy-v2 初始决策未完成且已恢复;不能用后续消息重新选模,请发起新资源决策');
144
+ throw new Error('初始决策未完成且已恢复;不能用后续消息重新选模,请重新创建子任务');
142
145
  if (record.status !== 'recommended')
143
- throw new Error(`policy-v2 ${record.status}:${record.reason ?? '初始决策未完成'};暂停并询问用户`);
146
+ throw new Error(`${record.status}:${record.reason ?? '初始决策未完成'};暂停并询问用户`);
144
147
  const saveFailure = (status, reason) => { record = { ...record, status, reason }; deps.store.set(record); pending.delete(agent.id); };
145
- if (record.policyVersion !== policyVersion || record.mappingVersion !== mappingVersion)
146
- throw new Error('policy-v2 未完成决策版本不可恢复');
147
148
  const explicit = record.explicit;
148
149
  let measure;
149
150
  let capability = 'bounded', depth = 'baseline';
@@ -153,36 +154,36 @@ export function policyHost(ctx, deps) {
153
154
  const result = await deps.jev.judgePolicy(buildState({ text }), payload.signal, 'child', explicit.model ? 'depth' : explicit.reasoningEffort ? 'capability' : 'both');
154
155
  if (!enabled(record.rootId)) {
155
156
  saveFailure('failed', '创建期间模式已变化');
156
- throw new Error('policy-v2 创建期间模式已变化;请重新创建');
157
+ throw new Error('创建期间模式已变化;请重新创建');
157
158
  }
158
159
  if (result.kind === 'cancelled') {
159
160
  saveFailure('failed', '初始判断已取消');
160
- throw new Error('policy-v2 已取消');
161
+ throw new Error('子任务判断已取消');
161
162
  }
162
163
  if (result.kind === 'failed') {
163
164
  saveFailure('failed', result.category);
164
- throw new Error(`policy-v2 Jev 失败:${CATEGORY_HINTS[result.category]};暂停委派并询问用户`);
165
+ throw new Error(`Jev 失败:${CATEGORY_HINTS[result.category]};暂停委派并询问用户`);
165
166
  }
166
167
  if (result.capability === 'insufficient_context') {
167
168
  saveFailure('needs-context', '任务目标或范围不足');
168
- throw new Error('policy-v2 needs-context:请补充任务目标与范围');
169
+ throw new Error('needs-context:请补充任务目标与范围');
169
170
  }
170
171
  measure = result;
171
172
  capability = result.capability ?? 'bounded';
172
173
  depth = result.depth;
173
174
  }
174
175
  try {
175
- const route = choosePolicyRoute(explicit.provider, capability, depth, await deps.catalog(), deps.settings().effortCap, explicit, record.family);
176
+ const route = choosePolicyRoute(explicit.provider, capability, depth, await deps.catalog(), deps.settings().effortCap, explicit, record.family, registry);
176
177
  const parent = ctx.agents.get(SessionId(record.parentId));
177
178
  const events = parent?.session.snapshotEvents() ?? [];
178
179
  const captured = events.find(event => event.type === 'subagent/model-selection-policy');
179
180
  if (captured) {
180
181
  const allowed = captured.data.allowedModels;
181
182
  if (!Array.isArray(allowed) || !allowed.some(item => item.provider === route.provider && item.model === route.model))
182
- throw new Error('policy-v2 路由不在父会话创建时的官方允许列表中;请新建会话');
183
+ throw new Error('路由不在父会话创建时的官方允许列表中;请新建会话');
183
184
  }
184
185
  if (!enabled(record.rootId) || payload.signal.aborted)
185
- throw new Error('policy-v2 创建已取消或模式已变化');
186
+ throw new Error('创建已取消或模式已变化');
186
187
  record = { ...record, route, status: 'recommended', decision: { id: ++decisionId, source: 'subagent', tier: capability, depth, capability, effort: route.reasoningEffort, model: route.model, provider: route.provider, status: 'recommended', ms: measure?.ms ?? 0, decisionProvider: measure?.decisionProvider ?? 'explicit', decisionModel: measure?.decisionModel ?? '未调用', ...(measure?.confidence !== undefined ? { confidence: measure.confidence } : {}), ...(measure?.capabilityConfidence !== undefined ? { capabilityConfidence: measure.capabilityConfidence } : {}), ...(measure?.probabilities ? { probabilities: measure.probabilities } : {}), ...(measure?.capabilityProbabilities ? { capabilityProbabilities: measure.capabilityProbabilities } : {}) } };
187
188
  deps.store.set(record);
188
189
  pending.delete(agent.id);
@@ -193,7 +194,7 @@ export function policyHost(ctx, deps) {
193
194
  }
194
195
  }
195
196
  if (!record.route && enabled(record.rootId))
196
- throw new Error('policy-v2 决策未完成;暂停子请求');
197
+ throw new Error('决策未完成;暂停子请求');
197
198
  return decision;
198
199
  });
199
200
  ctx.on('agent/request', async (payload, next) => {
@@ -204,13 +205,13 @@ export function policyHost(ctx, deps) {
204
205
  if (!runtime?.active || runtime.turn !== payload.turn || runtime.epoch !== epoch(agent.id) || !enabled(agent.id))
205
206
  return config;
206
207
  // Authoritative gate: the request's actual model decides, whatever the picker showed earlier.
207
- if (!autoSupportedModel(config.model)) {
208
+ if (!autoSupportedModel(config.model, registry)) {
208
209
  lockUnsupportedMain(agent);
209
210
  return config;
210
211
  }
211
212
  const unsupported = (effort = '模型不支持调整') => {
212
213
  const old = mainStatus.get(agent.id);
213
- mainStatus.set(agent.id, { ...old, policyVersion, depth: runtime.depth, reason: 'no-effort-support', ...(old?.decision ? { decision: { ...old.decision, status: 'unsupported', model: config.model, provider: config.provider, effort } } : {}) });
214
+ mainStatus.set(agent.id, { ...old, depth: runtime.depth, reason: 'no-effort-support', ...(old?.decision ? { decision: { ...old.decision, status: 'unsupported', model: config.model, provider: config.provider, effort } } : {}) });
214
215
  };
215
216
  const info = await ctx.llm.resolveModelInfo(config.provider, config.model, payload.signal);
216
217
  if (!enabled(agent.id) || runtime.epoch !== epoch(agent.id) || payload.signal.aborted)
@@ -222,7 +223,7 @@ export function policyHost(ctx, deps) {
222
223
  }
223
224
  let reasoningEffort;
224
225
  try {
225
- reasoningEffort = policyEffort(config.model, runtime.depth, efforts, deps.settings().effortCap, info.reasoning?.defaultEffort ? String(info.reasoning.defaultEffort) : undefined);
226
+ reasoningEffort = policyEffort(config.model, runtime.depth, efforts, deps.settings().effortCap, info.reasoning?.defaultEffort ? String(info.reasoning.defaultEffort) : undefined, registry);
226
227
  }
227
228
  catch (error) {
228
229
  // A supported model whose directory cannot produce an automatic effort must not fail the turn.
@@ -230,7 +231,7 @@ export function policyHost(ctx, deps) {
230
231
  unsupported();
231
232
  return config;
232
233
  }
233
- mainStatus.set(agent.id, { ...mainStatus.get(agent.id), policyVersion, depth: runtime.depth, effort: reasoningEffort, reason: 'jev', ...(mainStatus.get(agent.id)?.decision ? { decision: { ...mainStatus.get(agent.id).decision, effort: reasoningEffort, model: config.model, provider: config.provider } } : {}) });
234
+ mainStatus.set(agent.id, { ...mainStatus.get(agent.id), depth: runtime.depth, effort: reasoningEffort, reason: 'jev', ...(mainStatus.get(agent.id)?.decision ? { decision: { ...mainStatus.get(agent.id).decision, effort: reasoningEffort, model: config.model, provider: config.provider } } : {}) });
234
235
  if (reasoningEffort !== config.reasoningEffort) {
235
236
  try {
236
237
  await deps.selectModel({ sessionId: agent.id, provider: config.provider, model: config.model, reasoningEffort });
@@ -245,15 +246,15 @@ export function policyHost(ctx, deps) {
245
246
  const record = deps.store.get(agent.id);
246
247
  requireBindingRecord(agent, record);
247
248
  if (record && !record.route)
248
- throw new Error('policy-v2 初始路由未完成;不静默继承');
249
+ throw new Error('初始路由未完成;不静默继承');
249
250
  if (!record?.route)
250
251
  return config;
251
252
  if (record.status === 'failed')
252
- throw new Error('policy-v2 实际应用失败;暂停子请求并询问用户');
253
+ throw new Error('实际应用失败;暂停子请求并询问用户');
253
254
  if (record.parentId !== agent.session.header.parentSession)
254
- throw new Error('policy-v2 决策身份不匹配');
255
+ throw new Error('决策身份不匹配');
255
256
  // Existing routed instances keep their initial route even when AUTO stops making new decisions.
256
- validatePolicyRoute(await deps.catalog(), record.route.provider, record.route.model, record.route.reasoningEffort, deps.settings().effortCap);
257
+ validatePolicyRoute(await deps.catalog(), record.route.provider, record.route.model, record.route.reasoningEffort, deps.settings().effortCap, registry);
257
258
  return { ...config, ...record.route };
258
259
  }, { prepend: true });
259
260
  const confirmMainRequest = (session, requestSeq, turn) => {
@@ -285,7 +286,7 @@ export function policyHost(ctx, deps) {
285
286
  try {
286
287
  deps.feedback.bind({ sessionId: session.id, messageId: data.message.id, decisionId: d.id, depth: d.depth, model: d.model, effort: d.effort,
287
288
  ...(requestMetadata.has(session.id) && d.provider && decidedAt.has(d.id) ? { snapshot: {
288
- ...requestMetadata.get(session.id), decidedAt: decidedAt.get(d.id), policyVersion, mappingVersion,
289
+ ...requestMetadata.get(session.id), decidedAt: decidedAt.get(d.id), ...(familyOf(d.model) ? { strategy: familyOf(d.model) } : {}),
289
290
  decisionProvider: d.decisionProvider, decisionModel: d.decisionModel, ms: d.ms, provider: d.provider,
290
291
  ...(d.confidence !== undefined ? { confidence: d.confidence } : {}), ...(d.probabilities ? { probabilities: d.probabilities } : {}),
291
292
  } } : {}),
@@ -314,7 +315,7 @@ export function policyHost(ctx, deps) {
314
315
  latest.set(record.rootId, { ...record.decision, status: matches ? 'applied' : 'failed' });
315
316
  deps.store.set({ ...record, ...(record.decision ? { decision: { ...record.decision, status: matches ? 'applied' : 'failed' } } : {}), status: matches ? 'applied' : 'failed', ...(matches ? { actual: record.route } : { reason: '实际请求配置不匹配' }) });
316
317
  if (!matches)
317
- throw new Error('policy-v2 实际请求配置不匹配');
318
+ throw new Error('实际请求配置不匹配');
318
319
  });
319
320
  return {
320
321
  appliedDecision: (id) => enabled(id) ? applied.get(id)?.id : undefined,
@@ -1,11 +1,9 @@
1
1
  import { z } from 'zod';
2
- declare const schema: z.ZodObject<{
3
- version: z.ZodLiteral<1>;
2
+ import { type Registry } from './strategies/index.js';
3
+ declare function schemaFor(registry: Registry): z.ZodObject<{
4
4
  childId: z.ZodString;
5
5
  parentId: z.ZodString;
6
6
  rootId: z.ZodString;
7
- policyVersion: z.ZodString;
8
- mappingVersion: z.ZodString;
9
7
  status: z.ZodEnum<{
10
8
  failed: "failed";
11
9
  recommended: "recommended";
@@ -19,15 +17,11 @@ declare const schema: z.ZodObject<{
19
17
  teammate: "teammate";
20
18
  }>;
21
19
  family: z.ZodOptional<z.ZodEnum<{
22
- claude: "claude";
23
- codex: "codex";
20
+ [x: string]: string;
24
21
  }>>;
25
22
  explicit: z.ZodObject<{
26
23
  provider: z.ZodEnum<{
27
- claude: "claude";
28
- codex: "codex";
29
- "dsh-magpie-connect": "dsh-magpie-connect";
30
- "dsh-magpie-connector": "dsh-magpie-connector";
24
+ [x: string]: string;
31
25
  }>;
32
26
  model: z.ZodOptional<z.ZodString>;
33
27
  reasoningEffort: z.ZodOptional<z.ZodString>;
@@ -55,10 +49,7 @@ declare const schema: z.ZodObject<{
55
49
  }, z.core.$strict>>;
56
50
  route: z.ZodOptional<z.ZodObject<{
57
51
  provider: z.ZodEnum<{
58
- claude: "claude";
59
- codex: "codex";
60
- "dsh-magpie-connect": "dsh-magpie-connect";
61
- "dsh-magpie-connector": "dsh-magpie-connector";
52
+ [x: string]: string;
62
53
  }>;
63
54
  model: z.ZodString;
64
55
  reasoningEffort: z.ZodEnum<{
@@ -70,10 +61,7 @@ declare const schema: z.ZodObject<{
70
61
  }, z.core.$strict>>;
71
62
  actual: z.ZodOptional<z.ZodObject<{
72
63
  provider: z.ZodEnum<{
73
- claude: "claude";
74
- codex: "codex";
75
- "dsh-magpie-connect": "dsh-magpie-connect";
76
- "dsh-magpie-connector": "dsh-magpie-connector";
64
+ [x: string]: string;
77
65
  }>;
78
66
  model: z.ZodString;
79
67
  reasoningEffort: z.ZodEnum<{
@@ -85,11 +73,12 @@ declare const schema: z.ZodObject<{
85
73
  }, z.core.$strict>>;
86
74
  reason: z.ZodOptional<z.ZodString>;
87
75
  }, z.core.$strict>;
88
- export type ChildDecision = z.infer<typeof schema>;
76
+ export type ChildDecision = z.infer<ReturnType<typeof schemaFor>>;
89
77
  /** Strict per-child files: corrupt or mismatched records fail closed; no raw task or credentials. */
90
78
  export declare class PolicyStore {
91
79
  private readonly directory;
92
- constructor(directory: string);
80
+ private readonly schema;
81
+ constructor(directory: string, registry?: Registry);
93
82
  private path;
94
83
  bind(id: string): void;
95
84
  wasBound(id: string): boolean;
@@ -2,23 +2,30 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { mkdirSync, readFileSync, writeFileSync, renameSync, chmodSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { z } from 'zod';
5
- import { POLICY_PROVIDERS } from './policy-v2.js';
6
- const route = z.object({ provider: z.enum(POLICY_PROVIDERS), model: z.string().min(1).max(128), reasoningEffort: z.enum(['low', 'medium', 'high', 'xhigh']) }).strict();
7
- const schema = z.object({
8
- version: z.literal(1), childId: z.string(), parentId: z.string(), rootId: z.string(),
9
- policyVersion: z.string(), mappingVersion: z.string(),
10
- status: z.enum(['recommended', 'applied', 'unsupported', 'needs-context', 'failed']),
11
- path: z.enum(['ordinary', 'teammate', 'fork']),
12
- family: z.enum(['claude', 'codex']).optional(),
13
- explicit: z.object({ provider: z.enum(POLICY_PROVIDERS), model: z.string().optional(), reasoningEffort: z.string().optional() }).strict(),
14
- decision: z.object({ id: z.number(), source: z.enum(['main', 'subagent']), tier: z.string(), depth: z.string(), capability: z.string().optional(), effort: z.string(), model: z.string().optional(), provider: z.string().optional(), status: z.string(), ms: z.number(), decisionProvider: z.string(), decisionModel: z.string(), confidence: z.number().min(0).max(1).optional(), capabilityConfidence: z.number().min(0).max(1).optional(), probabilities: z.record(z.string(), z.number().min(0).max(1)).optional(), capabilityProbabilities: z.record(z.string(), z.number().min(0).max(1)).optional() }).strict().optional(),
15
- route: route.optional(), actual: route.optional(), reason: z.string().max(240).optional(),
16
- }).strict();
5
+ import { registry as builtin } from './strategies/index.js';
6
+ const enumOf = (values) => z.enum(values);
7
+ function schemaFor(registry) {
8
+ const providers = enumOf(registry.providers());
9
+ const route = z.object({ provider: providers, model: z.string().min(1).max(128), reasoningEffort: z.enum(['low', 'medium', 'high', 'xhigh']) }).strict();
10
+ return z.object({
11
+ childId: z.string(), parentId: z.string(), rootId: z.string(),
12
+ status: z.enum(['recommended', 'applied', 'unsupported', 'needs-context', 'failed']),
13
+ path: z.enum(['ordinary', 'teammate', 'fork']),
14
+ family: enumOf(registry.ids()).optional(),
15
+ explicit: z.object({ provider: providers, model: z.string().optional(), reasoningEffort: z.string().optional() }).strict(),
16
+ decision: z.object({ id: z.number(), source: z.enum(['main', 'subagent']), tier: z.string(), depth: z.string(), capability: z.string().optional(), effort: z.string(), model: z.string().optional(), provider: z.string().optional(), status: z.string(), ms: z.number(), decisionProvider: z.string(), decisionModel: z.string(), confidence: z.number().min(0).max(1).optional(), capabilityConfidence: z.number().min(0).max(1).optional(), probabilities: z.record(z.string(), z.number().min(0).max(1)).optional(), capabilityProbabilities: z.record(z.string(), z.number().min(0).max(1)).optional() }).strict().optional(),
17
+ route: route.optional(), actual: route.optional(), reason: z.string().max(240).optional(),
18
+ }).strict();
19
+ }
20
+ /** Records written before 0.5.0 carried these keys; reads accept exactly these values, then drop them. */
21
+ const historical = z.object({ version: z.literal(1).optional(), policyVersion: z.string().optional(), mappingVersion: z.string().optional() });
17
22
  /** Strict per-child files: corrupt or mismatched records fail closed; no raw task or credentials. */
18
23
  export class PolicyStore {
19
24
  directory;
20
- constructor(directory) {
25
+ schema;
26
+ constructor(directory, registry = builtin) {
21
27
  this.directory = directory;
28
+ this.schema = schemaFor(registry);
22
29
  }
23
30
  path(id) { return join(this.directory, `${createHash('sha256').update(id).digest('hex')}.json`); }
24
31
  bind(id) {
@@ -39,7 +46,12 @@ export class PolicyStore {
39
46
  }
40
47
  get(id) {
41
48
  try {
42
- const record = schema.parse(JSON.parse(readFileSync(this.path(id), 'utf8')));
49
+ const raw = JSON.parse(readFileSync(this.path(id), 'utf8'));
50
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
51
+ throw new Error('not an object');
52
+ const { version, policyVersion, mappingVersion, ...rest } = raw;
53
+ historical.parse({ version, policyVersion, mappingVersion });
54
+ const record = this.schema.parse(rest);
43
55
  if (record.childId !== id)
44
56
  throw new Error('child identity mismatch');
45
57
  return record;
@@ -47,11 +59,11 @@ export class PolicyStore {
47
59
  catch (error) {
48
60
  if (error.code === 'ENOENT')
49
61
  return undefined;
50
- throw new Error('policy-v2 决策无法恢复;暂停该子会话');
62
+ throw new Error('子任务决策无法恢复;暂停该子会话');
51
63
  }
52
64
  }
53
65
  set(value) {
54
- const checked = schema.parse(value);
66
+ const checked = this.schema.parse(value);
55
67
  mkdirSync(this.directory, { recursive: true, mode: 0o700 });
56
68
  chmodSync(this.directory, 0o700);
57
69
  const target = this.path(value.childId), temp = `${target}.${randomUUID()}.tmp`;
@@ -0,0 +1,20 @@
1
+ import type { Capability, Depth } from './jev.js';
2
+ import { type AutomaticEffort, type RoutingCatalog } from './settings-contract.js';
3
+ import { type Registry } from './strategies/index.js';
4
+ /** Family of a child model on an exact transport; undefined when no registered strategy owns it. */
5
+ export declare function modelFamily(provider: string, model: string, registry?: Registry): string | undefined;
6
+ export declare function validatePolicyRoute(catalog: RoutingCatalog, provider: string, model: string, effort: string, cap: AutomaticEffort, registry?: Registry): void;
7
+ export declare function policyEffort(model: string, depth: Depth, supported: readonly string[], cap: AutomaticEffort, defaultEffort?: string, registry?: Registry): AutomaticEffort;
8
+ export interface PolicyRoute {
9
+ provider: string;
10
+ model: string;
11
+ reasoningEffort: AutomaticEffort;
12
+ }
13
+ /**
14
+ * Child route: explicit dimensions are kept; otherwise the family's capability slot on this transport.
15
+ * A transport owned by a single family (empty prefix) implies that family; others need `family`.
16
+ */
17
+ export declare function choosePolicyRoute(provider: string, capability: Capability, depth: Depth, catalog: RoutingCatalog, cap: AutomaticEffort, explicit?: {
18
+ model?: string | undefined;
19
+ reasoningEffort?: string | undefined;
20
+ }, family?: string, registry?: Registry): PolicyRoute;