@post-print/agent-harness 1.1.8 → 2.0.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 +11 -93
- package/dist/agent-definition.d.ts +77 -0
- package/dist/agent-definition.d.ts.map +1 -0
- package/dist/agent-definition.js +46 -0
- package/dist/agent-definition.js.map +1 -0
- package/dist/agent-session.d.ts +17 -0
- package/dist/agent-session.d.ts.map +1 -0
- package/dist/agent-session.js +165 -0
- package/dist/agent-session.js.map +1 -0
- package/dist/agent-worker.d.ts +2 -0
- package/dist/agent-worker.d.ts.map +1 -0
- package/dist/agent-worker.js +167 -0
- package/dist/agent-worker.js.map +1 -0
- package/dist/capture.d.ts +1 -0
- package/dist/capture.d.ts.map +1 -1
- package/dist/capture.js +119 -101
- package/dist/capture.js.map +1 -1
- package/dist/claude-capture.d.ts +0 -1
- package/dist/claude-capture.d.ts.map +1 -1
- package/dist/claude-capture.js +96 -106
- package/dist/claude-capture.js.map +1 -1
- package/dist/claude-run.d.ts +3 -11
- package/dist/claude-run.d.ts.map +1 -1
- package/dist/claude-run.js +178 -166
- package/dist/claude-run.js.map +1 -1
- package/dist/cursor-auth.d.ts +0 -1
- package/dist/cursor-auth.d.ts.map +1 -1
- package/dist/cursor-auth.js +5 -3
- package/dist/cursor-auth.js.map +1 -1
- package/dist/cursor-run.d.ts +6 -34
- package/dist/cursor-run.d.ts.map +1 -1
- package/dist/cursor-run.js +123 -175
- package/dist/cursor-run.js.map +1 -1
- package/dist/index.d.ts +8 -40
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -47
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +37 -39
- package/dist/mcp.js.map +1 -1
- package/dist/openai-capture.d.ts.map +1 -1
- package/dist/openai-capture.js +66 -57
- package/dist/openai-capture.js.map +1 -1
- package/dist/openai-run.d.ts +4 -13
- package/dist/openai-run.d.ts.map +1 -1
- package/dist/openai-run.js +168 -159
- package/dist/openai-run.js.map +1 -1
- package/dist/sealed-workspace.d.ts +0 -8
- package/dist/sealed-workspace.d.ts.map +1 -1
- package/dist/sealed-workspace.js +32 -65
- package/dist/sealed-workspace.js.map +1 -1
- package/dist/skills-context.d.ts +0 -24
- package/dist/skills-context.d.ts.map +1 -1
- package/dist/skills-context.js +12 -156
- package/dist/skills-context.js.map +1 -1
- package/dist/types.d.ts +0 -84
- package/dist/types.d.ts.map +1 -1
- package/dist/user-skills.d.ts +6 -6
- package/dist/user-skills.d.ts.map +1 -1
- package/dist/user-skills.js +32 -27
- package/dist/user-skills.js.map +1 -1
- package/dist/working-tree-guard.d.ts.map +1 -1
- package/dist/working-tree-guard.js +3 -1
- package/dist/working-tree-guard.js.map +1 -1
- package/package.json +3 -3
- package/dist/adapters/index.d.ts +0 -28
- package/dist/adapters/index.d.ts.map +0 -1
- package/dist/adapters/index.js +0 -271
- package/dist/adapters/index.js.map +0 -1
- package/dist/classifier.d.ts +0 -15
- package/dist/classifier.d.ts.map +0 -1
- package/dist/classifier.js +0 -102
- package/dist/classifier.js.map +0 -1
- package/dist/context.d.ts +0 -26
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -227
- package/dist/context.js.map +0 -1
- package/dist/conversation.d.ts +0 -28
- package/dist/conversation.d.ts.map +0 -1
- package/dist/conversation.js +0 -191
- package/dist/conversation.js.map +0 -1
- package/dist/host-registry.d.ts +0 -11
- package/dist/host-registry.d.ts.map +0 -1
- package/dist/host-registry.js +0 -36
- package/dist/host-registry.js.map +0 -1
- package/dist/judge.d.ts +0 -132
- package/dist/judge.d.ts.map +0 -1
- package/dist/judge.js +0 -795
- package/dist/judge.js.map +0 -1
- package/dist/logger.d.ts +0 -11
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -20
- package/dist/logger.js.map +0 -1
- package/dist/main.d.ts +0 -9
- package/dist/main.d.ts.map +0 -1
- package/dist/main.js +0 -7
- package/dist/main.js.map +0 -1
- package/dist/retry.d.ts +0 -14
- package/dist/retry.d.ts.map +0 -1
- package/dist/retry.js +0 -52
- package/dist/retry.js.map +0 -1
- package/dist/user-simulator.d.ts +0 -10
- package/dist/user-simulator.d.ts.map +0 -1
- package/dist/user-simulator.js +0 -47
- package/dist/user-simulator.js.map +0 -1
- package/dist/worktree-leak.d.ts +0 -16
- package/dist/worktree-leak.d.ts.map +0 -1
- package/dist/worktree-leak.js +0 -110
- package/dist/worktree-leak.js.map +0 -1
- package/dist/worktree.d.ts +0 -10
- package/dist/worktree.d.ts.map +0 -1
- package/dist/worktree.js +0 -58
- package/dist/worktree.js.map +0 -1
package/dist/judge.js
DELETED
|
@@ -1,795 +0,0 @@
|
|
|
1
|
-
import { missingClassifierAuth, runClassifier } from "./classifier.js";
|
|
2
|
-
import { isTransientInfraError, resolveRetryMaxAttempts, withRetry } from "./retry.js";
|
|
3
|
-
import { sumUsageParts } from "./usage-breakdown.js";
|
|
4
|
-
// Optional info-string (`json`, `js`, `typescript`, …) on the opening fence line.
|
|
5
|
-
// Without this, ` ```js\n"yes"\n``` ` extracts `js\n"yes"` and never latches.
|
|
6
|
-
const JSON_FENCE_PATTERN = /```[^\n`]*\s*([\s\S]*?)```/;
|
|
7
|
-
const JSON_OBJECT_PATTERN = /\{[\s\S]*\}/;
|
|
8
|
-
const JUDGE_TOOL_RESULT_MAX_CHARS = 4_000;
|
|
9
|
-
function truncateJudgeText(text, max = JUDGE_TOOL_RESULT_MAX_CHARS) {
|
|
10
|
-
if (text.length <= max) {
|
|
11
|
-
return text;
|
|
12
|
-
}
|
|
13
|
-
return `${text.slice(0, max)}…`;
|
|
14
|
-
}
|
|
15
|
-
function formatOneToolCallForJudge(call, index) {
|
|
16
|
-
const args = JSON.stringify(call.args ?? {});
|
|
17
|
-
const lines = [`${index + 1}. ${call.name} ${truncateJudgeText(args, 2_000)}`];
|
|
18
|
-
if (call.result) {
|
|
19
|
-
lines.push(` result: ${truncateJudgeText(call.result)}`);
|
|
20
|
-
}
|
|
21
|
-
return lines.join("\n");
|
|
22
|
-
}
|
|
23
|
-
function formatToolCallsForJudge(trace) {
|
|
24
|
-
if (trace.toolCalls.length === 0) {
|
|
25
|
-
return "";
|
|
26
|
-
}
|
|
27
|
-
return [
|
|
28
|
-
"Tool calls:",
|
|
29
|
-
...trace.toolCalls.map((call, index) => formatOneToolCallForJudge(call, index)),
|
|
30
|
-
].join("\n");
|
|
31
|
-
}
|
|
32
|
-
function formatTraceChronologically(trace) {
|
|
33
|
-
const blocks = [];
|
|
34
|
-
let tie = 0;
|
|
35
|
-
for (const message of trace.messages) {
|
|
36
|
-
blocks.push({
|
|
37
|
-
seq: message.seq ?? Number.MAX_SAFE_INTEGER,
|
|
38
|
-
tie: tie++,
|
|
39
|
-
text: `${message.role}:\n${message.content}`,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
for (const [index, call] of trace.toolCalls.entries()) {
|
|
43
|
-
blocks.push({
|
|
44
|
-
seq: call.seq ?? Number.MAX_SAFE_INTEGER,
|
|
45
|
-
tie: tie++,
|
|
46
|
-
text: `Tool calls:\n${formatOneToolCallForJudge(call, index)}`,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
blocks.sort((a, b) => a.seq - b.seq || a.tie - b.tie);
|
|
50
|
-
return blocks.map((block) => block.text).join("\n\n---\n\n");
|
|
51
|
-
}
|
|
52
|
-
/** Full live transcript for a judge, including tool names, args, and results. */
|
|
53
|
-
export function formatTraceForJudge(trace) {
|
|
54
|
-
const hasSeq = trace.messages.some((message) => message.seq !== undefined) ||
|
|
55
|
-
trace.toolCalls.some((call) => call.seq !== undefined);
|
|
56
|
-
const body = hasSeq
|
|
57
|
-
? formatTraceChronologically(trace)
|
|
58
|
-
: [...trace.messages.map((m) => `${m.role}:\n${m.content}`), formatToolCallsForJudge(trace)]
|
|
59
|
-
.filter(Boolean)
|
|
60
|
-
.join("\n\n---\n\n");
|
|
61
|
-
return [
|
|
62
|
-
body,
|
|
63
|
-
trace.prBody ?? "",
|
|
64
|
-
trace.gitDiff ? `Git diff:\n${trace.gitDiff}` : "",
|
|
65
|
-
trace.shellCommands.length > 0 ? `Shell commands:\n${trace.shellCommands.join("\n")}` : "",
|
|
66
|
-
]
|
|
67
|
-
.filter(Boolean)
|
|
68
|
-
.join("\n\n---\n\n");
|
|
69
|
-
}
|
|
70
|
-
/** Judge questions for mustInvokeSkill — replaces prose regex heuristics. */
|
|
71
|
-
export function skillInvokeJudgeCriteria(skillNames) {
|
|
72
|
-
return skillNames.map((name) => ({
|
|
73
|
-
id: `invoke-skill:${name}`,
|
|
74
|
-
question: [
|
|
75
|
-
`Did the agent actually follow the "${name}" skill?`,
|
|
76
|
-
"Use tool calls, file reads, and later actions as evidence.",
|
|
77
|
-
"Naming the skill or using similar words is not enough.",
|
|
78
|
-
"A host skill load counts as obtaining the skill body.",
|
|
79
|
-
"Do not fail only because a confirmatory SKILL.md read happens after the reply.",
|
|
80
|
-
].join(" "),
|
|
81
|
-
}));
|
|
82
|
-
}
|
|
83
|
-
function normalizeVerdict(value) {
|
|
84
|
-
const verdict = String(value ?? "")
|
|
85
|
-
.trim()
|
|
86
|
-
.toLowerCase();
|
|
87
|
-
if (verdict === "yes" || verdict === "no") {
|
|
88
|
-
return verdict;
|
|
89
|
-
}
|
|
90
|
-
return undefined;
|
|
91
|
-
}
|
|
92
|
-
// Whole-reply opening fence is a contract attempt regardless of info-string
|
|
93
|
-
// (`json`, `js`, `typescript`, bare, …). Mid-prose fence mentions do not match.
|
|
94
|
-
const JUDGE_JSON_FENCE_OPEN_PATTERN = /^```/;
|
|
95
|
-
const JUDGE_VERDICT_KEY_PATTERN = /"verdict"\s*:/;
|
|
96
|
-
/** Same prefixes legacy YES/NO strips — gate same-line JSON peels. */
|
|
97
|
-
const JUDGE_VERDICT_PREFIX_PATTERN = /^(?:answer|verdict|line\s*1)\s*:\s*/i;
|
|
98
|
-
/** Complete JSON number token (rejects bare `1.` with no fractional digits). */
|
|
99
|
-
const JUDGE_JSON_NUMBER_TOKEN = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/;
|
|
100
|
-
const JUDGE_JSON_BOOL_NULL_TOKEN = /^(?:true|false|null)/i;
|
|
101
|
-
/** Closed fences, or a trailing unclosed fence (prose preamble + open contract). */
|
|
102
|
-
const MARKDOWN_FENCE_STRIP_PATTERN = /```[^\n`]*\s*[\s\S]*?(?:```|$)/g;
|
|
103
|
-
/**
|
|
104
|
-
* After a complete JSON number/bool/null token: whether the remainder is
|
|
105
|
-
* contract junk (latch) vs English continuation (allow YES/NO salvage).
|
|
106
|
-
*
|
|
107
|
-
* `true\nYES` / `3 YES` / `42,` / `3, YES` → latch.
|
|
108
|
-
* `true story` / `3 findings` / `3, findings` / `true, the agent` → salvage.
|
|
109
|
-
*/
|
|
110
|
-
function jsonPrimitiveRemainderIsContract(rest) {
|
|
111
|
-
if (rest === "") {
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
const restTrim = rest.trimStart();
|
|
115
|
-
if (restTrim === "") {
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
118
|
-
// Comma after a primitive is JSON-ish only when nothing English follows
|
|
119
|
-
// (`42,` / `3, YES`). List/prose continuations (`3, findings`, `1, 2, and 3`)
|
|
120
|
-
// must still reach YES/NO salvage.
|
|
121
|
-
if (restTrim.startsWith(",")) {
|
|
122
|
-
const afterComma = restTrim.slice(1).trimStart();
|
|
123
|
-
if (afterComma === "") {
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
126
|
-
if (/^(?:yes|no)\b/i.test(afterComma)) {
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
if (/[A-Za-z]/.test(afterComma)) {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
const firstWord = /^([A-Za-z]+)/.exec(restTrim)?.[1]?.toLowerCase();
|
|
135
|
-
if (firstWord === "yes" || firstWord === "no") {
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
if (/^[A-Za-z]/.test(restTrim)) {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Detect truncated/unparseable JSON-shaped contract attempts. Only replies
|
|
145
|
-
* that *are* JSON-shaped (value prefix or open with a fence) count — a mid-prose
|
|
146
|
-
* mention of ```json or an incidental blob is not a contract attempt.
|
|
147
|
-
*
|
|
148
|
-
* Number/bool/null prefixes latch only for a complete JSON token (optionally
|
|
149
|
-
* with trailing junk). Numbered-list prose (`1. …`) and English continuations
|
|
150
|
-
* (`true story`, `3 findings`) must not latch — YES/NO salvage still applies.
|
|
151
|
-
*/
|
|
152
|
-
function looksLikeJudgeJsonAttempt(text) {
|
|
153
|
-
const trimmed = text.trim();
|
|
154
|
-
if (JUDGE_JSON_FENCE_OPEN_PATTERN.test(trimmed)) {
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
if (/^["{[]/.test(trimmed)) {
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
const number = JUDGE_JSON_NUMBER_TOKEN.exec(trimmed);
|
|
161
|
-
if (number) {
|
|
162
|
-
const rest = trimmed.slice(number[0].length);
|
|
163
|
-
// `1. prose` / `10) prose` — list markers, not number-then-junk.
|
|
164
|
-
if (/^[.)]/.test(rest)) {
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
return jsonPrimitiveRemainderIsContract(rest);
|
|
168
|
-
}
|
|
169
|
-
const boolNull = JUDGE_JSON_BOOL_NULL_TOKEN.exec(trimmed);
|
|
170
|
-
if (boolNull) {
|
|
171
|
-
return jsonPrimitiveRemainderIsContract(trimmed.slice(boolNull[0].length));
|
|
172
|
-
}
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
/** Drop fenced bodies so legacy YES/NO does not match inside quoted JSON strings. */
|
|
176
|
-
function stripMarkdownFencesForLegacy(text) {
|
|
177
|
-
return text.replace(MARKDOWN_FENCE_STRIP_PATTERN, "\n");
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* True when the JSON contract body is array-shaped (`[` before any `{`),
|
|
181
|
-
* including truncated arrays. Object-extraction must not peel an inner
|
|
182
|
-
* `{…}` out of those replies.
|
|
183
|
-
*/
|
|
184
|
-
function isArrayShapedJudgeJson(text) {
|
|
185
|
-
const trimmed = text.trim();
|
|
186
|
-
const fenced = trimmed.match(JSON_FENCE_PATTERN);
|
|
187
|
-
const body = fenced?.[1]?.trim() ?? trimmed;
|
|
188
|
-
const bracket = body.indexOf("[");
|
|
189
|
-
if (bracket === -1) {
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
192
|
-
const brace = body.indexOf("{");
|
|
193
|
-
return brace === -1 || bracket < brace;
|
|
194
|
-
}
|
|
195
|
-
const INVALID_JUDGE_JSON = {
|
|
196
|
-
pass: false,
|
|
197
|
-
rationale: "judge returned invalid JSON (expected { verdict, evidence, rationale })",
|
|
198
|
-
evidence: [],
|
|
199
|
-
valid: false,
|
|
200
|
-
};
|
|
201
|
-
function arrayLooksLikeJudgeContract(parsed) {
|
|
202
|
-
if (parsed.some((item) => item !== null &&
|
|
203
|
-
typeof item === "object" &&
|
|
204
|
-
!Array.isArray(item) &&
|
|
205
|
-
"verdict" in item)) {
|
|
206
|
-
return true;
|
|
207
|
-
}
|
|
208
|
-
// Wrong-shaped contract answers like `["yes"]` / `["no"]` — not incidental
|
|
209
|
-
// lists such as `[1,2,3]` or `["intro","summary"]`.
|
|
210
|
-
return (parsed.length > 0 &&
|
|
211
|
-
parsed.every((item) => typeof item === "string" && normalizeVerdict(item) !== undefined));
|
|
212
|
-
}
|
|
213
|
-
/** Peeled objects with judge-schema keys but no `verdict` are contract attempts. */
|
|
214
|
-
function objectLooksLikeJudgeSchema(record) {
|
|
215
|
-
return "evidence" in record || "rationale" in record;
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Try structured judge JSON. `structured` latches only for real contract
|
|
219
|
-
* attempts: whole-text/fenced JSON bodies, verdict-shaped arrays or objects,
|
|
220
|
-
* or failed candidates carrying a `verdict` key — not incidental arrays,
|
|
221
|
-
* mid-prose truncated blobs, or fence mentions inside prose.
|
|
222
|
-
*/
|
|
223
|
-
function tryParseJudgeJson(text) {
|
|
224
|
-
const trimmed = text.trim();
|
|
225
|
-
const candidates = [trimmed];
|
|
226
|
-
const fenced = trimmed.match(JSON_FENCE_PATTERN);
|
|
227
|
-
const fencedBody = fenced?.[1]?.trim();
|
|
228
|
-
if (fencedBody) {
|
|
229
|
-
candidates.unshift(fencedBody);
|
|
230
|
-
}
|
|
231
|
-
const arrayShaped = isArrayShapedJudgeJson(trimmed);
|
|
232
|
-
// Prose-prefixed single objects may still use greedy `{…}` extraction.
|
|
233
|
-
// Array-shaped replies (including truncated `[{…}`) must not peel inner objects.
|
|
234
|
-
if (!arrayShaped) {
|
|
235
|
-
const objectMatch = trimmed.match(JSON_OBJECT_PATTERN);
|
|
236
|
-
if (objectMatch?.[0]) {
|
|
237
|
-
candidates.push(objectMatch[0]);
|
|
238
|
-
}
|
|
239
|
-
const brace = trimmed.indexOf("{");
|
|
240
|
-
if (brace > 0) {
|
|
241
|
-
candidates.push(trimmed.slice(brace));
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
const bracket = trimmed.indexOf("[");
|
|
246
|
-
if (bracket >= 0) {
|
|
247
|
-
candidates.push(trimmed.slice(bracket));
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
// Contract peels under a prose preamble (not mid-prose incidental quotes):
|
|
251
|
-
// - line-leading `"…` / `[…` (`Answer:\n"yes"`)
|
|
252
|
-
// - line-leading JSON number/bool/null (`Answer:\n42\nYES`)
|
|
253
|
-
// - same-line after answer|verdict|line 1: (`Answer: "yes"`, `Verdict: 42`)
|
|
254
|
-
// Mid-line quotes without that prefix (`YES` + ```js\n"yes"\n```) stay
|
|
255
|
-
// incidental — fence bodies use `fencedBody` / whole-reply fence primary.
|
|
256
|
-
const lineLeadingJsonValues = new Set();
|
|
257
|
-
let insideFence = false;
|
|
258
|
-
const pushContractPeel = (value) => {
|
|
259
|
-
const peel = value.trim();
|
|
260
|
-
if (!peel) {
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
candidates.push(peel);
|
|
264
|
-
lineLeadingJsonValues.add(peel);
|
|
265
|
-
};
|
|
266
|
-
const isLineLeadingJsonPrimitive = (lineTrim) => {
|
|
267
|
-
if (lineTrim.startsWith('"') || lineTrim.startsWith("[")) {
|
|
268
|
-
return true;
|
|
269
|
-
}
|
|
270
|
-
// Complete number/bool/null lines (optional contract junk) — not English
|
|
271
|
-
// (`3 findings`, `true story`) which looksLikeJudgeJsonAttempt rejects.
|
|
272
|
-
return (looksLikeJudgeJsonAttempt(lineTrim) &&
|
|
273
|
-
(JUDGE_JSON_NUMBER_TOKEN.test(lineTrim) || JUDGE_JSON_BOOL_NULL_TOKEN.test(lineTrim)));
|
|
274
|
-
};
|
|
275
|
-
for (const line of trimmed.split("\n")) {
|
|
276
|
-
const lineTrim = line.trim();
|
|
277
|
-
if (lineTrim.startsWith("```")) {
|
|
278
|
-
insideFence = !insideFence;
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
if (insideFence) {
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
if (isLineLeadingJsonPrimitive(lineTrim)) {
|
|
285
|
-
pushContractPeel(lineTrim);
|
|
286
|
-
}
|
|
287
|
-
const prefix = JUDGE_VERDICT_PREFIX_PATTERN.exec(lineTrim);
|
|
288
|
-
if (prefix) {
|
|
289
|
-
const remainder = lineTrim.slice(prefix[0].length).trim();
|
|
290
|
-
if (remainder.startsWith('"') ||
|
|
291
|
-
remainder.startsWith("[") ||
|
|
292
|
-
remainder.startsWith("{") ||
|
|
293
|
-
isLineLeadingJsonPrimitive(remainder)) {
|
|
294
|
-
pushContractPeel(remainder);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
let structured = false;
|
|
299
|
-
let failedJsonShape = false;
|
|
300
|
-
// Only whole-reply fences are primary contract attempts. Mid-prose fences
|
|
301
|
-
// (e.g. YES + ```js\n"yes"\n```) stay incidental like unfenced peels.
|
|
302
|
-
const wholeReplyFence = JUDGE_JSON_FENCE_OPEN_PATTERN.test(trimmed);
|
|
303
|
-
for (const candidate of candidates) {
|
|
304
|
-
const candidateTrim = candidate.trim();
|
|
305
|
-
const primary = candidateTrim === trimmed ||
|
|
306
|
-
(wholeReplyFence && fencedBody !== undefined && candidateTrim === fencedBody);
|
|
307
|
-
const lineLeading = lineLeadingJsonValues.has(candidateTrim);
|
|
308
|
-
try {
|
|
309
|
-
const parsed = JSON.parse(candidate);
|
|
310
|
-
if (parsed === null || typeof parsed !== "object") {
|
|
311
|
-
// Whole-text/fenced JSON primitives (`"yes"`, `42`, `true`, `null`)
|
|
312
|
-
// are contract attempts that violate the object contract — refuse
|
|
313
|
-
// and latch so callers do not YES/NO-salvage a quoted verdict.
|
|
314
|
-
// Preamble peels (`Answer:\n"yes"`, `Answer: "yes"`, `Answer:\n42`)
|
|
315
|
-
// are the same wrong-shaped contract; mid-line incidental quote
|
|
316
|
-
// peels stay out of `lineLeadingJsonValues`.
|
|
317
|
-
if (primary || lineLeading) {
|
|
318
|
-
return { result: { ...INVALID_JUDGE_JSON }, structured: true };
|
|
319
|
-
}
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
if (Array.isArray(parsed)) {
|
|
323
|
-
// Whole-text/fenced arrays are contract attempts; peeled arrays count
|
|
324
|
-
// when verdict-shaped or yes/no string arrays (`["yes"]`). Incidental
|
|
325
|
-
// lists (e.g. `Scores: [1,2,3]`) still allow YES/NO salvage.
|
|
326
|
-
if (primary || arrayLooksLikeJudgeContract(parsed)) {
|
|
327
|
-
return { result: { ...INVALID_JUDGE_JSON }, structured: true };
|
|
328
|
-
}
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
const record = parsed;
|
|
332
|
-
// Objects without `verdict`: schema-shaped peels (`evidence`/`rationale`)
|
|
333
|
-
// are failed contract attempts; bare quote blobs stay incidental.
|
|
334
|
-
if (!("verdict" in record)) {
|
|
335
|
-
if (primary || objectLooksLikeJudgeSchema(record)) {
|
|
336
|
-
return { result: { ...INVALID_JUDGE_JSON }, structured: true };
|
|
337
|
-
}
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
structured = true;
|
|
341
|
-
const verdict = normalizeVerdict(record.verdict);
|
|
342
|
-
if (verdict !== undefined) {
|
|
343
|
-
return {
|
|
344
|
-
result: {
|
|
345
|
-
pass: verdict === "yes",
|
|
346
|
-
rationale: String(record.rationale ?? "").trim() || "no rationale",
|
|
347
|
-
evidence: Array.isArray(record.evidence)
|
|
348
|
-
? record.evidence.map((item) => String(item)).filter(Boolean)
|
|
349
|
-
: [],
|
|
350
|
-
valid: true,
|
|
351
|
-
},
|
|
352
|
-
structured: true,
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
return { result: { ...INVALID_JUDGE_JSON }, structured: true };
|
|
356
|
-
}
|
|
357
|
-
catch {
|
|
358
|
-
// A failed parse latches `structured` only for real contract attempts:
|
|
359
|
-
// the whole/fenced body is a JSON value (object/array/primitive),
|
|
360
|
-
// including trailing junk after a primitive (`"yes" clearly`), or a
|
|
361
|
-
// peeled candidate carries a `"verdict":` key (truncated contract).
|
|
362
|
-
// Line-leading string/array peels (`Answer:\n"yes" clearly`,
|
|
363
|
-
// `Answer:\n["yes"`) latch the same way. Mid-prose incidental peels
|
|
364
|
-
// (e.g. `Evidence: {"quote":"hello"`) keep YES/NO salvage available.
|
|
365
|
-
if (primary) {
|
|
366
|
-
if (looksLikeJudgeJsonAttempt(candidateTrim)) {
|
|
367
|
-
failedJsonShape = true;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
else if ((candidateTrim.startsWith("{") || candidateTrim.startsWith("[")) &&
|
|
371
|
-
JUDGE_VERDICT_KEY_PATTERN.test(candidateTrim)) {
|
|
372
|
-
failedJsonShape = true;
|
|
373
|
-
}
|
|
374
|
-
else if (lineLeading && looksLikeJudgeJsonAttempt(candidateTrim)) {
|
|
375
|
-
failedJsonShape = true;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
return {
|
|
380
|
-
result: { ...INVALID_JUDGE_JSON },
|
|
381
|
-
structured: structured || failedJsonShape || looksLikeJudgeJsonAttempt(trimmed),
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
/** Parse structured judge JSON (`verdict`, `evidence`, `rationale`). */
|
|
385
|
-
export function parseJudgeJsonResponse(text) {
|
|
386
|
-
return tryParseJudgeJson(text).result;
|
|
387
|
-
}
|
|
388
|
-
const JUDGE_MARKDOWN_TRIM_PATTERN = /^\*+\s*|\s*\*+$/g;
|
|
389
|
-
const JUDGE_TRAILING_PUNCT_PATTERN = /[.:!]+$/g;
|
|
390
|
-
const JUDGE_TAIL_VERDICT_PATTERN = /\b(YES|NO)\.?\s*$/i;
|
|
391
|
-
const JUDGE_INLINE_VERDICT_PATTERN = /\b(YES|NO)\b/i;
|
|
392
|
-
function extractLegacyVerdict(line) {
|
|
393
|
-
const stripped = line
|
|
394
|
-
.replace(JUDGE_MARKDOWN_TRIM_PATTERN, "")
|
|
395
|
-
.replace(JUDGE_VERDICT_PREFIX_PATTERN, "")
|
|
396
|
-
.trim();
|
|
397
|
-
const head = stripped.replace(JUDGE_TRAILING_PUNCT_PATTERN, "").toUpperCase();
|
|
398
|
-
if (head === "YES" || head.startsWith("YES ")) {
|
|
399
|
-
return "yes";
|
|
400
|
-
}
|
|
401
|
-
if (head === "NO" || head.startsWith("NO ")) {
|
|
402
|
-
return "no";
|
|
403
|
-
}
|
|
404
|
-
const tailMatch = stripped.match(JUDGE_TAIL_VERDICT_PATTERN);
|
|
405
|
-
if (tailMatch?.[1]) {
|
|
406
|
-
return tailMatch[1].toLowerCase();
|
|
407
|
-
}
|
|
408
|
-
const inlineMatch = stripped.match(JUDGE_INLINE_VERDICT_PATTERN);
|
|
409
|
-
if (inlineMatch?.[1]) {
|
|
410
|
-
return inlineMatch[1].toLowerCase();
|
|
411
|
-
}
|
|
412
|
-
return undefined;
|
|
413
|
-
}
|
|
414
|
-
/** Fallback when model ignores JSON contract — parse YES/NO prose. */
|
|
415
|
-
export function parseJudgeLegacyResponse(text) {
|
|
416
|
-
const lines = text
|
|
417
|
-
.trim()
|
|
418
|
-
.split("\n")
|
|
419
|
-
.map((line) => line.trim())
|
|
420
|
-
.filter(Boolean);
|
|
421
|
-
for (let index = 0; index < Math.min(lines.length, 3); index++) {
|
|
422
|
-
const line = lines[index];
|
|
423
|
-
if (!line) {
|
|
424
|
-
continue;
|
|
425
|
-
}
|
|
426
|
-
const verdict = extractLegacyVerdict(line);
|
|
427
|
-
if (verdict !== undefined) {
|
|
428
|
-
const rationale = lines
|
|
429
|
-
.filter((_, i) => i !== index)
|
|
430
|
-
.join(" ")
|
|
431
|
-
.trim() || line.replace(JUDGE_TAIL_VERDICT_PATTERN, "").trim();
|
|
432
|
-
return {
|
|
433
|
-
pass: verdict === "yes",
|
|
434
|
-
rationale: rationale || line,
|
|
435
|
-
evidence: [],
|
|
436
|
-
valid: true,
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
const fallback = extractLegacyVerdict(lines[0] ?? text.trim());
|
|
441
|
-
if (fallback !== undefined) {
|
|
442
|
-
return {
|
|
443
|
-
pass: fallback === "yes",
|
|
444
|
-
rationale: lines.slice(1).join(" ").trim() || lines[0] || text,
|
|
445
|
-
evidence: [],
|
|
446
|
-
valid: true,
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
return {
|
|
450
|
-
pass: false,
|
|
451
|
-
rationale: "judge returned invalid JSON (expected { verdict, evidence, rationale })",
|
|
452
|
-
evidence: [],
|
|
453
|
-
valid: false,
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* Parse structured JSON first, then legacy YES/NO prose.
|
|
458
|
-
* Once a real JSON contract attempt is detected (array, object with `verdict`,
|
|
459
|
-
* or truncated JSON-shaped text), never salvage YES/NO from the body — invalid
|
|
460
|
-
* contracts are infra failures, not rubric answers.
|
|
461
|
-
*/
|
|
462
|
-
export function parseJudgeResponse(text) {
|
|
463
|
-
const { result, structured } = tryParseJudgeJson(text);
|
|
464
|
-
if (result.valid || structured) {
|
|
465
|
-
return result;
|
|
466
|
-
}
|
|
467
|
-
// Strip fences so preamble + ```json\n"yes"\n``` cannot salvage from the
|
|
468
|
-
// quoted primitive; outer YES/NO beside incidental fences still works.
|
|
469
|
-
return parseJudgeLegacyResponse(stripMarkdownFencesForLegacy(text));
|
|
470
|
-
}
|
|
471
|
-
function buildJudgePrompt(transcript, question, workspaces) {
|
|
472
|
-
return [
|
|
473
|
-
"You are a test harness classifier. You may inspect the explicitly listed read-only workspace evidence with file-read tools or read-only shell commands such as cat, sed, rg, ls, and stat. Shell commands may only inspect those evidence paths. Do not execute project code, edit files, install packages, use network access, or inspect any other path.",
|
|
474
|
-
"Decide whether the transcript and workspace evidence satisfy the criterion.",
|
|
475
|
-
"The transcript includes assistant text, tool names, tool args, and tool results.",
|
|
476
|
-
"A tool result is an outcome. Use it as evidence when the criterion asks about one.",
|
|
477
|
-
"Reply with one JSON object only — no markdown fences, no text before or after:",
|
|
478
|
-
'{"verdict":"yes"|"no","evidence":["exact direct quote from transcript"],"rationale":"one sentence"}',
|
|
479
|
-
"Evidence must contain one or more exact, contiguous excerpts copied from the transcript in double quotes.",
|
|
480
|
-
"Do not paraphrase, summarize, infer, or invent evidence. If no exact quote supports the verdict, return an empty evidence array.",
|
|
481
|
-
'Use verdict "yes" only when evidence clearly supports the criterion.',
|
|
482
|
-
...formatJudgeWorkspaces(workspaces),
|
|
483
|
-
"",
|
|
484
|
-
"Transcript:",
|
|
485
|
-
transcript,
|
|
486
|
-
"",
|
|
487
|
-
`Criterion: ${question}`,
|
|
488
|
-
].join("\n");
|
|
489
|
-
}
|
|
490
|
-
function formatJudgeWorkspaces(workspaces) {
|
|
491
|
-
if (!workspaces?.length)
|
|
492
|
-
return ["Workspace evidence: none was provided."];
|
|
493
|
-
return [
|
|
494
|
-
"Read-only workspace evidence (use only these paths; each path is immutable):",
|
|
495
|
-
...workspaces.map((workspace) => `- ${workspace.name}: ${workspace.path}`),
|
|
496
|
-
];
|
|
497
|
-
}
|
|
498
|
-
export function buildCompareJudgePrompt(options) {
|
|
499
|
-
return [
|
|
500
|
-
"You are a test harness classifier. You may inspect the explicitly listed read-only workspace evidence with file-read tools or read-only shell commands such as cat, sed, rg, ls, and stat. Shell commands may only inspect those evidence paths. Do not execute project code, edit files, install packages, use network access, or inspect any other path.",
|
|
501
|
-
"Decide whether the transcripts and workspace evidence satisfy the criterion.",
|
|
502
|
-
"Arm A and arm B are two agent runs of the same test.",
|
|
503
|
-
"The transcripts include assistant text, tool names, tool args, and tool results.",
|
|
504
|
-
"A tool result is an outcome. Use it as evidence when the criterion asks about one.",
|
|
505
|
-
"Reply with one JSON object only — no markdown fences, no text before or after:",
|
|
506
|
-
'{"verdict":"yes"|"no","evidence":["exact direct quote from a transcript"],"rationale":"one sentence"}',
|
|
507
|
-
"Evidence must contain one or more exact, contiguous excerpts copied from a transcript in double quotes.",
|
|
508
|
-
"Do not paraphrase, summarize, infer, or invent evidence. If no exact quote supports the verdict, return an empty evidence array.",
|
|
509
|
-
'Use verdict "yes" only when evidence clearly supports the criterion.',
|
|
510
|
-
...formatJudgeWorkspaces(options.workspaces),
|
|
511
|
-
"",
|
|
512
|
-
`Arm A (${options.aLabel}):`,
|
|
513
|
-
options.aTranscript,
|
|
514
|
-
"",
|
|
515
|
-
`Arm B (${options.bLabel}):`,
|
|
516
|
-
options.bTranscript,
|
|
517
|
-
"",
|
|
518
|
-
`Criterion: ${options.question}`,
|
|
519
|
-
].join("\n");
|
|
520
|
-
}
|
|
521
|
-
export function buildMultiArmCompareJudgePrompt(options) {
|
|
522
|
-
const armBlocks = options.arms.flatMap((arm) => [`Arm ${arm.label}:`, arm.transcript, ""]);
|
|
523
|
-
return [
|
|
524
|
-
"You are a test harness classifier. You may inspect the explicitly listed read-only workspace evidence with file-read tools or read-only shell commands such as cat, sed, rg, ls, and stat. Shell commands may only inspect those evidence paths. Do not execute project code, edit files, install packages, use network access, or inspect any other path.",
|
|
525
|
-
"Decide whether the transcripts and workspace evidence satisfy the criterion.",
|
|
526
|
-
"Each arm is one agent run of the same test.",
|
|
527
|
-
"Do not pick a single winner unless the criterion asks for one.",
|
|
528
|
-
"The transcripts include assistant text, tool names, tool args, and tool results.",
|
|
529
|
-
"A tool result is an outcome. Use it as evidence when the criterion asks about one.",
|
|
530
|
-
"Reply with one JSON object only — no markdown fences, no text before or after:",
|
|
531
|
-
'{"verdict":"yes"|"no","evidence":["exact direct quote from a transcript"],"rationale":"one sentence"}',
|
|
532
|
-
"Evidence must contain one or more exact, contiguous excerpts copied from a transcript in double quotes.",
|
|
533
|
-
"Do not paraphrase, summarize, infer, or invent evidence. If no exact quote supports the verdict, return an empty evidence array.",
|
|
534
|
-
'Use verdict "yes" only when evidence clearly supports the criterion.',
|
|
535
|
-
...formatJudgeWorkspaces(options.workspaces),
|
|
536
|
-
"",
|
|
537
|
-
...armBlocks,
|
|
538
|
-
`Criterion: ${options.question}`,
|
|
539
|
-
].join("\n");
|
|
540
|
-
}
|
|
541
|
-
function formatJudgeInfraError(result) {
|
|
542
|
-
const details = [];
|
|
543
|
-
if (result.rawStatus) {
|
|
544
|
-
details.push(`sdk: ${result.rawStatus}`);
|
|
545
|
-
}
|
|
546
|
-
if (result.sdkError?.code) {
|
|
547
|
-
details.push(`code: ${result.sdkError.code}`);
|
|
548
|
-
}
|
|
549
|
-
if (result.sdkError?.message) {
|
|
550
|
-
details.push(`error: ${result.sdkError.message}`);
|
|
551
|
-
}
|
|
552
|
-
if (details.length === 0) {
|
|
553
|
-
return `judge run status: ${result.status}`;
|
|
554
|
-
}
|
|
555
|
-
return `judge run status: ${result.status} (${details.join(", ")})`;
|
|
556
|
-
}
|
|
557
|
-
async function runJudgePromptOnce(prompt, options) {
|
|
558
|
-
const started = performance.now();
|
|
559
|
-
const host = options.host ?? "cursor";
|
|
560
|
-
const classify = options.classify ?? runClassifier;
|
|
561
|
-
const result = await classify({
|
|
562
|
-
host,
|
|
563
|
-
cwd: options.workspaces?.[0]?.path ?? options.cwd,
|
|
564
|
-
prompt,
|
|
565
|
-
apiKey: options.apiKey,
|
|
566
|
-
workspaces: options.workspaces,
|
|
567
|
-
onText: options.onText,
|
|
568
|
-
});
|
|
569
|
-
const durationMs = Math.round(performance.now() - started);
|
|
570
|
-
const usage = result.usage;
|
|
571
|
-
if (result.status !== "completed") {
|
|
572
|
-
const infraError = formatJudgeInfraError(result);
|
|
573
|
-
return {
|
|
574
|
-
pass: false,
|
|
575
|
-
rationale: infraError,
|
|
576
|
-
evidence: [],
|
|
577
|
-
error: infraError,
|
|
578
|
-
infraError,
|
|
579
|
-
rawSdkStatus: result.rawStatus ?? result.status,
|
|
580
|
-
sdkError: result.sdkError,
|
|
581
|
-
durationMs,
|
|
582
|
-
usage,
|
|
583
|
-
retryable: isTransientInfraError(infraError),
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
const parsed = parseJudgeResponse(result.text);
|
|
587
|
-
if (!parsed.valid) {
|
|
588
|
-
const rationale = `${parsed.rationale} (raw: ${result.text.slice(0, 160) || "empty"})`;
|
|
589
|
-
return {
|
|
590
|
-
pass: false,
|
|
591
|
-
rationale,
|
|
592
|
-
evidence: [],
|
|
593
|
-
response: result.text,
|
|
594
|
-
error: rationale,
|
|
595
|
-
parseError: rationale,
|
|
596
|
-
durationMs,
|
|
597
|
-
usage,
|
|
598
|
-
retryable: false,
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
return {
|
|
602
|
-
pass: parsed.pass,
|
|
603
|
-
rationale: parsed.rationale,
|
|
604
|
-
evidence: parsed.evidence,
|
|
605
|
-
response: result.text,
|
|
606
|
-
durationMs,
|
|
607
|
-
usage,
|
|
608
|
-
retryable: false,
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
async function runJudgePrompt(prompt, options) {
|
|
612
|
-
const host = options.host ?? "cursor";
|
|
613
|
-
const missing = missingClassifierAuth(host, options.apiKey);
|
|
614
|
-
if (missing) {
|
|
615
|
-
return {
|
|
616
|
-
pass: false,
|
|
617
|
-
rationale: missing,
|
|
618
|
-
evidence: [],
|
|
619
|
-
error: "missing api key",
|
|
620
|
-
infraError: "missing api key",
|
|
621
|
-
durationMs: 0,
|
|
622
|
-
attempt: 1,
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
const maxAttempts = resolveRetryMaxAttempts();
|
|
626
|
-
const started = performance.now();
|
|
627
|
-
let lastAttempt = 1;
|
|
628
|
-
try {
|
|
629
|
-
const { result, attempt } = await withRetry(async (attemptNumber) => {
|
|
630
|
-
lastAttempt = attemptNumber;
|
|
631
|
-
const outcome = await runJudgePromptOnce(prompt, options);
|
|
632
|
-
if (outcome.retryable && attemptNumber < maxAttempts) {
|
|
633
|
-
throw new Error(outcome.infraError ?? outcome.error ?? "judge infra error");
|
|
634
|
-
}
|
|
635
|
-
return outcome;
|
|
636
|
-
}, {
|
|
637
|
-
maxAttempts,
|
|
638
|
-
shouldRetry: (error) => isTransientInfraError(error instanceof Error ? error.message : String(error)),
|
|
639
|
-
});
|
|
640
|
-
return {
|
|
641
|
-
...result,
|
|
642
|
-
durationMs: Math.round(performance.now() - started),
|
|
643
|
-
attempt,
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
catch (error) {
|
|
647
|
-
const message = error instanceof Error ? error.message : "judge prompt failed";
|
|
648
|
-
return {
|
|
649
|
-
pass: false,
|
|
650
|
-
rationale: message,
|
|
651
|
-
evidence: [],
|
|
652
|
-
error: message,
|
|
653
|
-
infraError: message,
|
|
654
|
-
durationMs: Math.round(performance.now() - started),
|
|
655
|
-
attempt: lastAttempt,
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
/** Score fuzzy rubric criteria with a structured JSON judge (live runs only). */
|
|
660
|
-
export async function judgeTrace(trace, criteria, options) {
|
|
661
|
-
if (criteria.length === 0) {
|
|
662
|
-
return { verdicts: [], skipped: true };
|
|
663
|
-
}
|
|
664
|
-
const host = options.host ?? "cursor";
|
|
665
|
-
const missing = missingClassifierAuth(host, options.apiKey);
|
|
666
|
-
if (missing) {
|
|
667
|
-
return {
|
|
668
|
-
verdicts: [],
|
|
669
|
-
skipped: true,
|
|
670
|
-
error: `${missing} — judge criteria skipped`,
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
const transcript = formatTraceForJudge(trace);
|
|
674
|
-
return judgeCriteria(criteria, (question) => buildJudgePrompt(transcript, question, options.workspaces), options, {
|
|
675
|
-
transcriptChars: transcript.length,
|
|
676
|
-
workspaceEvidence: options.workspaces?.map((workspace) => workspace.name),
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
function compareJudgeArms(input) {
|
|
680
|
-
if ("arms" in input) {
|
|
681
|
-
return input.arms;
|
|
682
|
-
}
|
|
683
|
-
return [
|
|
684
|
-
{ label: input.aLabel, trace: input.a },
|
|
685
|
-
{ label: input.bLabel, trace: input.b },
|
|
686
|
-
];
|
|
687
|
-
}
|
|
688
|
-
function isCompareJudgePair(input) {
|
|
689
|
-
return "a" in input && "b" in input;
|
|
690
|
-
}
|
|
691
|
-
/** Score criteria against every arm transcript. */
|
|
692
|
-
export async function judgeCompareTraces(input, criteria, options) {
|
|
693
|
-
if (criteria.length === 0) {
|
|
694
|
-
return { verdicts: [], skipped: true };
|
|
695
|
-
}
|
|
696
|
-
const host = options.host ?? "cursor";
|
|
697
|
-
const missing = missingClassifierAuth(host, options.apiKey);
|
|
698
|
-
if (missing) {
|
|
699
|
-
return {
|
|
700
|
-
verdicts: [],
|
|
701
|
-
skipped: true,
|
|
702
|
-
error: `${missing} — judge criteria skipped`,
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
const pair = isCompareJudgePair(input)
|
|
706
|
-
? input
|
|
707
|
-
: input.arms.length === 2 && input.arms[0] && input.arms[1]
|
|
708
|
-
? {
|
|
709
|
-
aLabel: input.arms[0].label,
|
|
710
|
-
a: input.arms[0].trace,
|
|
711
|
-
bLabel: input.arms[1].label,
|
|
712
|
-
b: input.arms[1].trace,
|
|
713
|
-
}
|
|
714
|
-
: undefined;
|
|
715
|
-
if (pair) {
|
|
716
|
-
const aTranscript = formatTraceForJudge(pair.a);
|
|
717
|
-
const bTranscript = formatTraceForJudge(pair.b);
|
|
718
|
-
return judgeCriteria(criteria, (question) => buildCompareJudgePrompt({
|
|
719
|
-
aLabel: pair.aLabel,
|
|
720
|
-
aTranscript,
|
|
721
|
-
bLabel: pair.bLabel,
|
|
722
|
-
bTranscript,
|
|
723
|
-
question,
|
|
724
|
-
workspaces: options.workspaces,
|
|
725
|
-
}), options, {
|
|
726
|
-
transcriptChars: aTranscript.length + bTranscript.length,
|
|
727
|
-
workspaceEvidence: options.workspaces?.map((workspace) => workspace.name),
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
const arms = compareJudgeArms(input).map((arm) => ({
|
|
731
|
-
label: arm.label,
|
|
732
|
-
transcript: formatTraceForJudge(arm.trace),
|
|
733
|
-
}));
|
|
734
|
-
return judgeCriteria(criteria, (question) => buildMultiArmCompareJudgePrompt({ arms, question, workspaces: options.workspaces }), options, {
|
|
735
|
-
transcriptChars: arms.reduce((sum, arm) => sum + arm.transcript.length, 0),
|
|
736
|
-
workspaceEvidence: options.workspaces?.map((workspace) => workspace.name),
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
async function judgeCriteria(criteria, buildPrompt, options, meta) {
|
|
740
|
-
const verdicts = [];
|
|
741
|
-
for (const criterion of criteria) {
|
|
742
|
-
const prompt = buildPrompt(criterion.question);
|
|
743
|
-
options.onEvent?.({
|
|
744
|
-
type: "criterion_started",
|
|
745
|
-
id: criterion.id,
|
|
746
|
-
question: criterion.question,
|
|
747
|
-
});
|
|
748
|
-
const parsed = await runJudgePrompt(prompt, {
|
|
749
|
-
...options,
|
|
750
|
-
onText: (text) => {
|
|
751
|
-
options.onText?.(text);
|
|
752
|
-
options.onEvent?.({
|
|
753
|
-
type: "text",
|
|
754
|
-
id: criterion.id,
|
|
755
|
-
question: criterion.question,
|
|
756
|
-
text,
|
|
757
|
-
});
|
|
758
|
-
},
|
|
759
|
-
});
|
|
760
|
-
const verdict = {
|
|
761
|
-
id: criterion.id,
|
|
762
|
-
pass: parsed.pass,
|
|
763
|
-
rationale: parsed.rationale,
|
|
764
|
-
evidence: parsed.evidence,
|
|
765
|
-
prompt,
|
|
766
|
-
response: parsed.response,
|
|
767
|
-
infraError: parsed.infraError,
|
|
768
|
-
parseError: parsed.parseError,
|
|
769
|
-
rawSdkStatus: parsed.rawSdkStatus,
|
|
770
|
-
sdkError: parsed.sdkError,
|
|
771
|
-
attempt: parsed.attempt,
|
|
772
|
-
durationMs: parsed.durationMs,
|
|
773
|
-
usage: parsed.usage,
|
|
774
|
-
transcriptChars: meta.transcriptChars,
|
|
775
|
-
promptChars: prompt.length,
|
|
776
|
-
workspaceEvidence: meta.workspaceEvidence,
|
|
777
|
-
};
|
|
778
|
-
verdicts.push(verdict);
|
|
779
|
-
options.onEvent?.({ type: "criterion_finished", question: criterion.question, verdict });
|
|
780
|
-
if (parsed.error) {
|
|
781
|
-
return {
|
|
782
|
-
verdicts,
|
|
783
|
-
skipped: false,
|
|
784
|
-
error: parsed.error,
|
|
785
|
-
usage: sumUsageParts(verdicts.map((v) => v.usage)),
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
return {
|
|
790
|
-
verdicts,
|
|
791
|
-
skipped: false,
|
|
792
|
-
usage: sumUsageParts(verdicts.map((v) => v.usage)),
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
//# sourceMappingURL=judge.js.map
|