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.
- package/CHANGELOG.md +19 -0
- package/README.md +15 -10
- package/README.zh.md +15 -9
- package/docs/feedback-evaluation.md +20 -3
- package/docs/host-capability-report.md +2 -0
- package/docs/jev-policy-v2-validation.md +3 -1
- package/docs/magpie-live-acceptance.md +2 -0
- package/docs/policy-v2-development.md +5 -3
- package/docs/policy-v2-real-evaluation-plan.md +2 -0
- package/lib/client/control.d.ts +2 -2
- package/lib/client/control.js +10 -20
- package/lib/client/index.d.ts +9 -2
- package/lib/client/index.js +97 -12
- package/lib/client/label.d.ts +8 -11
- package/lib/client/label.js +51 -29
- package/lib/client/settings-section.d.ts +2 -2
- package/lib/client/settings-section.js +7 -69
- package/lib/client/strategy-details.js +3 -4
- package/lib/client.js +316 -489
- package/lib/context.d.ts +1 -3
- package/lib/context.js +3 -4
- package/lib/effort.d.ts +2 -10
- package/lib/effort.js +1 -31
- package/lib/endpoints.d.ts +11 -9
- package/lib/endpoints.js +44 -42
- package/lib/feedback.d.ts +77 -6
- package/lib/feedback.js +34 -3
- package/lib/index.d.ts +4 -0
- package/lib/index.js +54 -89
- package/lib/jev.d.ts +17 -29
- package/lib/jev.js +6 -34
- package/lib/policy-host.d.ts +13 -8
- package/lib/policy-host.js +98 -47
- package/lib/policy-store.d.ts +9 -17
- package/lib/policy-store.js +28 -15
- package/lib/routing.d.ts +20 -0
- package/lib/routing.js +51 -0
- package/lib/rubric.json +15 -6
- package/lib/settings-contract.d.ts +5 -21
- package/lib/settings-contract.js +3 -2
- package/lib/settings-host.d.ts +4 -8
- package/lib/settings-host.js +9 -9
- package/lib/settings-validation.d.ts +1 -7
- package/lib/settings-validation.js +0 -51
- package/lib/settings.d.ts +3 -8
- package/lib/settings.js +18 -177
- package/lib/state.d.ts +2 -4
- package/lib/state.js +2 -7
- package/lib/strategies/claude.d.ts +21 -0
- package/lib/strategies/claude.js +8 -0
- package/lib/strategies/codex.d.ts +21 -0
- package/lib/strategies/codex.js +9 -0
- package/lib/strategies/index.d.ts +22 -0
- package/lib/strategies/index.js +85 -0
- package/lib/strategies/types.d.ts +14 -0
- package/lib/strategies/types.js +1 -0
- package/lib/subagent-tool.d.ts +7 -7
- package/lib/subagent-tool.js +33 -76
- package/lib/support.d.ts +24 -0
- package/lib/support.js +36 -0
- package/lib/usage-report.d.ts +21 -2
- package/lib/usage-report.js +86 -22
- package/package.json +19 -7
- package/skills/auto-model-routing-report/SKILL.md +9 -3
- package/skills/subagent-model-routing/SKILL.md +14 -30
- package/lib/client/route-picker.d.ts +0 -21
- package/lib/client/route-picker.js +0 -75
- package/lib/judgements.d.ts +0 -41
- package/lib/judgements.js +0 -55
- package/lib/policy-v2-rubric.json +0 -21
- package/lib/policy-v2.d.ts +0 -20
- package/lib/policy-v2.js +0 -53
- package/lib/router.d.ts +0 -68
- package/lib/router.js +0 -153
- 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 {
|
|
7
|
-
import { endpoint, labelEndpoint } from './endpoints.js';
|
|
6
|
+
import { endpoint, labelEndpoint, observeEndpoint } from './endpoints.js';
|
|
8
7
|
import { FeedbackBook } from './feedback.js';
|
|
9
|
-
import {
|
|
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
|
-
|
|
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,
|
|
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
|
|
47
|
-
|
|
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: '
|
|
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 === '
|
|
156
|
-
|
|
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(
|
|
159
|
-
const {
|
|
160
|
-
const result = { ...
|
|
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
|
-
/**
|
|
48
|
-
export declare const
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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
|
-
/**
|
|
12
|
-
export const
|
|
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 =
|
|
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 >
|
|
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 {
|
package/lib/policy-host.d.ts
CHANGED
|
@@ -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:
|
|
32
|
+
provider: string;
|
|
27
33
|
model?: string | undefined;
|
|
28
34
|
reasoningEffort?: string | undefined;
|
|
29
35
|
};
|
|
30
|
-
family?:
|
|
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:
|
|
56
|
+
provider: string;
|
|
51
57
|
model: string;
|
|
52
58
|
reasoningEffort: "low" | "medium" | "high" | "xhigh";
|
|
53
59
|
} | undefined;
|
|
54
60
|
actual?: {
|
|
55
|
-
provider:
|
|
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;
|