@ngockhoale/ukit 3.1.0 → 3.1.2
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 +17 -0
- package/manifests/platform.full.yaml +24 -0
- package/package.json +1 -1
- package/src/core/agentRuntime/completionGate.js +4 -1
- package/src/core/agentRuntime/vmEngine.js +8 -1
- package/src/core/experiments/deliberation.js +6 -0
- package/src/core/gatewayProbe.js +53 -0
- package/src/core/memory/memoryFlags.js +6 -0
- package/src/core/runtimeConfig.js +3 -3
- package/src/decision/client.js +77 -8
- package/src/decision/protocol.js +4 -1
- package/src/decision/registry.js +145 -1
- package/src/decision/reviewVerdict.js +309 -0
- package/template_project/.claude/agents/code-reviewer.md +25 -1
- package/template_project/.claude/agents/ukit-small-task-maintainer.md +16 -0
- package/template_project/.claude/commands/ukit/handoff-fullstack.md +2 -0
- package/template_project/.claude/commands/ukit/handoff-review.md +12 -0
- package/template_project/.claude/ukit/index/review-verdict.mjs +592 -0
- package/template_project/.claude/ukit/index/route-task.mjs +41 -0
- package/template_project/.claude/ukit/index/sidecar-decision.mjs +595 -0
- package/template_project/.claude/ukit/index/unic-decision.mjs +119 -13
- package/template_project/.codex/settings.json +3 -0
- package/template_project/.omp/agents/code-reviewer.md +25 -1
- package/template_project/.omp/agents/ukit-small-task-maintainer.md +16 -0
- package/template_project/ukit/storage/config.json +276 -178
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 3.1.2 - 2026-09-26
|
|
6
|
+
|
|
7
|
+
- **Decision plane goes live on the UNIC provider** — the decision lane now resolves its own endpoint independent of the engine's LLM gateway, so UKit uses UNIC even when Claude Code/Codex/omp point elsewhere:
|
|
8
|
+
- **Endpoint chain**: `~/.ukit/storage/gateway.json` `{decision:{baseUrl,apiKey,scheme}}` → env `UKIT_DECISION_BASE_URL`/`UKIT_DECISION_API_KEY` → default `https://openai.unicjsc.com`; credential falls back to the shared UNIC token (ANTHROPIC_*). New resolvers `resolveDecisionBaseUrl`/`resolveDecisionApiKey` in `src/core/gatewayProbe.js`, mirrored in `unic-decision.mjs`.
|
|
9
|
+
- **Live transport fixes** (verified against the real gateway): SSE `data: [DONE]` trailer — sometimes glued to the JSON body — is stripped via balanced-object extraction; model answers use native arg names (`choice`/`noul`/`score`) now accepted alongside the schema's `value`; a `model_not_found` variant checkpoint retries once on the bare `unic-decision` alias.
|
|
10
|
+
- **Checkpoints**: shipped defaults collapse to the bare `unic-decision` model (the credential-safe alias); `unic-decision-multilingual`/variants remain configurable via `decisionPlane.checkpoints`.
|
|
11
|
+
- **Optional backlog drained**: injection-point contracts documented (deliberation `judge`, vmEngine `classifyFn`, completionGate `modelJudgment`), `memoryV2.decision` plane marked reserved, `resume.next-action.v1` + `verify.depth.v1` shadow questions wired, gatewayProbe marked migration-exempt.
|
|
12
|
+
|
|
13
|
+
## 3.1.1 - 2026-09-26
|
|
14
|
+
|
|
15
|
+
- **Decision plane — review + workflow families (UNIC_DECISION_MIGRATION S1–S4)** — the reserved `review` and `workflow` registry families are now populated and wired; all keys ship `rolloutStage: 'off'` so deterministic fallbacks stay authoritative (stage promotion is a separate owner decision):
|
|
16
|
+
- **Registry** (`src/decision/registry.js`): `review.verdict.v1`, `review.finding-bucket.v1`, `review.panel-verdict.v1`, `workflow.sidecar-lane.v1`, `workflow.sidecar-risk.v1`, `workflow.routing-needed.v1`, `workflow.step-budget.v1`, `workflow.compact-now.v1`, `workflow.summarize-vs-keep.v1` — all `choice` kind, `off`, deterministic fallback owners recorded.
|
|
17
|
+
- **Review adapter**: `src/decision/reviewVerdict.js` + installed twin `template_project/.claude/ukit/index/review-verdict.mjs` — findings (whitelisted `{id, severity, file, line, claim}`) in, stage-gated verdict + per-finding buckets out; reviewers still generate findings on the general model, only the bounded verdict moves to `unic-decision` when `decisionPlane.families.review.stage` is enabled.
|
|
18
|
+
- **Sidecar adapter**: `template_project/.claude/ukit/index/sidecar-decision.mjs` answers the six `.codex/settings.json` `smallTaskModel.decisionPolicy.decisions` via a bounded `unic-decision` batch (stage-gated `decisionPlane.families.workflow.stage`); adapter failure → deterministic rule answer, never another LLM.
|
|
19
|
+
- **Engine parity**: `.codex/ukit` is a symlink to `.claude/ukit` (installed layout), `decisionAdapter` hints in `.codex/settings.json`; all three index CLIs' `isMainModule` gates now use `realpathSync` + basename equality — symlinked invocation previously compared unresolved paths and silently exited 0 without running `main()`.
|
|
20
|
+
- **Docs/tests**: `docs/pstack/UNIC_DECISION_GUIDE.md` (family keys + adapters), `docs/GATEWAY.md` §8, `docs/MEMORY.md`; parity + symlink-invocation coverage in `tests/index/{unicDecision,reviewVerdictCli,sidecarDecision}.test.js`.
|
|
21
|
+
|
|
5
22
|
## 3.1.0 - 2026-09-26
|
|
6
23
|
|
|
7
24
|
- **Flight recorder — Data Foundation wave (cycle C81)** — the observability pipeline grows from record/segment plumbing into a full local diagnostics surface; still opt-in behind `observability.stage` (default `off`, `off→shadow→canary→default` per seam, absolute kill switch):
|
|
@@ -1750,6 +1750,30 @@ items:
|
|
|
1750
1750
|
packs:
|
|
1751
1751
|
- core
|
|
1752
1752
|
|
|
1753
|
+
- id: ukit-index-review-verdict-script
|
|
1754
|
+
type: config
|
|
1755
|
+
sourceTemplate: .claude/ukit/index/review-verdict.mjs
|
|
1756
|
+
targetPath: .claude/ukit/index/review-verdict.mjs
|
|
1757
|
+
requires:
|
|
1758
|
+
- ukit-index-unic-decision-script
|
|
1759
|
+
mergeStrategy: overwrite_with_backup
|
|
1760
|
+
variables: []
|
|
1761
|
+
enabledByDefault: true
|
|
1762
|
+
packs:
|
|
1763
|
+
- core
|
|
1764
|
+
|
|
1765
|
+
- id: ukit-index-sidecar-decision-script
|
|
1766
|
+
type: config
|
|
1767
|
+
sourceTemplate: .claude/ukit/index/sidecar-decision.mjs
|
|
1768
|
+
targetPath: .claude/ukit/index/sidecar-decision.mjs
|
|
1769
|
+
requires:
|
|
1770
|
+
- ukit-index-unic-decision-script
|
|
1771
|
+
mergeStrategy: overwrite_with_backup
|
|
1772
|
+
variables: []
|
|
1773
|
+
enabledByDefault: true
|
|
1774
|
+
packs:
|
|
1775
|
+
- core
|
|
1776
|
+
|
|
1753
1777
|
- id: ukit-index-extract-image-script
|
|
1754
1778
|
type: config
|
|
1755
1779
|
sourceTemplate: .claude/ukit/index/extract-image.mjs
|
package/package.json
CHANGED
|
@@ -152,7 +152,10 @@ export function evaluateCompletionEvidence({
|
|
|
152
152
|
verdict = 'FAILED';
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
// modelJudgment may only request more evidence, never promote
|
|
155
|
+
// modelJudgment may only request more evidence, never promote (enforced
|
|
156
|
+
// below: a judgment can downgrade PASS → MORE_EVIDENCE, nothing else). When
|
|
157
|
+
// model-backed, the judgment MUST originate from the decision plane
|
|
158
|
+
// (unic-decision, a bounded verdict question) — never a general LLM call.
|
|
156
159
|
const judgment = modelJudgment != null
|
|
157
160
|
? (typeof modelJudgment === 'string' ? modelJudgment : String(modelJudgment))
|
|
158
161
|
: null;
|
|
@@ -85,6 +85,11 @@ async function writeFileAtomic(target, data) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
+
* Migration contract (decision plane): `classifyFn` is invoked only when the
|
|
89
|
+
* IR itself cannot route an event (see deliver()). It may only consult the
|
|
90
|
+
* decision plane (unic-decision / createDecisionClient registry keys) for a
|
|
91
|
+
* bounded route/escalate answer — never a general LLM call.
|
|
92
|
+
*
|
|
88
93
|
* @param {{dir:string, now?:()=>Date, classifyFn?:Function, startFn?:Function, hooks?:object}} opts
|
|
89
94
|
*/
|
|
90
95
|
export function createVmEngine({ dir, now, classifyFn, startFn, hooks } = {}) {
|
|
@@ -512,7 +517,9 @@ export function createVmEngine({ dir, now, classifyFn, startFn, hooks } = {}) {
|
|
|
512
517
|
return { consumed: false, code: 'out_of_order' };
|
|
513
518
|
}
|
|
514
519
|
|
|
515
|
-
// classify only when the IR itself cannot route the event (G4-FR05)
|
|
520
|
+
// classify only when the IR itself cannot route the event (G4-FR05);
|
|
521
|
+
// classifyFn may consult the decision plane for bounded route/escalate
|
|
522
|
+
// answers only — never a general LLM call (see createVmEngine opts).
|
|
516
523
|
let outcome = event.safePayload?.outcome;
|
|
517
524
|
let targetMatched = matched;
|
|
518
525
|
if (!targetMatched) {
|
|
@@ -244,6 +244,12 @@ function deepFreezeCopy(value) {
|
|
|
244
244
|
* verdict is post-checked: hard-constraint waivers/violations and
|
|
245
245
|
* majority-only (citation-free) verdicts are rejected.
|
|
246
246
|
*
|
|
247
|
+
* Migration contract (decision plane): an injected `judge` implementation
|
|
248
|
+
* that performs bounded verdict work MUST route it through the decision
|
|
249
|
+
* plane (`createDecisionClient` + `src/decision/registry.js` keys) — never a
|
|
250
|
+
* general LLM call. The default `smartJudge` is deterministic: pure scoring,
|
|
251
|
+
* hard-constraint filtering, and a stable sort — no I/O, no model calls.
|
|
252
|
+
*
|
|
247
253
|
* @param {{candidates: object[], rubric: object, judge: Function,
|
|
248
254
|
* config?: object}} input
|
|
249
255
|
* @returns {{status: 'ok'|'disabled'|'rejected', selected?: object|null,
|
package/src/core/gatewayProbe.js
CHANGED
|
@@ -23,6 +23,52 @@ import path from 'node:path';
|
|
|
23
23
|
|
|
24
24
|
const GATEWAY_DOC = 'docs/GATEWAY.md';
|
|
25
25
|
const SETTINGS_RELATIVE_PROBE_PATH = path.join('.claude', 'settings.json');
|
|
26
|
+
// --- Decision-plane gateway resolution (UNIC_DECISION_MIGRATION backlog) -----
|
|
27
|
+
// The decision plane is a SEPARATE lane from the host's LLM endpoint: it must
|
|
28
|
+
// reach the UNIC OpenAI-compatible provider no matter what Claude Code/omp is
|
|
29
|
+
// configured to. Chain: ~/.ukit/storage/gateway.json {decision:{baseUrl,
|
|
30
|
+
// apiKey, scheme}} → env UKIT_DECISION_BASE_URL / UKIT_DECISION_API_KEY →
|
|
31
|
+
// DEFAULT_DECISION_BASE_URL + the shared UNIC token (ANTHROPIC_*) so a host
|
|
32
|
+
// already pointed at UNIC works with zero extra config. Missing key → the
|
|
33
|
+
// caller surfaces 'unavailable' and deterministic policy stays authoritative.
|
|
34
|
+
export const DEFAULT_DECISION_BASE_URL = 'https://openai.unicjsc.com';
|
|
35
|
+
const USER_GATEWAY_REL = path.join('.ukit', 'storage', 'gateway.json');
|
|
36
|
+
|
|
37
|
+
function readUserGatewayDecision(homeDir) {
|
|
38
|
+
if (!homeDir) return null;
|
|
39
|
+
const json = safeReadJson(path.join(homeDir, USER_GATEWAY_REL));
|
|
40
|
+
const decision = json?.decision;
|
|
41
|
+
return decision && typeof decision === 'object' ? decision : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Endpoint for the decision lane. Never throws; file and env malformed values
|
|
45
|
+
// are ignored, the default is the final fallback (not ANTHROPIC_BASE_URL —
|
|
46
|
+
// that env may point at a non-decision gateway).
|
|
47
|
+
export function resolveDecisionBaseUrl({ homeDir = os.homedir(), env = process.env } = {}) {
|
|
48
|
+
const file = readUserGatewayDecision(homeDir);
|
|
49
|
+
const fromFile = pickString(file?.baseUrl);
|
|
50
|
+
if (fromFile) return { baseUrl: fromFile, source: 'ukit-gateway-json' };
|
|
51
|
+
const fromEnv = pickString(env?.UKIT_DECISION_BASE_URL);
|
|
52
|
+
if (fromEnv) return { baseUrl: fromEnv, source: 'env' };
|
|
53
|
+
return { baseUrl: DEFAULT_DECISION_BASE_URL, source: 'default' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// API key for the decision lane. `scheme`: 'auth-token' → Authorization: Bearer
|
|
57
|
+
// (UNIC OpenAI-compatible default) | 'api-key' → x-api-key. Falls back to the
|
|
58
|
+
// host's UNIC token so existing installs need no duplicate credential.
|
|
59
|
+
export function resolveDecisionApiKey({ projectRoot, homeDir = os.homedir(), env = process.env } = {}) {
|
|
60
|
+
const file = readUserGatewayDecision(homeDir);
|
|
61
|
+
const fileKey = pickString(file?.apiKey);
|
|
62
|
+
if (fileKey) {
|
|
63
|
+
const scheme = file?.scheme === 'api-key' ? 'api-key' : 'auth-token';
|
|
64
|
+
return { value: fileKey, scheme, source: 'ukit-gateway-json' };
|
|
65
|
+
}
|
|
66
|
+
const envKey = pickString(env?.UKIT_DECISION_API_KEY);
|
|
67
|
+
if (envKey) return { value: envKey, scheme: 'auth-token', source: 'env' };
|
|
68
|
+
const shared = resolveGatewayApiKey({ projectRoot, homeDir, env });
|
|
69
|
+
if (shared?.value) return { ...shared, source: 'shared-unic-token' };
|
|
70
|
+
return { value: null, scheme: null, source: null };
|
|
71
|
+
}
|
|
26
72
|
|
|
27
73
|
function safeReadFile(filePath) {
|
|
28
74
|
try {
|
|
@@ -491,6 +537,13 @@ async function runNonStreamingProbe({ url, headers, body: requestBody, fetchImpl
|
|
|
491
537
|
}
|
|
492
538
|
|
|
493
539
|
/**
|
|
540
|
+
* Migration-contract exemption (decision plane): the two `fetchImpl` calls below
|
|
541
|
+
* are the ONLY generic-LLM HTTP calls under src/. They are diagnostic-only —
|
|
542
|
+
* connectivity probes that send a trivial "pong?" message and measure the
|
|
543
|
+
* transport (SSE events, buffering, Anthropic Message shape) — and are
|
|
544
|
+
* intentionally NOT routed through the decision plane: no bounded verdict
|
|
545
|
+
* vocabulary exists for a connectivity probe. Keep-as-probe; do not migrate.
|
|
546
|
+
*
|
|
494
547
|
* Runs both probes against `baseUrl`. Returns a structured result; never propagates exceptions
|
|
495
548
|
* — every failure is reflected in the returned object so the CLI can print it.
|
|
496
549
|
*
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
// resolveConfigStage machinery: off → shadow → canary → default. Flags only
|
|
5
5
|
// ever REDUCE capability — malformed/missing resolves 'off', killSwitch is
|
|
6
6
|
// absolute, and 'canary' without an opted-in projectId resolves 'off'.
|
|
7
|
+
// The `decision` plane is reserved for the memoryV2 candidate-classification
|
|
8
|
+
// lane (registry key `learn.candidate-class.v1`, family 'learn'). Note the
|
|
9
|
+
// decision-plane stage namespace is separate: promotion gates that key via
|
|
10
|
+
// `decisionPlane.families.learn.stage`, NOT `memoryV2.decision.stage` — this
|
|
11
|
+
// flag only guards the memoryV2 lane itself. Zero consumers today — do not
|
|
12
|
+
// remove; user configs may pin its stage key.
|
|
7
13
|
//
|
|
8
14
|
// Shadow receipts are the only telemetry: bounded JSONL of counts/codes/
|
|
9
15
|
// latency bands — never record text, prompt bodies, or secrets.
|
|
@@ -304,9 +304,9 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
|
|
|
304
304
|
stage: 'off',
|
|
305
305
|
logicalModel: 'unic-decision',
|
|
306
306
|
checkpoints: {
|
|
307
|
-
default: 'unic-decision
|
|
308
|
-
english: 'unic-decision
|
|
309
|
-
unknownLanguage: 'unic-decision
|
|
307
|
+
default: 'unic-decision',
|
|
308
|
+
english: 'unic-decision',
|
|
309
|
+
unknownLanguage: 'unic-decision',
|
|
310
310
|
},
|
|
311
311
|
timeoutMs: 5000,
|
|
312
312
|
maxStateTokens: { multilingual: 1024, english: 512 },
|
package/src/decision/client.js
CHANGED
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
|
|
28
28
|
import { createHash } from 'node:crypto';
|
|
29
29
|
import {
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
resolveDecisionBaseUrl,
|
|
31
|
+
resolveDecisionApiKey,
|
|
32
32
|
} from '../core/gatewayProbe.js';
|
|
33
33
|
import {
|
|
34
34
|
encodeBatch,
|
|
@@ -42,11 +42,20 @@ import {
|
|
|
42
42
|
} from './statePacket.js';
|
|
43
43
|
|
|
44
44
|
const DEFAULT_CHECKPOINTS = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
// Bare 'unic-decision' is the credential-safe alias — the only checkpoint a
|
|
46
|
+
// freshly-bound UNIC decision key serves. Variants (e.g.
|
|
47
|
+
// unic-decision-multilingual once the provider binds it) override via
|
|
48
|
+
// decisionPlane.checkpoints.
|
|
49
|
+
default: 'unic-decision',
|
|
50
|
+
english: 'unic-decision',
|
|
51
|
+
unknownLanguage: 'unic-decision',
|
|
48
52
|
};
|
|
49
53
|
|
|
54
|
+
// Credential-safe alias: every UNIC decision credential binds the bare name
|
|
55
|
+
// first — variants (unic-decision-multilingual, …) may lag. On model_not_found
|
|
56
|
+
// the client falls back here once per batch.
|
|
57
|
+
const FALLBACK_CHECKPOINT = 'unic-decision';
|
|
58
|
+
|
|
50
59
|
const DEFAULT_TIMEOUT_MS = 5000;
|
|
51
60
|
const DEFAULT_MAX_RETRIES = 1;
|
|
52
61
|
const DEFAULT_BREAKER = { failureThreshold: 3, cooldownMs: 30_000 };
|
|
@@ -102,9 +111,44 @@ function isTransientStatus(status) {
|
|
|
102
111
|
return status === 429 || (status >= 500 && status <= 599);
|
|
103
112
|
}
|
|
104
113
|
|
|
114
|
+
// The UNIC decision endpoint streams the completion even without stream:true —
|
|
115
|
+
// the body is one JSON object followed by an SSE "data: [DONE]" trailer.
|
|
116
|
+
// Parse the first JSON object, ignore the trailer.
|
|
117
|
+
function parseMaybeSse(text) {
|
|
118
|
+
const trimmed = String(text ?? '').trim();
|
|
119
|
+
if (!trimmed.startsWith('{')) {
|
|
120
|
+
// Pure SSE frames: join data payloads.
|
|
121
|
+
const payload = trimmed
|
|
122
|
+
.split('\n')
|
|
123
|
+
.filter((line) => line.startsWith('data:') && !line.includes('[DONE]'))
|
|
124
|
+
.map((line) => line.slice(5).trim())
|
|
125
|
+
.join('');
|
|
126
|
+
return JSON.parse(payload || trimmed);
|
|
127
|
+
}
|
|
128
|
+
// JSON object possibly followed by a glued SSE trailer (`}data: [DONE]`,
|
|
129
|
+
// observed live) — extract the balanced object, strings/comments aware.
|
|
130
|
+
let depth = 0;
|
|
131
|
+
for (let i = 0; i < trimmed.length; i += 1) {
|
|
132
|
+
const ch = trimmed[i];
|
|
133
|
+
if (ch === '"' || ch === "'") {
|
|
134
|
+
for (let j = i + 1; j < trimmed.length; j += 1) {
|
|
135
|
+
if (trimmed[j] === '\\') j += 1;
|
|
136
|
+
else if (trimmed[j] === ch) { i = j; break; }
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (ch === '{') depth += 1;
|
|
141
|
+
else if (ch === '}') {
|
|
142
|
+
depth -= 1;
|
|
143
|
+
if (depth === 0) return JSON.parse(trimmed.slice(0, i + 1));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return JSON.parse(trimmed);
|
|
147
|
+
}
|
|
148
|
+
|
|
105
149
|
async function readResponseBody(res) {
|
|
150
|
+
if (res && typeof res.text === 'function') return parseMaybeSse(await res.text());
|
|
106
151
|
if (res && typeof res.json === 'function') return res.json();
|
|
107
|
-
if (res && typeof res.text === 'function') return JSON.parse(await res.text());
|
|
108
152
|
return res;
|
|
109
153
|
}
|
|
110
154
|
|
|
@@ -167,9 +211,13 @@ export function createDecisionClient({
|
|
|
167
211
|
}
|
|
168
212
|
|
|
169
213
|
async function resolveEndpoint() {
|
|
170
|
-
|
|
214
|
+
// Decision lane resolves its own endpoint (user-level gateway.json →
|
|
215
|
+
// env → openai.unicjsc.com default), independent of the host's LLM
|
|
216
|
+
// endpoint — the UNIC provider is used even when the engine points
|
|
217
|
+
// elsewhere.
|
|
218
|
+
const base = await resolveDecisionBaseUrl({ projectRoot, homeDir, env });
|
|
171
219
|
if (!base?.baseUrl) return null;
|
|
172
|
-
const key = await
|
|
220
|
+
const key = await resolveDecisionApiKey({ projectRoot, homeDir, env });
|
|
173
221
|
return {
|
|
174
222
|
url: `${String(base.baseUrl).replace(/\/+$/, '')}/v1/chat/completions`,
|
|
175
223
|
headers: buildAuthHeaders(key),
|
|
@@ -305,6 +353,27 @@ export function createDecisionClient({
|
|
|
305
353
|
|
|
306
354
|
const status = res?.status ?? (res?.ok === false ? 500 : 200);
|
|
307
355
|
if (res?.ok === false || status < 200 || status >= 300) {
|
|
356
|
+
// A variant checkpoint without bound credentials (e.g.
|
|
357
|
+
// unic-decision-multilingual before provider binding) returns a 4xx
|
|
358
|
+
// model_not_found body — retry once on the bare `unic-decision`
|
|
359
|
+
// model, the credential-safe alias the owner provisions first.
|
|
360
|
+
if (checkpoint !== FALLBACK_CHECKPOINT) {
|
|
361
|
+
let notFound = status === 404;
|
|
362
|
+
try {
|
|
363
|
+
const clone = typeof res?.clone === 'function' ? res.clone() : null;
|
|
364
|
+
const errBody = clone ? parseMaybeSse(await clone.text()) : null;
|
|
365
|
+
if (errBody?.error?.code === 'model_not_found') notFound = true;
|
|
366
|
+
} catch { /* keep status-based guess */ }
|
|
367
|
+
if (notFound) {
|
|
368
|
+
checkpoint = FALLBACK_CHECKPOINT;
|
|
369
|
+
try {
|
|
370
|
+
body = JSON.stringify(
|
|
371
|
+
encodeBatch({ model: checkpoint, statePacket: serialized, questions }),
|
|
372
|
+
);
|
|
373
|
+
} catch { /* keep prior body */ }
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
308
377
|
if (isTransientStatus(status) && attempt + 1 < maxAttempts) continue;
|
|
309
378
|
if (status === 401 || status === 403) {
|
|
310
379
|
lastError = 'unavailable';
|
package/src/decision/protocol.js
CHANGED
|
@@ -169,7 +169,10 @@ function normalizeProbabilities(raw) {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
function validateValue(question, args) {
|
|
172
|
-
|
|
172
|
+
// The model answers with its native field names — `choice`/`noul`/`score` —
|
|
173
|
+
// not the schema's `value`; accept both so a live gateway isn't parsed as
|
|
174
|
+
// malformed.
|
|
175
|
+
const value = args?.value ?? args?.choice ?? args?.noul ?? args?.score;
|
|
173
176
|
if (question.kind === 'choice') {
|
|
174
177
|
if (typeof value !== 'string') return 'missing-value';
|
|
175
178
|
if (!Array.isArray(question.candidates) || !question.candidates.includes(value)) return 'out-of-enum';
|
package/src/decision/registry.js
CHANGED
|
@@ -259,7 +259,7 @@ export const DECISION_REGISTRY = Object.freeze([
|
|
|
259
259
|
family: 'learn',
|
|
260
260
|
owner: 'learningCandidates',
|
|
261
261
|
kind: 'choice',
|
|
262
|
-
description: 'Learning candidate classification for a captured signal.',
|
|
262
|
+
description: 'Learning candidate classification for a captured signal. Reserved for the memoryV2 decision lane; promotion gates via decisionPlane.families.learn.stage.',
|
|
263
263
|
candidatePolicy: 'owner-shortlist',
|
|
264
264
|
hardConstraints: ['promotion-approval'],
|
|
265
265
|
probabilityPolicy: 'raw-label',
|
|
@@ -269,6 +269,150 @@ export const DECISION_REGISTRY = Object.freeze([
|
|
|
269
269
|
cacheSensitivity: 'none',
|
|
270
270
|
leasePolicy: 'none',
|
|
271
271
|
},
|
|
272
|
+
{
|
|
273
|
+
decisionKey: 'review.verdict.v1',
|
|
274
|
+
schemaVersion: 1,
|
|
275
|
+
family: 'review',
|
|
276
|
+
owner: 'handoffReview',
|
|
277
|
+
kind: 'choice',
|
|
278
|
+
description: 'Bounded review verdict among APPROVED | APPROVED-WITH-MINOR | CHANGES-REQUESTED | CRITICAL.',
|
|
279
|
+
candidatePolicy: 'fixed-verdict-vocabulary',
|
|
280
|
+
hardConstraints: ['verdict-vocabulary'],
|
|
281
|
+
probabilityPolicy: 'raw-label',
|
|
282
|
+
fallbackPolicy: 'deterministic-human-review',
|
|
283
|
+
telemetryClass: 'decision',
|
|
284
|
+
rolloutStage: 'off',
|
|
285
|
+
cacheSensitivity: 'none',
|
|
286
|
+
leasePolicy: 'none',
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
decisionKey: 'review.finding-bucket.v1',
|
|
290
|
+
schemaVersion: 1,
|
|
291
|
+
family: 'review',
|
|
292
|
+
owner: 'handoffReview',
|
|
293
|
+
kind: 'choice',
|
|
294
|
+
description: 'Triage bucket for one review finding among Act on | Consider | Noted | Dismissed.',
|
|
295
|
+
candidatePolicy: 'fixed-bucket-vocabulary',
|
|
296
|
+
hardConstraints: ['bucket-vocabulary'],
|
|
297
|
+
probabilityPolicy: 'raw-label',
|
|
298
|
+
fallbackPolicy: 'deterministic-human-review',
|
|
299
|
+
telemetryClass: 'decision',
|
|
300
|
+
rolloutStage: 'off',
|
|
301
|
+
cacheSensitivity: 'none',
|
|
302
|
+
leasePolicy: 'none',
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
decisionKey: 'review.panel-verdict.v1',
|
|
306
|
+
schemaVersion: 1,
|
|
307
|
+
family: 'review',
|
|
308
|
+
owner: 'handoffReview',
|
|
309
|
+
kind: 'choice',
|
|
310
|
+
description: 'Aggregated panel verdict for a review artifact.',
|
|
311
|
+
candidatePolicy: 'fixed-verdict-vocabulary',
|
|
312
|
+
hardConstraints: ['verdict-vocabulary'],
|
|
313
|
+
probabilityPolicy: 'raw-label',
|
|
314
|
+
fallbackPolicy: 'deterministic-human-review',
|
|
315
|
+
telemetryClass: 'decision',
|
|
316
|
+
rolloutStage: 'off',
|
|
317
|
+
cacheSensitivity: 'none',
|
|
318
|
+
leasePolicy: 'none',
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
decisionKey: 'workflow.sidecar-lane.v1',
|
|
322
|
+
schemaVersion: 1,
|
|
323
|
+
family: 'workflow',
|
|
324
|
+
owner: 'smallTaskMaintainer',
|
|
325
|
+
kind: 'choice',
|
|
326
|
+
description: 'Bounded decision whether a chore belongs to the sidecar lane.',
|
|
327
|
+
candidatePolicy: 'owner-shortlist',
|
|
328
|
+
hardConstraints: ['lane-eligibility'],
|
|
329
|
+
probabilityPolicy: 'raw-label',
|
|
330
|
+
fallbackPolicy: 'deterministic-lane-rules',
|
|
331
|
+
telemetryClass: 'decision',
|
|
332
|
+
rolloutStage: 'off',
|
|
333
|
+
cacheSensitivity: 'none',
|
|
334
|
+
leasePolicy: 'none',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
decisionKey: 'workflow.sidecar-risk.v1',
|
|
338
|
+
schemaVersion: 1,
|
|
339
|
+
family: 'workflow',
|
|
340
|
+
owner: 'smallTaskMaintainer',
|
|
341
|
+
kind: 'choice',
|
|
342
|
+
description: 'Bounded risk class for a sidecar candidate (safe/reversible vs escalate).',
|
|
343
|
+
candidatePolicy: 'owner-shortlist',
|
|
344
|
+
hardConstraints: ['risk-floor'],
|
|
345
|
+
probabilityPolicy: 'raw-label',
|
|
346
|
+
fallbackPolicy: 'deterministic-lane-rules',
|
|
347
|
+
telemetryClass: 'decision',
|
|
348
|
+
rolloutStage: 'off',
|
|
349
|
+
cacheSensitivity: 'none',
|
|
350
|
+
leasePolicy: 'none',
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
decisionKey: 'workflow.routing-needed.v1',
|
|
354
|
+
schemaVersion: 1,
|
|
355
|
+
family: 'workflow',
|
|
356
|
+
owner: 'smallTaskMaintainer',
|
|
357
|
+
kind: 'choice',
|
|
358
|
+
description: 'Whether routing/intent classification is needed for a prompt.',
|
|
359
|
+
candidatePolicy: 'owner-shortlist',
|
|
360
|
+
hardConstraints: ['lane-eligibility'],
|
|
361
|
+
probabilityPolicy: 'raw-label',
|
|
362
|
+
fallbackPolicy: 'deterministic-route',
|
|
363
|
+
telemetryClass: 'decision',
|
|
364
|
+
rolloutStage: 'off',
|
|
365
|
+
cacheSensitivity: 'none',
|
|
366
|
+
leasePolicy: 'none',
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
decisionKey: 'workflow.step-budget.v1',
|
|
370
|
+
schemaVersion: 1,
|
|
371
|
+
family: 'workflow',
|
|
372
|
+
owner: 'smallTaskMaintainer',
|
|
373
|
+
kind: 'choice',
|
|
374
|
+
description: 'Bounded step-budget class for an in-flight task.',
|
|
375
|
+
candidatePolicy: 'owner-shortlist',
|
|
376
|
+
hardConstraints: ['task-boundary'],
|
|
377
|
+
probabilityPolicy: 'raw-label',
|
|
378
|
+
fallbackPolicy: 'deterministic-budget',
|
|
379
|
+
telemetryClass: 'decision',
|
|
380
|
+
rolloutStage: 'off',
|
|
381
|
+
cacheSensitivity: 'none',
|
|
382
|
+
leasePolicy: 'none',
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
decisionKey: 'workflow.compact-now.v1',
|
|
386
|
+
schemaVersion: 1,
|
|
387
|
+
family: 'workflow',
|
|
388
|
+
owner: 'smallTaskMaintainer',
|
|
389
|
+
kind: 'choice',
|
|
390
|
+
description: 'Whether to compact context at the current boundary.',
|
|
391
|
+
candidatePolicy: 'owner-shortlist',
|
|
392
|
+
hardConstraints: ['task-boundary'],
|
|
393
|
+
probabilityPolicy: 'raw-label',
|
|
394
|
+
fallbackPolicy: 'deterministic-threshold',
|
|
395
|
+
telemetryClass: 'decision',
|
|
396
|
+
rolloutStage: 'off',
|
|
397
|
+
cacheSensitivity: 'none',
|
|
398
|
+
leasePolicy: 'none',
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
decisionKey: 'workflow.summarize-vs-keep.v1',
|
|
402
|
+
schemaVersion: 1,
|
|
403
|
+
family: 'workflow',
|
|
404
|
+
owner: 'smallTaskMaintainer',
|
|
405
|
+
kind: 'choice',
|
|
406
|
+
description: 'Summarize vs keep for a stored artifact/message.',
|
|
407
|
+
candidatePolicy: 'owner-shortlist',
|
|
408
|
+
hardConstraints: ['task-boundary', 'source-freshness'],
|
|
409
|
+
probabilityPolicy: 'raw-label',
|
|
410
|
+
fallbackPolicy: 'deterministic-threshold',
|
|
411
|
+
telemetryClass: 'decision',
|
|
412
|
+
rolloutStage: 'off',
|
|
413
|
+
cacheSensitivity: 'none',
|
|
414
|
+
leasePolicy: 'none',
|
|
415
|
+
},
|
|
272
416
|
]);
|
|
273
417
|
|
|
274
418
|
// Deep-freeze the shipped catalog — entries are shared metadata consumed by
|