dsh-auto-model-routing 0.4.1 → 0.5.0

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 (75) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +15 -10
  3. package/README.zh.md +15 -9
  4. package/docs/feedback-evaluation.md +20 -3
  5. package/docs/host-capability-report.md +2 -0
  6. package/docs/jev-policy-v2-validation.md +3 -1
  7. package/docs/magpie-live-acceptance.md +2 -0
  8. package/docs/policy-v2-development.md +5 -3
  9. package/docs/policy-v2-real-evaluation-plan.md +2 -0
  10. package/lib/client/control.d.ts +2 -2
  11. package/lib/client/control.js +10 -20
  12. package/lib/client/index.d.ts +9 -2
  13. package/lib/client/index.js +97 -12
  14. package/lib/client/label.d.ts +8 -11
  15. package/lib/client/label.js +51 -29
  16. package/lib/client/settings-section.d.ts +2 -2
  17. package/lib/client/settings-section.js +7 -69
  18. package/lib/client/strategy-details.js +3 -4
  19. package/lib/client.js +316 -489
  20. package/lib/context.d.ts +1 -3
  21. package/lib/context.js +3 -4
  22. package/lib/effort.d.ts +2 -10
  23. package/lib/effort.js +1 -31
  24. package/lib/endpoints.d.ts +11 -9
  25. package/lib/endpoints.js +44 -42
  26. package/lib/feedback.d.ts +77 -6
  27. package/lib/feedback.js +34 -3
  28. package/lib/index.d.ts +4 -0
  29. package/lib/index.js +54 -89
  30. package/lib/jev.d.ts +17 -29
  31. package/lib/jev.js +6 -34
  32. package/lib/policy-host.d.ts +13 -8
  33. package/lib/policy-host.js +98 -47
  34. package/lib/policy-store.d.ts +9 -17
  35. package/lib/policy-store.js +28 -15
  36. package/lib/routing.d.ts +20 -0
  37. package/lib/routing.js +51 -0
  38. package/lib/rubric.json +15 -6
  39. package/lib/settings-contract.d.ts +5 -21
  40. package/lib/settings-contract.js +3 -2
  41. package/lib/settings-host.d.ts +4 -8
  42. package/lib/settings-host.js +9 -9
  43. package/lib/settings-validation.d.ts +1 -7
  44. package/lib/settings-validation.js +0 -51
  45. package/lib/settings.d.ts +3 -8
  46. package/lib/settings.js +18 -177
  47. package/lib/state.d.ts +2 -4
  48. package/lib/state.js +2 -7
  49. package/lib/strategies/claude.d.ts +21 -0
  50. package/lib/strategies/claude.js +8 -0
  51. package/lib/strategies/codex.d.ts +21 -0
  52. package/lib/strategies/codex.js +9 -0
  53. package/lib/strategies/index.d.ts +22 -0
  54. package/lib/strategies/index.js +85 -0
  55. package/lib/strategies/types.d.ts +14 -0
  56. package/lib/strategies/types.js +1 -0
  57. package/lib/subagent-tool.d.ts +7 -7
  58. package/lib/subagent-tool.js +33 -76
  59. package/lib/support.d.ts +24 -0
  60. package/lib/support.js +36 -0
  61. package/lib/usage-report.d.ts +21 -2
  62. package/lib/usage-report.js +86 -22
  63. package/package.json +19 -7
  64. package/skills/auto-model-routing-report/SKILL.md +9 -3
  65. package/skills/subagent-model-routing/SKILL.md +14 -30
  66. package/lib/client/route-picker.d.ts +0 -21
  67. package/lib/client/route-picker.js +0 -75
  68. package/lib/judgements.d.ts +0 -41
  69. package/lib/judgements.js +0 -55
  70. package/lib/policy-v2-rubric.json +0 -21
  71. package/lib/policy-v2.d.ts +0 -20
  72. package/lib/policy-v2.js +0 -53
  73. package/lib/router.d.ts +0 -68
  74. package/lib/router.js +0 -153
  75. package/scripts/label-report.mjs +0 -50
package/lib/index.js CHANGED
@@ -3,48 +3,43 @@ import { SettingsStore } from './settings.js';
3
3
  import { settingsHost } from './settings-host.js';
4
4
  import { homedir } from 'node:os';
5
5
  import { FileStateStore, RoutingState } from './state.js';
6
- import { Router } from './router.js';
7
- import { endpoint, labelEndpoint } from './endpoints.js';
6
+ import { endpoint, labelEndpoint, observeEndpoint } from './endpoints.js';
8
7
  import { FeedbackBook } from './feedback.js';
