argus-reviewer-e2e 0.1.2 → 0.2.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 +30 -5
- package/action/action.yml +22 -0
- package/action/sticky-comment.mjs +287 -79
- package/dist/api.d.ts +1 -0
- package/dist/api.js +1 -0
- package/dist/cli.d.ts +67 -0
- package/dist/cli.js +463 -86
- package/dist/config.d.ts +105 -2
- package/dist/config.js +136 -9
- package/dist/debug.d.ts +1 -0
- package/dist/debug.js +9 -3
- package/dist/detect.d.ts +11 -1
- package/dist/detect.js +19 -3
- package/dist/evidence/ci.d.ts +47 -2
- package/dist/evidence/ci.js +98 -6
- package/dist/evidence/gate.d.ts +10 -0
- package/dist/evidence/gate.js +29 -0
- package/dist/evidence/link.d.ts +6 -1
- package/dist/evidence/link.js +7 -5
- package/dist/executor/sandbox.d.ts +105 -0
- package/dist/executor/sandbox.js +231 -0
- package/dist/probe/author.d.ts +50 -0
- package/dist/probe/author.js +149 -0
- package/dist/probe/harness.d.ts +30 -0
- package/dist/probe/harness.js +99 -0
- package/dist/probe/queue.d.ts +105 -0
- package/dist/probe/queue.js +446 -0
- package/dist/review/adjudicate.d.ts +63 -0
- package/dist/review/adjudicate.js +111 -0
- package/dist/review/secrets.d.ts +88 -0
- package/dist/review/secrets.js +220 -0
- package/dist/review/triage.d.ts +76 -0
- package/dist/review/triage.js +163 -0
- package/dist/trust.d.ts +50 -0
- package/dist/trust.js +103 -0
- package/dist/vision/cost.d.ts +14 -1
- package/dist/vision/cost.js +13 -0
- package/dist/vision/decisions.d.ts +95 -0
- package/dist/vision/decisions.js +232 -0
- package/package.json +3 -2
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* U7 PR triage lane — one batched `decide` call before chunk review
|
|
3
|
+
* produces a typed-probability triage record (the pace two-round
|
|
4
|
+
* pattern). Jev routes and annotates, never gates: every chunk is still
|
|
5
|
+
* reviewed by a code model and the deterministic verdict stays
|
|
6
|
+
* authoritative. `decide` failure degrades open — the record lands with
|
|
7
|
+
* `unadjudicated` and routing keeps the configured (strong) model.
|
|
8
|
+
*
|
|
9
|
+
* PR title/body in `state` are untrusted text — Jev is the only
|
|
10
|
+
* consumer; they never reach the verdict path.
|
|
11
|
+
*/
|
|
12
|
+
import { debug } from '../debug.js';
|
|
13
|
+
import { describeDecisionError, isChoiceAnswer, isNoulAnswer, isScoreAnswer, JEV_DEFAULT_MODEL, } from '../vision/decisions.js';
|
|
14
|
+
// The choice criteria own the vocabulary — TRIAGE_AREAS derives from
|
|
15
|
+
// it so an offered option can never drift out of the record type.
|
|
16
|
+
const RISK_AREA_CRITERIA = {
|
|
17
|
+
auth: 'authentication, authorization, tokens, sessions, permissions',
|
|
18
|
+
billing: 'payments, invoices, usage metering, cost accounting',
|
|
19
|
+
data: 'persistence, migrations, integrity, serialization, caching',
|
|
20
|
+
ops: 'CI, deploy, infra, configuration, tooling, observability',
|
|
21
|
+
none: 'no meaningful risk area in this diff',
|
|
22
|
+
};
|
|
23
|
+
export const TRIAGE_AREAS = Object.keys(RISK_AREA_CRITERIA);
|
|
24
|
+
/** Diff excerpt bound — triage needs shape, not full fidelity. */
|
|
25
|
+
const MAX_DIFF_STATE_CHARS = 12_000;
|
|
26
|
+
/** Per-file bound — one huge patch must not starve every other file. */
|
|
27
|
+
const MAX_FILE_EXCERPT = 4_000;
|
|
28
|
+
const MAX_TITLE_CHARS = 300;
|
|
29
|
+
const MAX_BODY_CHARS = 2_000;
|
|
30
|
+
const MAX_FILE_LIST = 100;
|
|
31
|
+
export function buildTriageState(opts) {
|
|
32
|
+
let budget = MAX_DIFF_STATE_CHARS;
|
|
33
|
+
const excerpts = [];
|
|
34
|
+
for (const f of opts.files) {
|
|
35
|
+
if (budget <= 0)
|
|
36
|
+
break;
|
|
37
|
+
if (f.patch === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
const take = f.patch.slice(0, Math.min(budget, MAX_FILE_EXCERPT));
|
|
40
|
+
excerpts.push(take);
|
|
41
|
+
budget -= take.length;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
title: (opts.title ?? '').slice(0, MAX_TITLE_CHARS),
|
|
45
|
+
body: (opts.body ?? '').slice(0, MAX_BODY_CHARS),
|
|
46
|
+
files: opts.files.slice(0, MAX_FILE_LIST).map((f) => f.filename),
|
|
47
|
+
totalFiles: opts.files.length,
|
|
48
|
+
diffExcerpt: excerpts.join('\n\n'),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Question IDs — one spelling for builder and reader. */
|
|
52
|
+
const Q = { deep: 'needs_deep_review', risk: 'risk', area: 'top_risk_area' };
|
|
53
|
+
function buildTriageQuestions() {
|
|
54
|
+
return {
|
|
55
|
+
[Q.deep]: {
|
|
56
|
+
type: 'noul',
|
|
57
|
+
instructions: 'does this PR warrant careful review beyond a skim? yes for changes touching ' +
|
|
58
|
+
'auth, money, data integrity, concurrency, secrets handling, public APIs, or ' +
|
|
59
|
+
'irreversible ops; no for docs, comments, formatting, or metadata-only changes.',
|
|
60
|
+
},
|
|
61
|
+
[Q.risk]: {
|
|
62
|
+
type: 'score',
|
|
63
|
+
instructions: 'blast radius if this PR merges broken — pick the closest rubric level.',
|
|
64
|
+
criteria: [
|
|
65
|
+
'cosmetic only — docs, comments, formatting, metadata',
|
|
66
|
+
'minor — internal-only paths, limited blast radius',
|
|
67
|
+
'moderate — user-visible defects plausible',
|
|
68
|
+
'high — security-sensitive or data-handling paths touched',
|
|
69
|
+
'severe — auth, billing, or data-loss surface',
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
[Q.area]: {
|
|
73
|
+
type: 'choice',
|
|
74
|
+
instructions: 'the single subsystem most likely to hide a defect in this diff.',
|
|
75
|
+
criteria: RISK_AREA_CRITERIA,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export async function triagePr(opts) {
|
|
80
|
+
try {
|
|
81
|
+
const { answers, model } = await opts.client.decide({
|
|
82
|
+
...(opts.model !== undefined ? { model: opts.model } : {}),
|
|
83
|
+
state: opts.state,
|
|
84
|
+
questions: buildTriageQuestions(),
|
|
85
|
+
});
|
|
86
|
+
const deep = answers[Q.deep];
|
|
87
|
+
const risk = answers[Q.risk];
|
|
88
|
+
const area = answers[Q.area];
|
|
89
|
+
const rec = {
|
|
90
|
+
mode: opts.mode,
|
|
91
|
+
model,
|
|
92
|
+
diffExcerptChars: opts.state.diffExcerpt.length,
|
|
93
|
+
};
|
|
94
|
+
if (deep !== undefined && isNoulAnswer(deep))
|
|
95
|
+
rec.needsDeepReview = deep.noul;
|
|
96
|
+
if (risk !== undefined && isScoreAnswer(risk))
|
|
97
|
+
rec.risk = risk.score;
|
|
98
|
+
if (area !== undefined &&
|
|
99
|
+
isChoiceAnswer(area) &&
|
|
100
|
+
TRIAGE_AREAS.includes(area.choice)) {
|
|
101
|
+
rec.topRiskArea = area.choice;
|
|
102
|
+
if (area.confidence !== undefined)
|
|
103
|
+
rec.topRiskAreaConfidence = area.confidence;
|
|
104
|
+
}
|
|
105
|
+
// A call that answered none of the typed questions adjudicated nothing.
|
|
106
|
+
if (rec.needsDeepReview === undefined &&
|
|
107
|
+
rec.risk === undefined &&
|
|
108
|
+
rec.topRiskArea === undefined) {
|
|
109
|
+
rec.unadjudicated = true;
|
|
110
|
+
}
|
|
111
|
+
return rec;
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
debug('triage', `adjudication failed — degrading to annotate: ${describeDecisionError(e)}`);
|
|
115
|
+
return { mode: opts.mode, model: opts.model ?? JEV_DEFAULT_MODEL, unadjudicated: true };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 'route' mode model selection — cheap tier only on a clear low-risk
|
|
120
|
+
* signal (risk ≤ 2 AND deep-review < 0.5) backed by actual diff
|
|
121
|
+
* evidence. Any ambiguity (missing fields, unadjudicated, unset
|
|
122
|
+
* lowRiskModel, or a title/body-only triage — fully attacker-steerable
|
|
123
|
+
* text) keeps the configured model: coverage stays constant and the
|
|
124
|
+
* expensive path is the default.
|
|
125
|
+
*/
|
|
126
|
+
export function routeModel(opts) {
|
|
127
|
+
const rec = opts.record;
|
|
128
|
+
if (rec === undefined ||
|
|
129
|
+
rec.mode !== 'route' ||
|
|
130
|
+
rec.unadjudicated === true ||
|
|
131
|
+
opts.lowRiskModel === undefined ||
|
|
132
|
+
rec.risk === undefined ||
|
|
133
|
+
rec.needsDeepReview === undefined ||
|
|
134
|
+
(rec.diffExcerptChars ?? 0) === 0) {
|
|
135
|
+
return { model: opts.configured, reason: 'no routing signal' };
|
|
136
|
+
}
|
|
137
|
+
if (rec.risk <= 2 && rec.needsDeepReview < 0.5) {
|
|
138
|
+
return {
|
|
139
|
+
model: opts.lowRiskModel,
|
|
140
|
+
reason: `low risk — risk=${rec.risk}, needsDeepReview=${rec.needsDeepReview.toFixed(2)}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
model: opts.configured,
|
|
145
|
+
reason: `risk=${rec.risk}, needsDeepReview=${rec.needsDeepReview.toFixed(2)}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* U9 — the probe lane's advisory ordering signal. Present only when
|
|
150
|
+
* triage adjudicated a real area with its confidence attached; 'none'
|
|
151
|
+
* is the null-area sentinel, not an ordering signal, and the
|
|
152
|
+
* confidence floor itself is queue policy (MIN_AREA_CONFIDENCE).
|
|
153
|
+
*/
|
|
154
|
+
export function triageAreaSignal(rec) {
|
|
155
|
+
if (rec === undefined ||
|
|
156
|
+
rec.unadjudicated === true ||
|
|
157
|
+
rec.topRiskArea === undefined ||
|
|
158
|
+
rec.topRiskArea === 'none' ||
|
|
159
|
+
rec.topRiskAreaConfidence === undefined) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
return { area: rec.topRiskArea, confidence: rec.topRiskAreaConfidence };
|
|
163
|
+
}
|
package/dist/trust.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { PrMeta } from './evidence/ci.js';
|
|
2
|
+
/**
|
|
3
|
+
* Checkout trust — decided BEFORE `loadConfig` runs, because loading a
|
|
4
|
+
* PR-controlled `argus-reviewer.config.ts` executes arbitrary code beside
|
|
5
|
+
* `OPENROUTER_API_KEY`/`GITHUB_TOKEN` (#58).
|
|
6
|
+
*
|
|
7
|
+
* Trust keys on fork status of the checked-out tree, never on
|
|
8
|
+
* `author_association` — a MEMBER can author a hostile fork PR.
|
|
9
|
+
*/
|
|
10
|
+
export type Trust = 'trusted' | 'untrusted';
|
|
11
|
+
export interface TrustResult {
|
|
12
|
+
trust: Trust;
|
|
13
|
+
reason: string;
|
|
14
|
+
/**
|
|
15
|
+
* PR number derived from the event payload on `issue_comment` events
|
|
16
|
+
* (`issue.number` when the issue is a PR). Undefined otherwise — the
|
|
17
|
+
* caller's own repo/pr derivation still applies.
|
|
18
|
+
*/
|
|
19
|
+
pr: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
export interface ResolveTrustOpts {
|
|
22
|
+
env: Record<string, string | undefined>;
|
|
23
|
+
/**
|
|
24
|
+
* Fetches PR metadata — required only on `issue_comment` (the payload has
|
|
25
|
+
* no `head.repo.fork`) and as a fallback when a `pull_request*` payload
|
|
26
|
+
* is unreadable. Injectable for tests.
|
|
27
|
+
*/
|
|
28
|
+
fetchMeta?: (repo: string, pr: string, token: string) => Promise<PrMeta | undefined>;
|
|
29
|
+
/** Injectable event-payload reader for tests. Defaults to node fs. */
|
|
30
|
+
readEventFile?: (path: string) => Promise<string>;
|
|
31
|
+
/** Human-readable note on the resolved decision (e.g. ctx.err). */
|
|
32
|
+
note?: (line: string) => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolves whether the checked-out tree may execute config code.
|
|
36
|
+
*
|
|
37
|
+
* - `pull_request*` events: fork status from the event payload — no token
|
|
38
|
+
* needed (`head.repo.fork`). Payload absent/unreadable → `fetchMeta`
|
|
39
|
+
* fallback → still unknown → untrusted.
|
|
40
|
+
* - `issue_comment`: `fetchMeta` on `issue.number`; unavailable → untrusted.
|
|
41
|
+
* - Any other present `GITHUB_EVENT_NAME` (`workflow_run`, `push`,
|
|
42
|
+
* `workflow_dispatch`, …): untrusted — unlisted CI events fail closed
|
|
43
|
+
* because privileged-CI-over-fork-checkout patterns (workflow_run over a
|
|
44
|
+
* fork SHA) land exactly there. Maintainers opt out explicitly with
|
|
45
|
+
* `ARGUS_TRUSTED=1`.
|
|
46
|
+
* - No event env at all (local run): trusted unless `ARGUS_UNTRUSTED=1`.
|
|
47
|
+
* - `ARGUS_UNTRUSTED=1` always wins; `ARGUS_TRUSTED=1` overrides event
|
|
48
|
+
* resolution but never `ARGUS_UNTRUSTED`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveTrust(opts: ResolveTrustOpts): Promise<TrustResult>;
|
package/dist/trust.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
async function readEventPayload(env, readEventFile) {
|
|
3
|
+
const eventPath = env.GITHUB_EVENT_PATH;
|
|
4
|
+
if (eventPath === undefined || eventPath === '')
|
|
5
|
+
return undefined;
|
|
6
|
+
try {
|
|
7
|
+
const parsed = JSON.parse(await readEventFile(eventPath));
|
|
8
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
return parsed;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function tracePr(env) {
|
|
18
|
+
const raw = env.ARGUS_REVIEWER_TRACE;
|
|
19
|
+
if (raw === undefined)
|
|
20
|
+
return undefined;
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
return typeof parsed.pr === 'string' && parsed.pr !== '' ? parsed.pr : undefined;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function fetchMetaFor(opts, prOverride) {
|
|
30
|
+
const { env, fetchMeta } = opts;
|
|
31
|
+
if (fetchMeta === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
const repo = env.GITHUB_REPOSITORY;
|
|
34
|
+
const token = env.GITHUB_TOKEN ?? env.GH_TOKEN;
|
|
35
|
+
const pr = prOverride ?? tracePr(env);
|
|
36
|
+
if (repo === undefined || token === undefined || pr === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
return fetchMeta(repo, pr, token);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolves whether the checked-out tree may execute config code.
|
|
42
|
+
*
|
|
43
|
+
* - `pull_request*` events: fork status from the event payload — no token
|
|
44
|
+
* needed (`head.repo.fork`). Payload absent/unreadable → `fetchMeta`
|
|
45
|
+
* fallback → still unknown → untrusted.
|
|
46
|
+
* - `issue_comment`: `fetchMeta` on `issue.number`; unavailable → untrusted.
|
|
47
|
+
* - Any other present `GITHUB_EVENT_NAME` (`workflow_run`, `push`,
|
|
48
|
+
* `workflow_dispatch`, …): untrusted — unlisted CI events fail closed
|
|
49
|
+
* because privileged-CI-over-fork-checkout patterns (workflow_run over a
|
|
50
|
+
* fork SHA) land exactly there. Maintainers opt out explicitly with
|
|
51
|
+
* `ARGUS_TRUSTED=1`.
|
|
52
|
+
* - No event env at all (local run): trusted unless `ARGUS_UNTRUSTED=1`.
|
|
53
|
+
* - `ARGUS_UNTRUSTED=1` always wins; `ARGUS_TRUSTED=1` overrides event
|
|
54
|
+
* resolution but never `ARGUS_UNTRUSTED`.
|
|
55
|
+
*/
|
|
56
|
+
export async function resolveTrust(opts) {
|
|
57
|
+
const { env, note } = opts;
|
|
58
|
+
const readEventFile = opts.readEventFile ?? ((p) => readFile(p, 'utf8'));
|
|
59
|
+
const done = (trust, reason, pr) => {
|
|
60
|
+
note?.(`trust: ${trust} — ${reason}`);
|
|
61
|
+
return { trust, reason, pr };
|
|
62
|
+
};
|
|
63
|
+
if (env.ARGUS_UNTRUSTED === '1') {
|
|
64
|
+
return done('untrusted', 'ARGUS_UNTRUSTED=1');
|
|
65
|
+
}
|
|
66
|
+
if (env.ARGUS_TRUSTED === '1') {
|
|
67
|
+
return done('trusted', 'ARGUS_TRUSTED=1 override');
|
|
68
|
+
}
|
|
69
|
+
const eventName = env.GITHUB_EVENT_NAME;
|
|
70
|
+
if (eventName === undefined || eventName === '') {
|
|
71
|
+
return done('trusted', 'local run — no CI event context');
|
|
72
|
+
}
|
|
73
|
+
const payload = await readEventPayload(env, readEventFile);
|
|
74
|
+
if (eventName === 'pull_request' || eventName === 'pull_request_target') {
|
|
75
|
+
const fork = payload?.pull_request?.head?.repo?.fork;
|
|
76
|
+
if (fork === true)
|
|
77
|
+
return done('untrusted', 'fork PR head checkout');
|
|
78
|
+
if (fork === false)
|
|
79
|
+
return done('trusted', 'same-repo PR checkout');
|
|
80
|
+
// Payload absent/unreadable (or head.repo null — a deleted source fork
|
|
81
|
+
// fails closed the same way evidence/ci.ts's isFork does): try the API.
|
|
82
|
+
const meta = await fetchMetaFor(opts);
|
|
83
|
+
if (meta !== undefined) {
|
|
84
|
+
return meta.isFork
|
|
85
|
+
? done('untrusted', 'fork PR (metadata)')
|
|
86
|
+
: done('trusted', 'same-repo PR (metadata)');
|
|
87
|
+
}
|
|
88
|
+
return done('untrusted', 'PR fork status unavailable — failing closed');
|
|
89
|
+
}
|
|
90
|
+
if (eventName === 'issue_comment') {
|
|
91
|
+
const pr = payload?.issue?.pull_request !== undefined && typeof payload.issue?.number === 'number'
|
|
92
|
+
? String(payload.issue.number)
|
|
93
|
+
: undefined;
|
|
94
|
+
const meta = await fetchMetaFor(opts, pr);
|
|
95
|
+
if (meta !== undefined) {
|
|
96
|
+
return meta.isFork
|
|
97
|
+
? done('untrusted', 'issue_comment on fork PR', pr)
|
|
98
|
+
: done('trusted', 'issue_comment on same-repo PR', pr);
|
|
99
|
+
}
|
|
100
|
+
return done('untrusted', 'issue_comment PR metadata unavailable — failing closed', pr);
|
|
101
|
+
}
|
|
102
|
+
return done('untrusted', `unlisted CI event "${eventName}" — failing closed`);
|
|
103
|
+
}
|
package/dist/vision/cost.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type CallKind = 'ground' | 'heal' | 'assert' | 'code';
|
|
1
|
+
export type CallKind = 'ground' | 'heal' | 'assert' | 'code' | 'decide';
|
|
2
2
|
export interface CallCost {
|
|
3
3
|
model: string;
|
|
4
4
|
provider: string;
|
|
@@ -30,6 +30,19 @@ export interface OpenRouterResponse {
|
|
|
30
30
|
usage: Usage;
|
|
31
31
|
}
|
|
32
32
|
export declare function makeCallCost(response: OpenRouterResponse, kind: CallKind): CallCost;
|
|
33
|
+
/** Decisions API (`/api/alpha/decisions`) usage shape — no `cost_details`. */
|
|
34
|
+
export interface DecisionsResponse {
|
|
35
|
+
id?: string;
|
|
36
|
+
model?: string;
|
|
37
|
+
provider?: ProviderValue;
|
|
38
|
+
answers?: Record<string, unknown>;
|
|
39
|
+
usage?: {
|
|
40
|
+
input_tokens?: number;
|
|
41
|
+
output_tokens?: number;
|
|
42
|
+
cost?: number;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export declare function makeDecisionsCallCost(response: DecisionsResponse, kind: CallKind): CallCost;
|
|
33
46
|
export declare function extractUsageCost(response: OpenRouterResponse): {
|
|
34
47
|
costUsd: number;
|
|
35
48
|
upstreamCostUsd: number;
|
package/dist/vision/cost.js
CHANGED
|
@@ -8,6 +8,19 @@ export function makeCallCost(response, kind) {
|
|
|
8
8
|
kind,
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
|
+
export function makeDecisionsCallCost(response, kind) {
|
|
12
|
+
const providerName = typeof response.provider === 'string' ? response.provider : response.provider?.name ?? 'unknown';
|
|
13
|
+
const usage = response.usage ?? {};
|
|
14
|
+
// Coerce — a string cost would throw downstream at toFixed and discard
|
|
15
|
+
// a valid decision; a string token count would concatenate.
|
|
16
|
+
return {
|
|
17
|
+
model: response.model ?? 'unknown',
|
|
18
|
+
provider: providerName,
|
|
19
|
+
tokens: (Number(usage.input_tokens) || 0) + (Number(usage.output_tokens) || 0),
|
|
20
|
+
costUsd: Number(usage.cost) || 0,
|
|
21
|
+
kind,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
11
24
|
export function extractUsageCost(response) {
|
|
12
25
|
return {
|
|
13
26
|
costUsd: response.usage.cost,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { CallCost, ProviderValue } from './cost.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pinned Jev slug — the alias `~typesafe/jev-latest` drifts silently and
|
|
4
|
+
* adjudication thresholds are calibrated to a version. The alias stays
|
|
5
|
+
* usable via `config.decisionModel` for experimentation.
|
|
6
|
+
*/
|
|
7
|
+
export declare const JEV_DEFAULT_MODEL = "typesafe/jev-1.13-20260917";
|
|
8
|
+
export type DecisionErrorKind = 'auth' | 'validation' | 'rate_limited' | 'overloaded' | 'server_error' | 'timeout' | 'unexpected';
|
|
9
|
+
export declare class DecisionError extends Error {
|
|
10
|
+
readonly kind: DecisionErrorKind;
|
|
11
|
+
readonly retryable: boolean;
|
|
12
|
+
constructor(kind: DecisionErrorKind, message: string, retryable: boolean);
|
|
13
|
+
}
|
|
14
|
+
export interface NoulQuestion {
|
|
15
|
+
type: 'noul';
|
|
16
|
+
instructions: string;
|
|
17
|
+
/** Optional yes/no clarifications, sent verbatim. */
|
|
18
|
+
true?: string;
|
|
19
|
+
false?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ChoiceQuestion {
|
|
22
|
+
type: 'choice';
|
|
23
|
+
instructions: string;
|
|
24
|
+
/** Option ID -> description. Up to 255 options. */
|
|
25
|
+
criteria: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
export interface ScoreQuestion {
|
|
28
|
+
type: 'score';
|
|
29
|
+
instructions: string;
|
|
30
|
+
/** 2-10 ordered rubric levels, low to high. */
|
|
31
|
+
criteria: string[];
|
|
32
|
+
}
|
|
33
|
+
export type DecisionQuestion = NoulQuestion | ChoiceQuestion | ScoreQuestion;
|
|
34
|
+
export interface NoulAnswer {
|
|
35
|
+
noul: number;
|
|
36
|
+
}
|
|
37
|
+
export interface ChoiceAnswer {
|
|
38
|
+
choice: string;
|
|
39
|
+
probabilities?: Record<string, number>;
|
|
40
|
+
confidence?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface ScoreAnswer {
|
|
43
|
+
score: number;
|
|
44
|
+
legend?: string[];
|
|
45
|
+
probabilities?: number[];
|
|
46
|
+
confidence?: number;
|
|
47
|
+
}
|
|
48
|
+
export type DecisionAnswer = NoulAnswer | ChoiceAnswer | ScoreAnswer;
|
|
49
|
+
/** Type guards over the answer union — one `in` check per lane otherwise. */
|
|
50
|
+
export declare const isNoulAnswer: (a: DecisionAnswer) => a is NoulAnswer;
|
|
51
|
+
export declare const isChoiceAnswer: (a: DecisionAnswer) => a is ChoiceAnswer;
|
|
52
|
+
export declare const isScoreAnswer: (a: DecisionAnswer) => a is ScoreAnswer;
|
|
53
|
+
/** Short error label for lane debug lines: DecisionError kind, else message. */
|
|
54
|
+
export declare function describeDecisionError(e: unknown): string;
|
|
55
|
+
/** Shared per-call batch cap for the Jev lanes (secrets, findings, triage). */
|
|
56
|
+
export declare const MAX_CANDIDATES = 50;
|
|
57
|
+
export interface DecisionClientOptions {
|
|
58
|
+
apiKey: string;
|
|
59
|
+
fetch?: typeof fetch;
|
|
60
|
+
trace?: Record<string, string>;
|
|
61
|
+
/** Request timeout in ms. Default 15_000. */
|
|
62
|
+
timeoutMs?: number;
|
|
63
|
+
onCall?: (call: {
|
|
64
|
+
id: string;
|
|
65
|
+
model: string;
|
|
66
|
+
provider: string;
|
|
67
|
+
kind: 'decide';
|
|
68
|
+
costUsd: number;
|
|
69
|
+
tokens: number;
|
|
70
|
+
trace?: Record<string, string>;
|
|
71
|
+
}) => void;
|
|
72
|
+
}
|
|
73
|
+
export declare class DecisionClient {
|
|
74
|
+
private _apiKey;
|
|
75
|
+
private _fetch;
|
|
76
|
+
private _trace;
|
|
77
|
+
private _timeoutMs;
|
|
78
|
+
private _onCall;
|
|
79
|
+
constructor(opts: DecisionClientOptions);
|
|
80
|
+
/**
|
|
81
|
+
* Ask typed questions about `state`. Throws a typed `DecisionError` on
|
|
82
|
+
* any failure — callers catch and degrade to unadjudicated; a decision
|
|
83
|
+
* failure must never suppress findings (KTD6).
|
|
84
|
+
*/
|
|
85
|
+
decide(opts: {
|
|
86
|
+
model?: string;
|
|
87
|
+
state: unknown;
|
|
88
|
+
questions: Record<string, DecisionQuestion>;
|
|
89
|
+
}): Promise<{
|
|
90
|
+
answers: Record<string, DecisionAnswer>;
|
|
91
|
+
cost: CallCost;
|
|
92
|
+
model: string;
|
|
93
|
+
}>;
|
|
94
|
+
}
|
|
95
|
+
export type { ProviderValue };
|