sneakoscope 10.2.0 → 10.3.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/README.md +6 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/command-manifest-lite.js +3 -1
- package/dist/cli/command-registry.js +9 -1
- package/dist/cli/install-helpers-menubar.js +57 -0
- package/dist/cli/install-helpers.js +10 -1
- package/dist/commands/decision.js +7 -0
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/agents/native-worker-backend-router.js +7 -2
- package/dist/core/codex-app/menubar/constants.js +2 -0
- package/dist/core/decisions/cli.js +324 -0
- package/dist/core/decisions/config.js +109 -0
- package/dist/core/decisions/evaluation.js +62 -0
- package/dist/core/decisions/integration.js +262 -0
- package/dist/core/decisions/openrouter.js +270 -0
- package/dist/core/decisions/policy.js +393 -0
- package/dist/core/decisions/questions.js +215 -0
- package/dist/core/decisions/receipt.js +41 -0
- package/dist/core/decisions/recovery.js +39 -0
- package/dist/core/decisions/routing.js +58 -0
- package/dist/core/decisions/state.js +173 -0
- package/dist/core/decisions/types.js +69 -0
- package/dist/core/feature-fixtures.js +1 -0
- package/dist/core/feature-registry.js +1 -0
- package/dist/core/hooks-runtime/subagent-context.js +5 -3
- package/dist/core/hooks-runtime/subagent-spawn-policy.js +9 -3
- package/dist/core/provider/model-router.js +3 -3
- package/dist/core/routes.js +2 -1
- package/dist/core/subagents/model-policy.js +15 -0
- package/dist/core/subagents/official-subagent-lock.js +15 -0
- package/dist/core/subagents/official-subagent-preparation.js +265 -24
- package/dist/core/subagents/official-subagent-prompt.js +56 -13
- package/dist/core/update/update-migration-state/retired-local-decision.js +203 -0
- package/dist/core/update/update-migration-state.js +6 -0
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +6 -3
- package/dist/native/sks-menubar/Sources/LocalDecisionModels.swift +166 -0
- package/dist/native/sks-menubar/Sources/LocalDecisionViewController.swift +200 -0
- package/dist/native/sks-menubar/Sources/OverviewViewController.swift +4 -0
- package/dist/native/sks-menubar/Sources/SidebarItem.swift +2 -0
- package/dist/scripts/current-surface-update-e2e-check.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { CHOICE_MIN_CONFIDENCE, CHOICE_MIN_PROBABILITY, CONTEXT_DROP_NOUL_MAX, DISTRIBUTION_SUM_TOLERANCE, KEEP_BASELINE_CHOICE, NEEDS_EVIDENCE_CHOICE, POLICY_REVISION, ROUTING_RISK_NOUL_MIN, UNKNOWN_USAGE, sealedRoutingModel } from './types.js';
|
|
2
|
+
export function compileDecision(bundle, response) {
|
|
3
|
+
const usage = usageFromWire(response.usage);
|
|
4
|
+
const decoded = decodeWireResponse(bundle, response);
|
|
5
|
+
if (!decoded.ok) {
|
|
6
|
+
return { kind: 'keep_baseline', binding: bundle.binding, reason: decoded.reason, usage };
|
|
7
|
+
}
|
|
8
|
+
const effects = [];
|
|
9
|
+
let fallbackReason = null;
|
|
10
|
+
const planBinding = Object.entries(bundle.questionBindings).find(([, binding]) => binding.kind === 'plan');
|
|
11
|
+
if (planBinding) {
|
|
12
|
+
const compiled = compilePlan(bundle, decoded.response.answers[planBinding[0]]);
|
|
13
|
+
if (compiled.kind === 'effect')
|
|
14
|
+
effects.push(compiled.effect);
|
|
15
|
+
else if (!fallbackReason)
|
|
16
|
+
fallbackReason = compiled.reason;
|
|
17
|
+
}
|
|
18
|
+
const routing = compileRouting(bundle, decoded.response.answers);
|
|
19
|
+
for (const effect of routing.effects)
|
|
20
|
+
effects.push(effect);
|
|
21
|
+
if (routing.reason && !fallbackReason)
|
|
22
|
+
fallbackReason = routing.reason;
|
|
23
|
+
const contextEffect = compileContext(bundle, decoded.response.answers);
|
|
24
|
+
if (contextEffect.kind === 'effect')
|
|
25
|
+
effects.push(contextEffect.effect);
|
|
26
|
+
else if (contextEffect.reason && !fallbackReason)
|
|
27
|
+
fallbackReason = contextEffect.reason;
|
|
28
|
+
const recoveryBinding = Object.entries(bundle.questionBindings).find(([, binding]) => binding.kind === 'recovery');
|
|
29
|
+
if (recoveryBinding) {
|
|
30
|
+
const compiled = compileRecovery(bundle, decoded.response.answers[recoveryBinding[0]]);
|
|
31
|
+
if (compiled.kind === 'effect')
|
|
32
|
+
effects.push(compiled.effect);
|
|
33
|
+
else if (!fallbackReason)
|
|
34
|
+
fallbackReason = compiled.reason;
|
|
35
|
+
}
|
|
36
|
+
if (effects.length === 0) {
|
|
37
|
+
return {
|
|
38
|
+
kind: 'keep_baseline',
|
|
39
|
+
binding: bundle.binding,
|
|
40
|
+
reason: fallbackReason || 'keep_baseline_selected',
|
|
41
|
+
usage
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
kind: 'apply',
|
|
46
|
+
binding: bundle.binding,
|
|
47
|
+
resolvedModel: decoded.response.model,
|
|
48
|
+
effects: effects,
|
|
49
|
+
usage
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function decodeWireResponse(bundle, response) {
|
|
53
|
+
if (!isRecord(response))
|
|
54
|
+
return { ok: false, reason: 'invalid_response' };
|
|
55
|
+
if (hasOwn(response, 'inputTokens') && !hasOwn(response, 'usage')) {
|
|
56
|
+
return { ok: false, reason: 'invalid_response' };
|
|
57
|
+
}
|
|
58
|
+
const model = text(response.model);
|
|
59
|
+
if (!model)
|
|
60
|
+
return { ok: false, reason: 'invalid_response' };
|
|
61
|
+
if (model !== bundle.binding.requestedModel)
|
|
62
|
+
return { ok: false, reason: 'unknown_model' };
|
|
63
|
+
const answersRaw = response.answers;
|
|
64
|
+
if (!isRecord(answersRaw))
|
|
65
|
+
return { ok: false, reason: 'invalid_response' };
|
|
66
|
+
const usage = decodeUsage(response.usage);
|
|
67
|
+
if (!usage)
|
|
68
|
+
return { ok: false, reason: 'invalid_response' };
|
|
69
|
+
const answers = {};
|
|
70
|
+
for (const [id, question] of Object.entries(bundle.request.questions)) {
|
|
71
|
+
const rawAnswer = answersRaw[id];
|
|
72
|
+
if (rawAnswer === undefined) {
|
|
73
|
+
if (isRequiredQuestion(bundle, id))
|
|
74
|
+
return { ok: false, reason: 'missing_answer' };
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const decoded = decodeAnswer(question, rawAnswer);
|
|
78
|
+
if (!decoded.ok)
|
|
79
|
+
return { ok: false, reason: decoded.reason };
|
|
80
|
+
answers[id] = decoded.answer;
|
|
81
|
+
}
|
|
82
|
+
const decoded = {
|
|
83
|
+
model,
|
|
84
|
+
answers,
|
|
85
|
+
usage
|
|
86
|
+
};
|
|
87
|
+
const id = text(response.id);
|
|
88
|
+
if (id)
|
|
89
|
+
decoded.id = id;
|
|
90
|
+
const provider = text(response.provider);
|
|
91
|
+
if (provider)
|
|
92
|
+
decoded.provider = provider;
|
|
93
|
+
return { ok: true, response: decoded };
|
|
94
|
+
}
|
|
95
|
+
export function usageFromWire(usage) {
|
|
96
|
+
if (!usage)
|
|
97
|
+
return { ...UNKNOWN_USAGE };
|
|
98
|
+
return {
|
|
99
|
+
inputTokens: usage.input_tokens,
|
|
100
|
+
outputTokens: usage.output_tokens,
|
|
101
|
+
reportedCost: usage.cost === undefined ? null : usage.cost,
|
|
102
|
+
evidence: 'provider_response'
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function decodeUsage(value) {
|
|
106
|
+
if (!isRecord(value))
|
|
107
|
+
return null;
|
|
108
|
+
if (hasOwn(value, 'inputTokens') && !hasOwn(value, 'input_tokens'))
|
|
109
|
+
return null;
|
|
110
|
+
if (hasOwn(value, 'outputTokens') && !hasOwn(value, 'output_tokens'))
|
|
111
|
+
return null;
|
|
112
|
+
const inputTokens = value.input_tokens;
|
|
113
|
+
const outputTokens = value.output_tokens;
|
|
114
|
+
if (!isFiniteNumber(inputTokens) || inputTokens < 0)
|
|
115
|
+
return null;
|
|
116
|
+
if (!isFiniteNumber(outputTokens) || outputTokens < 0)
|
|
117
|
+
return null;
|
|
118
|
+
const usage = {
|
|
119
|
+
input_tokens: inputTokens,
|
|
120
|
+
output_tokens: outputTokens
|
|
121
|
+
};
|
|
122
|
+
if (hasOwn(value, 'cost')) {
|
|
123
|
+
if (!isFiniteNumber(value.cost) || value.cost < 0)
|
|
124
|
+
return null;
|
|
125
|
+
usage.cost = value.cost;
|
|
126
|
+
}
|
|
127
|
+
return usage;
|
|
128
|
+
}
|
|
129
|
+
function compilePlan(bundle, answer) {
|
|
130
|
+
if (!answer)
|
|
131
|
+
return { kind: 'baseline', reason: 'missing_answer' };
|
|
132
|
+
if (answer.type !== 'choice')
|
|
133
|
+
return { kind: 'baseline', reason: 'invalid_response' };
|
|
134
|
+
if (answer.choice === KEEP_BASELINE_CHOICE)
|
|
135
|
+
return { kind: 'baseline', reason: 'keep_baseline_selected' };
|
|
136
|
+
const candidate = bundle.planCandidates.find((row) => row.id === answer.choice);
|
|
137
|
+
if (!candidate)
|
|
138
|
+
return { kind: 'baseline', reason: 'invalid_response' };
|
|
139
|
+
const uncertainty = requiredChoiceUncertainty(answer, planChoiceIds(bundle));
|
|
140
|
+
if (!uncertainty.ok)
|
|
141
|
+
return { kind: 'baseline', reason: uncertainty.reason };
|
|
142
|
+
return { kind: 'effect', effect: { kind: 'select_plan', planId: candidate.id } };
|
|
143
|
+
}
|
|
144
|
+
function compileRouting(bundle, answers) {
|
|
145
|
+
const effects = [];
|
|
146
|
+
let reason = null;
|
|
147
|
+
for (const roleId of routingRoleIds(bundle)) {
|
|
148
|
+
const compiled = compileRoleRouting(bundle, answers, roleId);
|
|
149
|
+
if (compiled.kind === 'effect')
|
|
150
|
+
effects.push(compiled.effect);
|
|
151
|
+
else if (!reason)
|
|
152
|
+
reason = compiled.reason;
|
|
153
|
+
}
|
|
154
|
+
return { effects, reason };
|
|
155
|
+
}
|
|
156
|
+
function compileRoleRouting(bundle, answers, roleId) {
|
|
157
|
+
const choiceId = questionIdFor(bundle, 'routing', roleId);
|
|
158
|
+
const answer = choiceId ? answers[choiceId] : undefined;
|
|
159
|
+
if (!answer)
|
|
160
|
+
return { kind: 'baseline', reason: 'missing_answer' };
|
|
161
|
+
if (answer.type !== 'choice')
|
|
162
|
+
return { kind: 'baseline', reason: 'invalid_response' };
|
|
163
|
+
if (answer.choice === KEEP_BASELINE_CHOICE)
|
|
164
|
+
return { kind: 'baseline', reason: 'keep_baseline_selected' };
|
|
165
|
+
const selected = sealedRoutingModel(answer.choice);
|
|
166
|
+
if (!selected)
|
|
167
|
+
return { kind: 'baseline', reason: 'invalid_response' };
|
|
168
|
+
const choiceQuestion = choiceId ? bundle.request.questions[choiceId] : undefined;
|
|
169
|
+
const labels = choiceQuestion?.type === 'choice' ? Object.keys(choiceQuestion.criteria) : [];
|
|
170
|
+
const uncertainty = requiredChoiceUncertainty(answer, labels);
|
|
171
|
+
if (!uncertainty.ok)
|
|
172
|
+
return { kind: 'baseline', reason: uncertainty.reason };
|
|
173
|
+
const model = escalateRole(bundle, answers, roleId) ? 'gpt-6-astra' : selected.id;
|
|
174
|
+
return { kind: 'effect', effect: { kind: 'select_routing', roleId, model } };
|
|
175
|
+
}
|
|
176
|
+
function escalateRole(bundle, answers, roleId) {
|
|
177
|
+
const difficultyId = questionIdFor(bundle, 'routing_difficulty', roleId);
|
|
178
|
+
const difficulty = difficultyId ? answers[difficultyId] : undefined;
|
|
179
|
+
const difficultyQuestion = difficultyId ? bundle.request.questions[difficultyId] : undefined;
|
|
180
|
+
if (difficulty?.type === 'score' && difficultyQuestion?.type === 'score') {
|
|
181
|
+
const top = difficultyQuestion.criteria.length - 1;
|
|
182
|
+
if (difficulty.score >= top)
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
const riskId = questionIdFor(bundle, 'routing_risk', roleId);
|
|
186
|
+
const risk = riskId ? answers[riskId] : undefined;
|
|
187
|
+
return risk?.type === 'noul' && risk.noul >= ROUTING_RISK_NOUL_MIN;
|
|
188
|
+
}
|
|
189
|
+
function routingRoleIds(bundle) {
|
|
190
|
+
const ids = [];
|
|
191
|
+
for (const binding of Object.values(bundle.questionBindings)) {
|
|
192
|
+
if (binding.kind === 'routing' && !ids.includes(binding.roleId))
|
|
193
|
+
ids.push(binding.roleId);
|
|
194
|
+
}
|
|
195
|
+
return ids;
|
|
196
|
+
}
|
|
197
|
+
function questionIdFor(bundle, kind, roleId) {
|
|
198
|
+
const found = Object.entries(bundle.questionBindings).find(([, binding]) => (binding.kind === kind && 'roleId' in binding && binding.roleId === roleId));
|
|
199
|
+
return found ? found[0] : null;
|
|
200
|
+
}
|
|
201
|
+
function compileContext(bundle, answers) {
|
|
202
|
+
const optional = bundle.contextCandidates.filter((row) => !row.pinned);
|
|
203
|
+
if (optional.length === 0)
|
|
204
|
+
return { kind: 'baseline', reason: null };
|
|
205
|
+
const keepIds = new Set(bundle.contextCandidates.filter((row) => row.pinned).map((row) => row.id));
|
|
206
|
+
let sawUsable = false;
|
|
207
|
+
for (const [questionId, binding] of Object.entries(bundle.questionBindings)) {
|
|
208
|
+
if (binding.kind !== 'context_keep')
|
|
209
|
+
continue;
|
|
210
|
+
const candidate = bundle.contextCandidates.find((row) => row.id === binding.candidateId);
|
|
211
|
+
if (!candidate || candidate.pinned)
|
|
212
|
+
continue;
|
|
213
|
+
const answer = answers[questionId];
|
|
214
|
+
if (!answer)
|
|
215
|
+
continue;
|
|
216
|
+
if (answer.type !== 'noul')
|
|
217
|
+
return { kind: 'baseline', reason: 'invalid_response' };
|
|
218
|
+
if (!isFiniteNumber(answer.noul) || answer.noul < 0 || answer.noul > 1) {
|
|
219
|
+
return { kind: 'baseline', reason: 'invalid_response' };
|
|
220
|
+
}
|
|
221
|
+
sawUsable = true;
|
|
222
|
+
if (canDropOptional(candidate, answer.noul))
|
|
223
|
+
continue;
|
|
224
|
+
keepIds.add(candidate.id);
|
|
225
|
+
}
|
|
226
|
+
for (const candidate of optional) {
|
|
227
|
+
if (!Object.values(bundle.questionBindings).some((binding) => (binding.kind === 'context_keep' && binding.candidateId === candidate.id))) {
|
|
228
|
+
keepIds.add(candidate.id);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (!sawUsable)
|
|
232
|
+
return { kind: 'baseline', reason: null };
|
|
233
|
+
return { kind: 'effect', effect: { kind: 'select_optional_context', keepIds: [...keepIds] } };
|
|
234
|
+
}
|
|
235
|
+
function compileRecovery(bundle, answer) {
|
|
236
|
+
if (!answer)
|
|
237
|
+
return { kind: 'baseline', reason: 'missing_answer' };
|
|
238
|
+
if (answer.type !== 'choice')
|
|
239
|
+
return { kind: 'baseline', reason: 'invalid_response' };
|
|
240
|
+
if (answer.choice === KEEP_BASELINE_CHOICE)
|
|
241
|
+
return { kind: 'baseline', reason: 'keep_baseline_selected' };
|
|
242
|
+
if (answer.choice === NEEDS_EVIDENCE_CHOICE)
|
|
243
|
+
return { kind: 'baseline', reason: 'needs_evidence' };
|
|
244
|
+
const candidate = bundle.recoveryCandidates.find((row) => row.id === answer.choice && row.authorized);
|
|
245
|
+
if (!candidate)
|
|
246
|
+
return { kind: 'baseline', reason: 'invalid_response' };
|
|
247
|
+
const labels = [
|
|
248
|
+
...bundle.recoveryCandidates.map((row) => row.id),
|
|
249
|
+
KEEP_BASELINE_CHOICE,
|
|
250
|
+
NEEDS_EVIDENCE_CHOICE
|
|
251
|
+
];
|
|
252
|
+
const uncertainty = requiredChoiceUncertainty(answer, labels);
|
|
253
|
+
if (!uncertainty.ok)
|
|
254
|
+
return { kind: 'baseline', reason: uncertainty.reason };
|
|
255
|
+
return { kind: 'effect', effect: { kind: 'dispatch_recovery', actionId: candidate.id } };
|
|
256
|
+
}
|
|
257
|
+
function canDropOptional(candidate, noul) {
|
|
258
|
+
return !candidate.pinned
|
|
259
|
+
&& candidate.fresh
|
|
260
|
+
&& candidate.reproducible
|
|
261
|
+
&& Boolean(candidate.excerpt)
|
|
262
|
+
&& noul <= CONTEXT_DROP_NOUL_MAX;
|
|
263
|
+
}
|
|
264
|
+
function requiredChoiceUncertainty(answer, expectedKeys) {
|
|
265
|
+
if (answer.confidence === undefined || answer.probabilities === undefined) {
|
|
266
|
+
return { ok: false, reason: 'insufficient_uncertainty' };
|
|
267
|
+
}
|
|
268
|
+
if (!isFiniteNumber(answer.confidence) || answer.confidence < 0 || answer.confidence > 1) {
|
|
269
|
+
return { ok: false, reason: 'invalid_response' };
|
|
270
|
+
}
|
|
271
|
+
const distribution = validateDistribution(answer.probabilities, expectedKeys);
|
|
272
|
+
if (!distribution.ok)
|
|
273
|
+
return distribution;
|
|
274
|
+
const selected = answer.probabilities[answer.choice];
|
|
275
|
+
if (!isFiniteNumber(selected))
|
|
276
|
+
return { ok: false, reason: 'invalid_response' };
|
|
277
|
+
if (answer.confidence < CHOICE_MIN_CONFIDENCE || selected < CHOICE_MIN_PROBABILITY) {
|
|
278
|
+
return { ok: false, reason: 'uncertain' };
|
|
279
|
+
}
|
|
280
|
+
return { ok: true };
|
|
281
|
+
}
|
|
282
|
+
export function validateDistribution(probabilities, expectedKeys) {
|
|
283
|
+
const keys = Object.keys(probabilities);
|
|
284
|
+
if (keys.length !== expectedKeys.length)
|
|
285
|
+
return { ok: false, reason: 'invalid_response' };
|
|
286
|
+
const expected = new Set(expectedKeys);
|
|
287
|
+
let sum = 0;
|
|
288
|
+
for (const key of keys) {
|
|
289
|
+
if (!expected.has(key))
|
|
290
|
+
return { ok: false, reason: 'invalid_response' };
|
|
291
|
+
const value = probabilities[key];
|
|
292
|
+
if (!isFiniteNumber(value) || value < 0 || value > 1)
|
|
293
|
+
return { ok: false, reason: 'invalid_response' };
|
|
294
|
+
sum += value;
|
|
295
|
+
}
|
|
296
|
+
if (Math.abs(sum - 1) > DISTRIBUTION_SUM_TOLERANCE)
|
|
297
|
+
return { ok: false, reason: 'invalid_response' };
|
|
298
|
+
return { ok: true };
|
|
299
|
+
}
|
|
300
|
+
function decodeAnswer(question, raw) {
|
|
301
|
+
if (!isRecord(raw))
|
|
302
|
+
return { ok: false, reason: 'invalid_response' };
|
|
303
|
+
const type = text(raw.type);
|
|
304
|
+
if (type !== question.type) {
|
|
305
|
+
if (type === 'choice' || type === 'noul' || type === 'score') {
|
|
306
|
+
return { ok: false, reason: 'invalid_response' };
|
|
307
|
+
}
|
|
308
|
+
return { ok: false, reason: 'invalid_response' };
|
|
309
|
+
}
|
|
310
|
+
if (question.type === 'choice') {
|
|
311
|
+
const choice = text(raw.choice);
|
|
312
|
+
if (!choice)
|
|
313
|
+
return { ok: false, reason: 'invalid_response' };
|
|
314
|
+
const allowed = new Set([...Object.keys(question.criteria), KEEP_BASELINE_CHOICE, NEEDS_EVIDENCE_CHOICE]);
|
|
315
|
+
if (!allowed.has(choice))
|
|
316
|
+
return { ok: false, reason: 'invalid_response' };
|
|
317
|
+
const answer = { type: 'choice', choice };
|
|
318
|
+
if (hasOwn(raw, 'confidence')) {
|
|
319
|
+
if (!isFiniteNumber(raw.confidence) || raw.confidence < 0 || raw.confidence > 1) {
|
|
320
|
+
return { ok: false, reason: 'invalid_response' };
|
|
321
|
+
}
|
|
322
|
+
answer.confidence = raw.confidence;
|
|
323
|
+
}
|
|
324
|
+
if (hasOwn(raw, 'probabilities')) {
|
|
325
|
+
if (!isRecord(raw.probabilities))
|
|
326
|
+
return { ok: false, reason: 'invalid_response' };
|
|
327
|
+
const probabilities = {};
|
|
328
|
+
for (const [key, value] of Object.entries(raw.probabilities)) {
|
|
329
|
+
if (!isFiniteNumber(value) || value < 0 || value > 1)
|
|
330
|
+
return { ok: false, reason: 'invalid_response' };
|
|
331
|
+
probabilities[key] = value;
|
|
332
|
+
}
|
|
333
|
+
const expected = Object.keys(question.criteria);
|
|
334
|
+
const checked = validateDistribution(probabilities, expected);
|
|
335
|
+
if (!checked.ok)
|
|
336
|
+
return checked;
|
|
337
|
+
answer.probabilities = probabilities;
|
|
338
|
+
}
|
|
339
|
+
return { ok: true, answer };
|
|
340
|
+
}
|
|
341
|
+
if (question.type === 'noul') {
|
|
342
|
+
if (!isFiniteNumber(raw.noul) || raw.noul < 0 || raw.noul > 1) {
|
|
343
|
+
return { ok: false, reason: 'invalid_response' };
|
|
344
|
+
}
|
|
345
|
+
return { ok: true, answer: { type: 'noul', noul: raw.noul } };
|
|
346
|
+
}
|
|
347
|
+
if (!isFiniteNumber(raw.score) || raw.score < 0 || raw.score > question.criteria.length - 1) {
|
|
348
|
+
return { ok: false, reason: 'invalid_response' };
|
|
349
|
+
}
|
|
350
|
+
const answer = { type: 'score', score: raw.score };
|
|
351
|
+
if (hasOwn(raw, 'confidence')) {
|
|
352
|
+
if (!isFiniteNumber(raw.confidence) || raw.confidence < 0 || raw.confidence > 1) {
|
|
353
|
+
return { ok: false, reason: 'invalid_response' };
|
|
354
|
+
}
|
|
355
|
+
answer.confidence = raw.confidence;
|
|
356
|
+
}
|
|
357
|
+
if (hasOwn(raw, 'probabilities')) {
|
|
358
|
+
if (!isRecord(raw.probabilities))
|
|
359
|
+
return { ok: false, reason: 'invalid_response' };
|
|
360
|
+
const probabilities = {};
|
|
361
|
+
for (const [key, value] of Object.entries(raw.probabilities)) {
|
|
362
|
+
if (!isFiniteNumber(value) || value < 0 || value > 1)
|
|
363
|
+
return { ok: false, reason: 'invalid_response' };
|
|
364
|
+
probabilities[key] = value;
|
|
365
|
+
}
|
|
366
|
+
const expected = question.criteria.map((_, index) => String(index));
|
|
367
|
+
const checked = validateDistribution(probabilities, expected);
|
|
368
|
+
if (!checked.ok)
|
|
369
|
+
return checked;
|
|
370
|
+
answer.probabilities = probabilities;
|
|
371
|
+
}
|
|
372
|
+
return { ok: true, answer };
|
|
373
|
+
}
|
|
374
|
+
function planChoiceIds(bundle) {
|
|
375
|
+
return [...bundle.planCandidates.map((row) => row.id), KEEP_BASELINE_CHOICE];
|
|
376
|
+
}
|
|
377
|
+
function isRequiredQuestion(bundle, id) {
|
|
378
|
+
const binding = bundle.questionBindings[id];
|
|
379
|
+
return binding?.kind === 'plan' || binding?.kind === 'recovery' || binding?.kind === 'context_keep';
|
|
380
|
+
}
|
|
381
|
+
function isRecord(value) {
|
|
382
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
383
|
+
}
|
|
384
|
+
function hasOwn(value, key) {
|
|
385
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
386
|
+
}
|
|
387
|
+
function isFiniteNumber(value) {
|
|
388
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
389
|
+
}
|
|
390
|
+
function text(value) {
|
|
391
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
392
|
+
}
|
|
393
|
+
export const COMPILER_POLICY_REVISION = POLICY_REVISION;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { CONTEXT_RELEVANCE_RUBRIC, DESIGN_DEFAULTS, KEEP_BASELINE_CHOICE, NEEDS_EVIDENCE_CHOICE, QUESTION_REVISION, ROUTING_DIFFICULTY_RUBRIC, SEALED_ROUTING_MODELS } from './types.js';
|
|
2
|
+
import { buildDecisionBinding, redactDecisionText } from './state.js';
|
|
3
|
+
const SAME_BATCH_DEPENDENCY = /\b(selected above|answer above|previous answer|the (plan|action) selected|if the (plan|action) selected)\b/i;
|
|
4
|
+
export const CONTEXT_RELEVANCE_LEVELS = CONTEXT_RELEVANCE_RUBRIC.length;
|
|
5
|
+
export function assertIndependentQuestion(instructions) {
|
|
6
|
+
if (SAME_BATCH_DEPENDENCY.test(instructions)) {
|
|
7
|
+
throw new Error('question_depends_on_same_batch_answer');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function validatePlanCoverage(requiredSliceIds, candidate, allowlistedDuplicates = []) {
|
|
11
|
+
const required = [...requiredSliceIds].sort();
|
|
12
|
+
const covered = [...candidate.sliceIds].sort();
|
|
13
|
+
const issues = [];
|
|
14
|
+
if (required.join('\0') !== covered.join('\0'))
|
|
15
|
+
issues.push('slice_coverage_mismatch');
|
|
16
|
+
const seen = new Map();
|
|
17
|
+
for (const group of candidate.groups) {
|
|
18
|
+
for (const id of group.sliceIds) {
|
|
19
|
+
seen.set(id, (seen.get(id) || 0) + 1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const grouped = [...seen.keys()].sort();
|
|
23
|
+
if (grouped.join('\0') !== required.join('\0'))
|
|
24
|
+
issues.push('group_coverage_mismatch');
|
|
25
|
+
for (const [id, count] of seen) {
|
|
26
|
+
if (count > 1 && !allowlistedDuplicates.includes(id))
|
|
27
|
+
issues.push(`duplicate_slice:${id}`);
|
|
28
|
+
}
|
|
29
|
+
return issues;
|
|
30
|
+
}
|
|
31
|
+
export function buildDecisionBundle(input) {
|
|
32
|
+
const planCandidates = [...(input.planCandidates || [])];
|
|
33
|
+
const contextCandidates = [...(input.contextCandidates || [])];
|
|
34
|
+
const recoveryCandidates = [...(input.recoveryCandidates || [])];
|
|
35
|
+
const routingCandidates = [...(input.routingCandidates || [])];
|
|
36
|
+
const questions = {};
|
|
37
|
+
const questionBindings = {};
|
|
38
|
+
const recoverySlot = recoveryCandidates.length > 0 ? 1 : 0;
|
|
39
|
+
if (planCandidates.length > 1) {
|
|
40
|
+
const instructions = redactDecisionText(`Choose an already valid plan in state.plans for the supplied task and slices. Prefer preserving quality and low elapsed time; do not remove required work. Select ${KEEP_BASELINE_CHOICE} when the supplied evidence does not distinguish the plans. Required slice IDs: ${(input.requiredSliceIds || []).join(', ') || 'none'}.`);
|
|
41
|
+
assertIndependentQuestion(instructions);
|
|
42
|
+
questions.plan = {
|
|
43
|
+
type: 'choice',
|
|
44
|
+
instructions,
|
|
45
|
+
criteria: Object.fromEntries([
|
|
46
|
+
...planCandidates.map((plan) => [plan.id, plan.summary]),
|
|
47
|
+
[KEEP_BASELINE_CHOICE, 'Evidence is insufficient to justify changing the baseline.']
|
|
48
|
+
])
|
|
49
|
+
};
|
|
50
|
+
questionBindings.plan = { kind: 'plan' };
|
|
51
|
+
}
|
|
52
|
+
const routedRoles = appendRoutingChoices(routingCandidates, questions, questionBindings, recoverySlot);
|
|
53
|
+
for (const candidate of contextCandidates.filter((row) => !row.pinned && row.excerpt)) {
|
|
54
|
+
if (Object.keys(questions).length + 2 > DESIGN_DEFAULTS.maxQuestions - recoverySlot)
|
|
55
|
+
break;
|
|
56
|
+
const keepId = `keep_${candidate.id}`;
|
|
57
|
+
const keepInstructions = redactDecisionText(`Is the original excerpt at context.${candidate.id} (source ${candidate.sourcePath}) needed for any requirement of the stated task? Use the excerpt content; do not infer relevance from the ID.`);
|
|
58
|
+
assertIndependentQuestion(keepInstructions);
|
|
59
|
+
questions[keepId] = {
|
|
60
|
+
type: 'noul',
|
|
61
|
+
instructions: keepInstructions,
|
|
62
|
+
criteria: {
|
|
63
|
+
true: 'The excerpt contains task-relevant requirements or evidence.',
|
|
64
|
+
false: 'The excerpt contains no information needed for the requested work.'
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
questionBindings[keepId] = { kind: 'context_keep', candidateId: candidate.id };
|
|
68
|
+
const scoreId = `relevance_${candidate.id}`;
|
|
69
|
+
const scoreInstructions = redactDecisionText(`Rate the usefulness of context.${candidate.id}.excerpt from ${candidate.sourcePath} for the concrete objective, using the same task-specific rubric used for other optional evidence.`);
|
|
70
|
+
assertIndependentQuestion(scoreInstructions);
|
|
71
|
+
questions[scoreId] = {
|
|
72
|
+
type: 'score',
|
|
73
|
+
instructions: scoreInstructions,
|
|
74
|
+
criteria: [...CONTEXT_RELEVANCE_RUBRIC]
|
|
75
|
+
};
|
|
76
|
+
questionBindings[scoreId] = {
|
|
77
|
+
kind: 'context_relevance',
|
|
78
|
+
candidateId: candidate.id,
|
|
79
|
+
levelCount: CONTEXT_RELEVANCE_LEVELS
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
appendRoutingSpeculation(routedRoles, questions, questionBindings, recoverySlot);
|
|
83
|
+
if (recoveryCandidates.length > 0 && Object.keys(questions).length < DESIGN_DEFAULTS.maxQuestions) {
|
|
84
|
+
const instructions = redactDecisionText(`Choose one already authorized recovery handler for the observed diagnostic. ${input.recoveryDiagnostic || ''} Select ${KEEP_BASELINE_CHOICE} to keep the current handler path, or ${NEEDS_EVIDENCE_CHOICE} when more evidence is required. Do not invent a command or path.`);
|
|
85
|
+
assertIndependentQuestion(instructions);
|
|
86
|
+
questions.recovery = {
|
|
87
|
+
type: 'choice',
|
|
88
|
+
instructions,
|
|
89
|
+
criteria: Object.fromEntries([
|
|
90
|
+
...recoveryCandidates.map((row) => [row.id, row.summary]),
|
|
91
|
+
[KEEP_BASELINE_CHOICE, 'Keep the current deterministic recovery path.'],
|
|
92
|
+
[NEEDS_EVIDENCE_CHOICE, 'More evidence is required before any handler runs.']
|
|
93
|
+
])
|
|
94
|
+
};
|
|
95
|
+
questionBindings.recovery = { kind: 'recovery' };
|
|
96
|
+
}
|
|
97
|
+
const state = {
|
|
98
|
+
task: { goal: redactDecisionText(input.goal, 1_200) },
|
|
99
|
+
facts: {
|
|
100
|
+
all_required_slices_must_be_preserved: true,
|
|
101
|
+
...(input.facts || {})
|
|
102
|
+
},
|
|
103
|
+
slices: (input.requiredSliceIds || []).map((id) => ({ id })),
|
|
104
|
+
plans: Object.fromEntries(planCandidates.map((plan) => [plan.id, {
|
|
105
|
+
worker_count: plan.workerCount,
|
|
106
|
+
groups: plan.groups.map((group) => ({
|
|
107
|
+
id: group.id,
|
|
108
|
+
slice_ids: group.sliceIds,
|
|
109
|
+
role: group.role,
|
|
110
|
+
effort: group.effort
|
|
111
|
+
})),
|
|
112
|
+
all_required_work_preserved: true,
|
|
113
|
+
required_verification_ids: plan.requiredVerificationIds
|
|
114
|
+
}])),
|
|
115
|
+
context: Object.fromEntries(contextCandidates.map((row) => [row.id, {
|
|
116
|
+
source: row.sourcePath,
|
|
117
|
+
excerpt: row.excerpt,
|
|
118
|
+
fresh: row.fresh,
|
|
119
|
+
pinned: row.pinned,
|
|
120
|
+
reproducible: row.reproducible
|
|
121
|
+
}])),
|
|
122
|
+
recovery: {
|
|
123
|
+
diagnostic: redactDecisionText(input.recoveryDiagnostic || '', 800),
|
|
124
|
+
handlers: recoveryCandidates
|
|
125
|
+
},
|
|
126
|
+
routing: {
|
|
127
|
+
models: Object.fromEntries(SEALED_ROUTING_MODELS.map((row) => [row.id, {
|
|
128
|
+
effort: row.effort,
|
|
129
|
+
summary: row.summary
|
|
130
|
+
}])),
|
|
131
|
+
roles: Object.fromEntries(routedRoles.map((role) => [role.id, {
|
|
132
|
+
summary: redactDecisionText(role.summary, 240)
|
|
133
|
+
}]))
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const request = {
|
|
137
|
+
model: DESIGN_DEFAULTS.model,
|
|
138
|
+
state,
|
|
139
|
+
questions,
|
|
140
|
+
provider: { zdr: true, data_collection: 'deny', allow_fallbacks: false }
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
binding: buildDecisionBinding({
|
|
144
|
+
projectId: input.projectId,
|
|
145
|
+
workflowRunId: input.workflowRunId,
|
|
146
|
+
workflowRevision: input.workflowRevision,
|
|
147
|
+
sourceDigest: input.sourceDigest,
|
|
148
|
+
graphDigest: input.graphDigest,
|
|
149
|
+
candidates: { planCandidates, contextCandidates, recoveryCandidates, routingCandidates: routedRoles },
|
|
150
|
+
questions,
|
|
151
|
+
requestedModel: DESIGN_DEFAULTS.model
|
|
152
|
+
}),
|
|
153
|
+
request,
|
|
154
|
+
planCandidates,
|
|
155
|
+
contextCandidates,
|
|
156
|
+
recoveryCandidates,
|
|
157
|
+
routingCandidates: routedRoles,
|
|
158
|
+
baselinePlanId: input.baselinePlanId ?? planCandidates[0]?.id ?? null,
|
|
159
|
+
questionBindings
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function questionRoom(questions, recoverySlot) {
|
|
163
|
+
return DESIGN_DEFAULTS.maxQuestions - recoverySlot - Object.keys(questions).length;
|
|
164
|
+
}
|
|
165
|
+
function appendRoutingChoices(roles, questions, bindings, recoverySlot) {
|
|
166
|
+
const included = [];
|
|
167
|
+
for (const role of roles) {
|
|
168
|
+
if (questionRoom(questions, recoverySlot) < 1)
|
|
169
|
+
break;
|
|
170
|
+
const id = `route_${role.id}`;
|
|
171
|
+
const instructions = redactDecisionText(`Choose the sealed model for routing.roles.${role.id} using state.task and that role summary. Prefer the fastest sealed model that can do the work. Use gpt-6-astra for judgment, ambiguity, or high-stakes work. Select ${KEEP_BASELINE_CHOICE} when the work is not distinguished. Do not invent a model.`);
|
|
172
|
+
assertIndependentQuestion(instructions);
|
|
173
|
+
questions[id] = {
|
|
174
|
+
type: 'choice',
|
|
175
|
+
instructions,
|
|
176
|
+
criteria: {
|
|
177
|
+
...Object.fromEntries(SEALED_ROUTING_MODELS.map((row) => [row.id, row.summary])),
|
|
178
|
+
[KEEP_BASELINE_CHOICE]: 'The role work is not distinguished enough to leave the baseline model.'
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
bindings[id] = { kind: 'routing', roleId: role.id };
|
|
182
|
+
included.push(role);
|
|
183
|
+
}
|
|
184
|
+
return included;
|
|
185
|
+
}
|
|
186
|
+
function appendRoutingSpeculation(roles, questions, bindings, recoverySlot) {
|
|
187
|
+
for (const role of roles) {
|
|
188
|
+
if (questionRoom(questions, recoverySlot) < 1)
|
|
189
|
+
break;
|
|
190
|
+
const difficultyId = `difficulty_${role.id}`;
|
|
191
|
+
const difficultyInstructions = redactDecisionText(`Rate the difficulty of routing.roles.${role.id} for state.task. Use only that role summary and the task.`);
|
|
192
|
+
assertIndependentQuestion(difficultyInstructions);
|
|
193
|
+
questions[difficultyId] = {
|
|
194
|
+
type: 'score',
|
|
195
|
+
instructions: difficultyInstructions,
|
|
196
|
+
criteria: [...ROUTING_DIFFICULTY_RUBRIC]
|
|
197
|
+
};
|
|
198
|
+
bindings[difficultyId] = { kind: 'routing_difficulty', roleId: role.id };
|
|
199
|
+
if (questionRoom(questions, recoverySlot) < 1)
|
|
200
|
+
break;
|
|
201
|
+
const riskId = `risk_${role.id}`;
|
|
202
|
+
const riskInstructions = redactDecisionText(`Does routing.roles.${role.id} need gpt-6-astra because the work is high-stakes, ambiguous, or unsafe on a faster sealed model?`);
|
|
203
|
+
assertIndependentQuestion(riskInstructions);
|
|
204
|
+
questions[riskId] = {
|
|
205
|
+
type: 'noul',
|
|
206
|
+
instructions: riskInstructions,
|
|
207
|
+
criteria: {
|
|
208
|
+
true: 'The work needs the most capable sealed model.',
|
|
209
|
+
false: 'A faster sealed model can do this work.'
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
bindings[riskId] = { kind: 'routing_risk', roleId: role.id };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export { QUESTION_REVISION };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { randomId } from '../fsx.js';
|
|
2
|
+
import { redactDecisionValue } from './openrouter.js';
|
|
3
|
+
import { RECEIPT_SCHEMA, UNKNOWN_USAGE } from './types.js';
|
|
4
|
+
export function usageReceiptFromCompiled(compiled) {
|
|
5
|
+
return { ...compiled.usage };
|
|
6
|
+
}
|
|
7
|
+
export function unknownUsageReceipt() {
|
|
8
|
+
return { ...UNKNOWN_USAGE };
|
|
9
|
+
}
|
|
10
|
+
export function cacheHitUsage(historical) {
|
|
11
|
+
return {
|
|
12
|
+
inputTokens: historical.inputTokens,
|
|
13
|
+
outputTokens: historical.outputTokens,
|
|
14
|
+
reportedCost: historical.reportedCost,
|
|
15
|
+
evidence: historical.evidence
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function buildDecisionReceipt(input) {
|
|
19
|
+
const usage = input.cacheHit
|
|
20
|
+
? {
|
|
21
|
+
inputTokens: input.compiled.usage.inputTokens,
|
|
22
|
+
outputTokens: input.compiled.usage.outputTokens,
|
|
23
|
+
reportedCost: null,
|
|
24
|
+
evidence: input.compiled.usage.evidence
|
|
25
|
+
}
|
|
26
|
+
: { ...input.compiled.usage };
|
|
27
|
+
const receipt = {
|
|
28
|
+
schema: RECEIPT_SCHEMA,
|
|
29
|
+
decisionId: `jev-${randomId(12)}`,
|
|
30
|
+
binding: input.binding,
|
|
31
|
+
resolvedModel: input.resolvedModel ?? (input.compiled.kind === 'apply' ? input.compiled.resolvedModel : null),
|
|
32
|
+
responseId: input.responseId ?? null,
|
|
33
|
+
result: input.result,
|
|
34
|
+
reason: input.reason,
|
|
35
|
+
usage,
|
|
36
|
+
elapsedMs: Math.max(0, Math.trunc(input.elapsedMs)),
|
|
37
|
+
cacheHit: input.cacheHit === true,
|
|
38
|
+
consumptionEvidence: input.consumptionEvidence ?? null
|
|
39
|
+
};
|
|
40
|
+
return redactDecisionValue(receipt, input.env);
|
|
41
|
+
}
|