9
- import { JudgementBook } from './judgements.js';
10
- import { userInput } from './input.js';
11
- import { JevClient, rubricVersion } from './jev.js';
8
+ import { JevClient } from './jev.js';
12
9
  import { routeSubagentTool } from './subagent-tool.js';
13
- import { buildState, previousTurn } from './context.js';
14
10
  import { policyHost } from './policy-host.js';
15
11
  import { PolicyStore } from './policy-store.js';
12
+ import { currentMainSelection, mainSupported } from './support.js';
13
+ import { createRegistry, registry as builtin } from './strategies/index.js';
16
14
  export const name = 'dsh-auto-model-routing';
15
+ /** Child decision records. The historical directory name is kept on purpose: records are not migrated. */
16
+ export const CHILD_DECISIONS_DIR = 'state/auto-model-routing/policy-v2';
17
17
  export const inject = ['llm', 'agents', 'tools'];
18
- const knownEfforts = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
19
18
  export function apply(ctx, config = {}) {
20
19
  const home = config.home ?? process.env.DSH_HOME ?? resolve(homedir(), '.dsh');
20
+ const registry = config.strategies ? createRegistry(config.strategies) : builtin;
21
21
  const settings = new SettingsStore(resolve(home, 'plugins/dsh-auto-model-routing/config.json'));
22
- const hostSettings = settingsHost(ctx, settings);
22
+ const hostSettings = settingsHost(ctx, settings, registry);
23
23
  const jev = new JevClient({ get: hostSettings.credential }, config.jevBase, config.jevTimeoutMs ?? (() => settings.read().settings.jevTimeoutMs), config.jevBase ? undefined : () => settings.read().settings.decisionService);
24
24
  const stateFiles = new FileStateStore(resolve(home, 'state/auto-model-routing/sessions'));
25
25
  const state = new RoutingState(stateFiles);
26
- const book = new JudgementBook(resolve(home, 'state/auto-model-routing/judgements.jsonl'));
27
- const router = new Router(state, (sessionId, turn, tier, model, effort) => {
28
- const line = { sessionId, turn, tier, ...(effort ? { effort } : {}), model, rubricVersion };
29
- book.judged(line);
30
- // Recording is best effort: a failed append never touches the request or the mode.
31
- try {
32
- book.append(line);
33
- }
34
- catch (error) {
35
- ctx.logger.warn(`auto-model-routing: could not record the judgement: ${String(error)}`);
36
- }
37
- }, () => settings.read().settings);
38
26
  const feedback = new FeedbackBook(resolve(home, 'state/auto-model-routing/allocation-feedback.jsonl'));
39
- const policy = policyHost(ctx, { feedback, jev, state, store: new PolicyStore(resolve(home, 'state/auto-model-routing/policy-v2')), settings: () => settings.read().settings, catalog: () => hostSettings.catalog(true) });
27
+ // Official writer is available only while the session controller is mounted.
28
+ let selectModel;
29
+ 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: () => hostSettings.catalog(), registry });
40
31
  const toastOn = () => settings.read().settings.decisionToast;
41
- ctx.effect(() => ctx.tools.register(routeSubagentTool(jev, { agents: ctx.agents, state, readSettings: () => settings.read().settings, catalog: hostSettings.catalog, judged: (sessionId, value) => router.judged(sessionId, value) })));
32
+ ctx.effect(() => ctx.tools.register(routeSubagentTool(jev, { agents: ctx.agents, state, readSettings: () => settings.read().settings, catalog: hostSettings.catalog, registry })));
42
33
  ctx.on('agent/created', ({ agent, source }) => {
43
34
  // Factory resume announces `resume`; a nonzero initial seed also excludes restored/forked histories.
44
35
  if (source !== 'startup' || agent.session.header.origin === 'subagent' || agent.session.firstLiveSeq !== 0 || agent.session.header.isSeeded || agent.session.snapshotEvents().some(event => event.type === 'session/end-seed') || stateFiles.get(agent.id))
45
36
  return;
46
- if (settings.read().settings.defaultMode === 'AUTO')
47
- state.setMode(agent.id, 'AUTO');
37
+ if (settings.read().settings.defaultMode !== 'AUTO')
38
+ return;
39
+ // A new session whose default main model is outside AUTO's registered families starts in MANUAL.
40
+ if (!mainSupported(currentMainSelection(ctx, agent), registry))
41
+ return;
42
+ state.setMode(agent.id, 'AUTO');
48
43
  return undefined;
49
44
  });
50
45
  // Agent is the WeakMap identity; origin stays on the immutable session header.
@@ -57,69 +52,12 @@ export function apply(ctx, config = {}) {
57
52
  }
58
53
  return value;
59
54
  };
60
- ctx.on('agent/pre-step', async (payload, next) => {
61
- const decision = await next();
62
- // Judge only steps that actually enter, using the messages that enter (a rejected step sends nothing).
63
- if (settings.read().settings.policy === 'policy-v2' || payload.step !== 1 || decision.kind !== 'enter')
64
- return decision;
65
- const agent = ref(payload.agent);
66
- if (agent.origin !== 'subagent' && userInput(decision.messages).userTurn)
67
- book.userTurn(agent.id, payload.turn);
68
- const job = router.begin(agent, payload.turn, decision.messages);
69
- if (!job)
70
- return decision;
71
- // History before this turn's messages enter: the previous user message and assistant reply.
72
- const prior = previousTurn(payload.agent.session.deriveMessages());
73
- const result = await jev.judge(buildState({ previous: job.previous, prevUser: prior.user, prevAssistant: prior.assistant, text: job.text }), payload.signal);
74
- if (result.kind === 'ok') {
75
- const { kind: _, tier, ...measure } = result;
76
- router.resolve(agent, payload.turn, tier, measure);
77
- }
78
- else if (result.kind === 'failed')
79
- router.fail(agent, payload.turn, result.category);
80
- else
81
- router.cancel(agent, payload.turn);
82
- return decision;
83
- });
84
- // Reply → user turn, for the reply-bar tier button (memory only; empty after a restart).
85
- ctx.on('session/event', (session, event) => {
86
- if (event.type !== 'assistant/message')
87
- return;
88
- const data = event.data;
89
- book.assistant(session.id, data.message.id, data.turn);
90
- });
91
- // Official selection writer, set while the session controller is available.
92
- let selectModel;
93
- const inflight = new Map();
94
- ctx.on('agent/request', async (payload, next) => {
95
- const config = await next();
96
- if (settings.read().settings.policy === 'policy-v2' || payload.agent.session.header.origin === 'subagent')
97
- return config;
98
- const model = router.needsModelInfo(ref(payload.agent)) ? await ctx.llm.resolveModelInfo(config.provider, config.model, payload.signal) : undefined;
99
- const efforts = (model?.reasoning?.efforts ?? []).map(effort => effort.id).filter(id => knownEfforts.has(id));
100
- const catalog = model && settings.read().settings.mainEfforts ? await hostSettings.catalog() : undefined;
101
- const routed = router.request(ref(payload.agent), payload.turn, config, efforts, catalog);
102
- // AUTO moved the effort away from the official selection: make that selection (and the picker) carry it.
103
- // `config` is the official selection for this request, so a user pick made since the last sync is caught too.
104
- // Not awaited and never fatal; `inflight` only stops repeat calls before the first one lands.
105
- if (selectModel && routed.reasoningEffort && routed.reasoningEffort !== config.reasoningEffort) {
106
- const key = `${routed.provider}\0${routed.model}\0${routed.reasoningEffort}`;
107
- if (inflight.get(payload.agent.id) !== key) {
108
- inflight.set(payload.agent.id, key);
109
- selectModel({ sessionId: payload.agent.id, provider: routed.provider, model: routed.model, reasoningEffort: routed.reasoningEffort })
110
- .catch(error => ctx.logger.warn(`auto-model-routing: could not sync the model selection: ${String(error)}`))
111
- .finally(() => { if (inflight.get(payload.agent.id) === key)
112
- inflight.delete(payload.agent.id); });
113
- }
114
- }
115
- return routed;
116
- }, { prepend: true });
117
55
  ctx.inject(['sessionController'], child => {
118
56
  selectModel = request => child.sessionController.selectModel(request);
119
57
  child.effect(() => () => { selectModel = undefined; });
120
58
  });
121
59
  ctx.inject(['connection', 'sessionController'], child => {
122
- for (const operation of ['state', 'setMode', 'label', 'settings', 'saveSettings', 'setKey', 'testDecision', 'childRouting']) {
60
+ for (const operation of ['state', 'setMode', 'label', 'observe', 'settings', 'saveSettings', 'setKey', 'testDecision', 'childRouting']) {
123
61
  child.effect(() => child.connection.fetch.register({
124
62
  path: `/api/auto-model-routing.${operation}`,
125
63
  methods: ['POST'], requestBody: 'buffered',
@@ -139,25 +77,52 @@ export function apply(ctx, config = {}) {
139
77
  const result = await child.sessionController.resolveAgent(id);
140
78
  return 'agent' in result ? ref(result.agent) : undefined;
141
79
  };
80
+ const resolveSessionAgent = async (id) => {
81
+ const result = await child.sessionController.resolveAgent(id);
82
+ return 'agent' in result ? result.agent : undefined;
83
+ };
142
84
  const payload = enveloped ? wire.payload : body;
85
+ let manualDecision;
143
86
  if (operation === 'setMode' && typeof payload?.sessionId === 'string') {
144
87
  const id = payload.sessionId;
145
88
  const mode = payload.mode;
146
- if ((mode === 'AUTO' || mode === 'MANUAL') && state.read(id).mode !== mode && await resolveAgent(id))
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 };
147
93
  policy.invalidate(id);
94
+ }
148
95
  }
149
96
  const response = operation === 'childRouting'
150
97
  ? (typeof payload?.sessionId === 'string'
151
- ? Response.json(policy.status(payload.sessionId) ?? { status: 'unsupported', reason: '没有已绑定的 policy-v2 决策' })
98
+ ? Response.json(policy.status(payload.sessionId) ?? { status: 'unsupported', reason: '没有已绑定的子任务决策' })
152
99
  : Response.json({ error: 'invalid request' }, { status: 400 }))
153
100
  : operation === 'settings' || operation === 'saveSettings' || operation === 'setKey' || operation === 'testDecision'
154
101
  ? await hostSettings.endpoint(operation, payload)
155
- : operation === 'label' ? await labelEndpoint(payload, resolveAgent, book, feedback) : await endpoint(operation, payload, resolveAgent, router, state, toastOn);
156
- if ((operation === 'state' || operation === 'setMode') && response.ok && settings.read().settings.policy === 'policy-v2') {
102
+ : 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 endpoint(operation, payload, resolveAgent, state);
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
+ }
157
122
  const value = await response.json();
158
- const main = policy.mainStatus(payload.sessionId);
159
- const { decision: _legacyDecision, effort: _legacyEffort, reason: _legacyReason, history: _legacyHistory, ...base } = value;
160
- const result = { ...base, history: [], policy: 'policy-v2', ...main };
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 } : {}) } };
161
126
  return Response.json(enveloped ? { type: 'server-response', rpcId: wire.rpcId, result: { ok: true, value: result } } : result);
162
127
  }
163
128
  if (!enveloped)
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,33 +1,39 @@
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';
7
8
  export declare function policyHost(ctx: Context, deps: {
9
+ selectModel: (request: {
10
+ sessionId: string;
11
+ provider: string;
12
+ model: string;
13
+ reasoningEffort: string;
14
+ }) => Promise<unknown>;
8
15
  feedback: FeedbackBook;
9
16
  jev: JevClient;
10
17
  state: RoutingState;
11
18
  store: PolicyStore;
12
19
  settings: () => Settings;
13
20
  catalog: () => Promise<RoutingCatalog>;
21
+ registry?: Registry;
14
22
  }): {
23
+ appliedDecision: (id: string) => number | undefined;
15
24
  invalidate: (id: string) => void;
16
25
  status: (id: string) => {
17
- version: 1;
18
26
  childId: string;
19
27
  parentId: string;
20
28
  rootId: string;
21
- policyVersion: string;
22
- mappingVersion: string;
23
29
  status: "failed" | "recommended" | "unsupported" | "needs-context" | "applied";
24
30
  path: "ordinary" | "fork" | "teammate";
25
31
  explicit: {
26
- provider: "claude" | "codex" | "dsh-magpie-connect";
32
+ provider: string;
27
33
  model?: string | undefined;
28
34
  reasoningEffort?: string | undefined;
29
35
  };
30
- family?: "claude" | "codex" | undefined;
36
+ family?: string | undefined;
31
37
  decision?: {
32
38
  id: number;
33
39
  source: "main" | "subagent";
@@ -47,12 +53,12 @@ export declare function policyHost(ctx: Context, deps: {
47
53
  capabilityProbabilities?: Record<string, number> | undefined;
48
54
  } | undefined;
49
55
  route?: {
50
- provider: "claude" | "codex" | "dsh-magpie-connect";
56
+ provider: string;
51
57
  model: string;
52
58
  reasoningEffort: "low" | "medium" | "high" | "xhigh";
53
59
  } | undefined;
54
60
  actual?: {
55
- provider: "claude" | "codex" | "dsh-magpie-connect";
61
+ provider: string;
56
62
  model: string;
57
63
  reasoningEffort: "low" | "medium" | "high" | "xhigh";
58
64
  } | undefined;
@@ -60,7 +66,6 @@ export declare function policyHost(ctx: Context, deps: {
60
66
  } | undefined;
61
67
  mainStatus: (id: string) => {
62
68
  decision?: NonNullable<ChildDecision["decision"]>;
63
- policyVersion?: string;
64
69
  depth?: Depth;
65
70
  effort?: string;
66
71
  reason?: string;