@hachej/boring-agent 0.1.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/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/chunk-W73RUHY3.js +77 -0
- package/dist/eval/index.d.ts +241 -0
- package/dist/eval/index.js +508 -0
- package/dist/front/index.d.ts +394 -0
- package/dist/front/index.js +5026 -0
- package/dist/front/styles.css +4712 -0
- package/dist/harness-CMiJ4kok.d.ts +62 -0
- package/dist/sandbox-handle-store-OPdvRwie.d.ts +283 -0
- package/dist/server/index.d.ts +327 -0
- package/dist/server/index.js +6649 -0
- package/dist/shared/index.d.ts +225 -0
- package/dist/shared/index.js +75 -0
- package/package.json +132 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
// src/eval/evalPrompt.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
|
|
4
|
+
// src/eval/types.ts
|
|
5
|
+
var EvalAny = /* @__PURE__ */ Symbol.for("@hachej/boring-agent/eval/EvalAny");
|
|
6
|
+
function EvalRegex(re) {
|
|
7
|
+
return { __evalRegex: typeof re === "string" ? new RegExp(re) : re };
|
|
8
|
+
}
|
|
9
|
+
function isEvalRegex(value) {
|
|
10
|
+
return typeof value === "object" && value !== null && "__evalRegex" in value && value.__evalRegex instanceof RegExp;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/eval/matcher.ts
|
|
14
|
+
function callSatisfies(expected, actual) {
|
|
15
|
+
if (expected.tool !== actual.tool) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
matchedIndex: -1,
|
|
19
|
+
reason: `tool mismatch: expected "${expected.tool}", got "${actual.tool}"`
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (!expected.params) {
|
|
23
|
+
return { ok: true, matchedIndex: 0 };
|
|
24
|
+
}
|
|
25
|
+
const paramsCheck = matchParams(expected.params, actual.params, expected.strict ?? false);
|
|
26
|
+
if (!paramsCheck.ok) {
|
|
27
|
+
return { ok: false, matchedIndex: -1, reason: paramsCheck.reason };
|
|
28
|
+
}
|
|
29
|
+
return { ok: true, matchedIndex: 0 };
|
|
30
|
+
}
|
|
31
|
+
function someCallMatches(expected, actual) {
|
|
32
|
+
const expectedList = Array.isArray(expected) ? expected : [expected];
|
|
33
|
+
for (const exp of expectedList) {
|
|
34
|
+
let matched = false;
|
|
35
|
+
let lastReason;
|
|
36
|
+
for (const cand of actual) {
|
|
37
|
+
const out = callSatisfies(exp, cand);
|
|
38
|
+
if (out.ok) {
|
|
39
|
+
matched = true;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
lastReason = out.reason;
|
|
43
|
+
}
|
|
44
|
+
if (!matched) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
matchedIndex: -1,
|
|
48
|
+
reason: lastReason ? `no actual call matched expected ${formatExpected(exp)} \u2014 closest mismatch: ${lastReason}` : `no actual call matched expected ${formatExpected(exp)} (actual was empty)`
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { ok: true, matchedIndex: 0 };
|
|
53
|
+
}
|
|
54
|
+
function firstCallMatches(expected, actual) {
|
|
55
|
+
if (actual.length === 0) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
matchedIndex: -1,
|
|
59
|
+
reason: `expectFirst: no tool calls captured (LLM answered in plain text)`
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return callSatisfies(expected, actual[0]);
|
|
63
|
+
}
|
|
64
|
+
function noToolCallMatches(actual) {
|
|
65
|
+
if (actual.length === 0) {
|
|
66
|
+
return { ok: true, matchedIndex: -1 };
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
matchedIndex: -1,
|
|
71
|
+
reason: `expectNoToolCall: ${actual.length} tool call(s) captured: ${actual.map((c) => c.tool).join(", ")}`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function matchParams(expected, actual, strict, pathPrefix = "") {
|
|
75
|
+
for (const key of Object.keys(expected)) {
|
|
76
|
+
const expVal = expected[key];
|
|
77
|
+
const actVal = actual[key];
|
|
78
|
+
const path = pathPrefix ? `${pathPrefix}.${key}` : key;
|
|
79
|
+
if (expVal === EvalAny || isEvalAnySymbol(expVal)) {
|
|
80
|
+
if (actVal === void 0) {
|
|
81
|
+
return { ok: false, reason: `${path}: missing (expected !EvalAny)` };
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (isEvalRegex(expVal)) {
|
|
86
|
+
if (typeof actVal !== "string") {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
reason: `${path}: expected string for !EvalRegex, got ${typeof actVal}`
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (!expVal.__evalRegex.test(actVal)) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
reason: `${path}: ${JSON.stringify(actVal)} does not match /${expVal.__evalRegex.source}/`
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (typeof expVal === "object" && expVal !== null && !Array.isArray(expVal)) {
|
|
101
|
+
if (typeof actVal !== "object" || actVal === null || Array.isArray(actVal)) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
reason: `${path}: expected object, got ${describe(actVal)}`
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const nested = matchParams(
|
|
108
|
+
expVal,
|
|
109
|
+
actVal,
|
|
110
|
+
strict,
|
|
111
|
+
path
|
|
112
|
+
);
|
|
113
|
+
if (!nested.ok) return nested;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (Array.isArray(expVal)) {
|
|
117
|
+
if (!Array.isArray(actVal)) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
reason: `${path}: expected array, got ${describe(actVal)}`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (expVal.length !== actVal.length) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
reason: `${path}: array length mismatch (expected ${expVal.length}, got ${actVal.length})`
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
for (let i = 0; i < expVal.length; i++) {
|
|
130
|
+
const elemMatch = matchParams(
|
|
131
|
+
{ item: expVal[i] },
|
|
132
|
+
{ item: actVal[i] },
|
|
133
|
+
strict,
|
|
134
|
+
`${path}[${i}]`
|
|
135
|
+
);
|
|
136
|
+
if (!elemMatch.ok) return elemMatch;
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (expVal !== actVal) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
reason: `${path}: expected ${JSON.stringify(expVal)}, got ${JSON.stringify(actVal)}`
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (strict) {
|
|
148
|
+
for (const key of Object.keys(actual)) {
|
|
149
|
+
if (!(key in expected)) {
|
|
150
|
+
const path = pathPrefix ? `${pathPrefix}.${key}` : key;
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
reason: `${path}: unexpected key (strict mode)`
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { ok: true };
|
|
159
|
+
}
|
|
160
|
+
function isEvalAnySymbol(value) {
|
|
161
|
+
return value === EvalAny;
|
|
162
|
+
}
|
|
163
|
+
function describe(value) {
|
|
164
|
+
if (value === null) return "null";
|
|
165
|
+
if (Array.isArray(value)) return "array";
|
|
166
|
+
if (value === void 0) return "undefined";
|
|
167
|
+
return typeof value;
|
|
168
|
+
}
|
|
169
|
+
function formatExpected(exp) {
|
|
170
|
+
if (!exp.params) return `{tool: ${exp.tool}}`;
|
|
171
|
+
return `{tool: ${exp.tool}, params: ${safeStringify(exp.params)}}`;
|
|
172
|
+
}
|
|
173
|
+
function safeStringify(value) {
|
|
174
|
+
try {
|
|
175
|
+
return JSON.stringify(value, (_k, v) => {
|
|
176
|
+
if (v === EvalAny) return "<EvalAny>";
|
|
177
|
+
if (isEvalRegex(v)) return `<EvalRegex /${v.__evalRegex.source}/>`;
|
|
178
|
+
return v;
|
|
179
|
+
});
|
|
180
|
+
} catch {
|
|
181
|
+
return "<unserializable>";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/eval/evalConfig.ts
|
|
186
|
+
var DEFAULT_EVAL_MODEL = { provider: "openrouter", id: "qwen/qwen3.6-plus" };
|
|
187
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
188
|
+
var DEFAULT_SUITE_TIMEOUT_MS = 5 * 6e4;
|
|
189
|
+
var DEFAULT_CONCURRENCY = 4;
|
|
190
|
+
|
|
191
|
+
// src/eval/evalPrompt.ts
|
|
192
|
+
async function evalAgentPrompt(opts) {
|
|
193
|
+
validateMutuallyExclusive(opts);
|
|
194
|
+
const sessionId = opts.sessionId ?? randomUUID();
|
|
195
|
+
const model = parseModelString(opts.model ?? DEFAULT_EVAL_MODEL);
|
|
196
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
197
|
+
const maxRetries = opts.retries ?? 0;
|
|
198
|
+
let attempts = 0;
|
|
199
|
+
let lastResult = null;
|
|
200
|
+
while (attempts <= maxRetries) {
|
|
201
|
+
attempts += 1;
|
|
202
|
+
let captured;
|
|
203
|
+
try {
|
|
204
|
+
captured = await runOnce(opts.app, {
|
|
205
|
+
sessionId: `${sessionId}-attempt-${attempts}`,
|
|
206
|
+
prompt: opts.prompt,
|
|
207
|
+
systemPrompt: opts.systemPrompt,
|
|
208
|
+
model,
|
|
209
|
+
timeoutMs
|
|
210
|
+
});
|
|
211
|
+
} catch (err) {
|
|
212
|
+
lastResult = {
|
|
213
|
+
ok: false,
|
|
214
|
+
actual: [],
|
|
215
|
+
text: "",
|
|
216
|
+
attempts,
|
|
217
|
+
reason: `transport: ${err instanceof Error ? err.message : String(err)}`
|
|
218
|
+
};
|
|
219
|
+
if (attempts > maxRetries) return lastResult;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const matchResult = matchExpectations(opts, captured.toolCalls);
|
|
223
|
+
lastResult = {
|
|
224
|
+
ok: matchResult.ok,
|
|
225
|
+
actual: captured.toolCalls,
|
|
226
|
+
text: captured.text,
|
|
227
|
+
usage: captured.usage,
|
|
228
|
+
attempts,
|
|
229
|
+
reason: matchResult.ok ? void 0 : matchResult.reason
|
|
230
|
+
};
|
|
231
|
+
if (matchResult.ok) return lastResult;
|
|
232
|
+
}
|
|
233
|
+
return lastResult ?? {
|
|
234
|
+
ok: false,
|
|
235
|
+
actual: [],
|
|
236
|
+
text: "",
|
|
237
|
+
attempts,
|
|
238
|
+
reason: "no attempts ran"
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function validateMutuallyExclusive(opts) {
|
|
242
|
+
const set = [
|
|
243
|
+
opts.expect !== void 0,
|
|
244
|
+
opts.expectFirst !== void 0,
|
|
245
|
+
opts.expectNoToolCall === true
|
|
246
|
+
].filter(Boolean).length;
|
|
247
|
+
if (set === 0) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
"evalAgentPrompt: provide one of `expect`, `expectFirst`, or `expectNoToolCall: true`"
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (set > 1) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
"evalAgentPrompt: `expect`, `expectFirst`, and `expectNoToolCall` are mutually exclusive"
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function matchExpectations(opts, actual) {
|
|
259
|
+
if (opts.expectNoToolCall) {
|
|
260
|
+
return noToolCallMatches(actual);
|
|
261
|
+
}
|
|
262
|
+
if (opts.expectFirst) {
|
|
263
|
+
return firstCallMatches(opts.expectFirst, actual);
|
|
264
|
+
}
|
|
265
|
+
return someCallMatches(opts.expect, actual);
|
|
266
|
+
}
|
|
267
|
+
async function runOnce(app, opts) {
|
|
268
|
+
await app.inject({
|
|
269
|
+
method: "POST",
|
|
270
|
+
url: "/api/v1/agent/sessions",
|
|
271
|
+
payload: { id: opts.sessionId }
|
|
272
|
+
});
|
|
273
|
+
const userMessage = opts.systemPrompt ? `[SYSTEM]
|
|
274
|
+
${opts.systemPrompt}
|
|
275
|
+
[/SYSTEM]
|
|
276
|
+
|
|
277
|
+
${opts.prompt}` : opts.prompt;
|
|
278
|
+
let res;
|
|
279
|
+
try {
|
|
280
|
+
res = await withTimeout(
|
|
281
|
+
app.inject({
|
|
282
|
+
method: "POST",
|
|
283
|
+
url: "/api/v1/agent/chat",
|
|
284
|
+
payload: {
|
|
285
|
+
sessionId: opts.sessionId,
|
|
286
|
+
message: userMessage,
|
|
287
|
+
model: opts.model
|
|
288
|
+
}
|
|
289
|
+
}),
|
|
290
|
+
opts.timeoutMs,
|
|
291
|
+
`chat request exceeded ${opts.timeoutMs}ms`
|
|
292
|
+
);
|
|
293
|
+
} finally {
|
|
294
|
+
void app.inject({
|
|
295
|
+
method: "DELETE",
|
|
296
|
+
url: `/api/v1/agent/sessions/${encodeURIComponent(opts.sessionId)}`
|
|
297
|
+
}).catch(() => {
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
if (res.statusCode !== 200) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`chat returned ${res.statusCode}: ${res.body.slice(0, 256)}`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return parseSseStream(res.body);
|
|
306
|
+
}
|
|
307
|
+
function parseSseStream(body) {
|
|
308
|
+
const toolCalls = [];
|
|
309
|
+
const textParts = [];
|
|
310
|
+
let usage;
|
|
311
|
+
for (const line of body.split("\n")) {
|
|
312
|
+
const trimmed = line.trim();
|
|
313
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
314
|
+
const payload = trimmed.slice("data:".length).trim();
|
|
315
|
+
if (!payload || payload === "[DONE]") continue;
|
|
316
|
+
let chunk;
|
|
317
|
+
try {
|
|
318
|
+
chunk = JSON.parse(payload);
|
|
319
|
+
} catch {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const type = chunk.type;
|
|
323
|
+
if (type === "tool-input-available") {
|
|
324
|
+
const toolName = chunk.toolName;
|
|
325
|
+
if (typeof toolName !== "string") continue;
|
|
326
|
+
const input = chunk.input;
|
|
327
|
+
const params = input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
328
|
+
toolCalls.push({ tool: toolName, params });
|
|
329
|
+
} else if (type === "text-delta") {
|
|
330
|
+
const delta = chunk.delta;
|
|
331
|
+
if (typeof delta === "string") textParts.push(delta);
|
|
332
|
+
} else if (type === "data-usage") {
|
|
333
|
+
const data = chunk.data;
|
|
334
|
+
if (data && typeof data === "object") {
|
|
335
|
+
const obj = data;
|
|
336
|
+
if (typeof obj.input === "number" && typeof obj.output === "number") {
|
|
337
|
+
usage = { input: obj.input, output: obj.output };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return { toolCalls, text: textParts.join(""), usage };
|
|
343
|
+
}
|
|
344
|
+
function withTimeout(p, ms, label) {
|
|
345
|
+
return new Promise((resolve, reject) => {
|
|
346
|
+
const timer = setTimeout(() => reject(new Error(label)), ms);
|
|
347
|
+
p.then(
|
|
348
|
+
(v) => {
|
|
349
|
+
clearTimeout(timer);
|
|
350
|
+
resolve(v);
|
|
351
|
+
},
|
|
352
|
+
(err) => {
|
|
353
|
+
clearTimeout(timer);
|
|
354
|
+
reject(err);
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
function parseModelString(input) {
|
|
360
|
+
if (typeof input === "object") return input;
|
|
361
|
+
if (input.startsWith("claude-")) return { provider: "anthropic", id: input };
|
|
362
|
+
if (input.startsWith("gpt-")) return { provider: "openai", id: input };
|
|
363
|
+
if (input.startsWith("qwen/")) return { provider: "openrouter", id: input };
|
|
364
|
+
return { provider: "anthropic", id: input };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/eval/runSuite.ts
|
|
368
|
+
import { readFile } from "fs/promises";
|
|
369
|
+
|
|
370
|
+
// src/eval/yamlSchema.ts
|
|
371
|
+
import { parse, Schema } from "yaml";
|
|
372
|
+
var evalSchema = new Schema({
|
|
373
|
+
customTags: [
|
|
374
|
+
{
|
|
375
|
+
tag: "!EvalAny",
|
|
376
|
+
// No value — `!EvalAny` alone resolves to the symbol.
|
|
377
|
+
identify: (value) => value === EvalAny,
|
|
378
|
+
resolve: () => EvalAny
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
tag: "!EvalRegex",
|
|
382
|
+
// Scalar value — `!EvalRegex "^chart:"` resolves to a matcher object.
|
|
383
|
+
identify: (value) => typeof value === "object" && value !== null && "__evalRegex" in value,
|
|
384
|
+
resolve: (str) => EvalRegex(str)
|
|
385
|
+
}
|
|
386
|
+
]
|
|
387
|
+
});
|
|
388
|
+
function parseFixtureYaml(text) {
|
|
389
|
+
const parsed = parse(text, { schema: evalSchema });
|
|
390
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
391
|
+
throw new Error(
|
|
392
|
+
`Eval fixture must be a YAML object with a "prompts" key. Got: ${describe2(parsed)}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
const obj = parsed;
|
|
396
|
+
if (!Array.isArray(obj.prompts)) {
|
|
397
|
+
throw new Error(`Eval fixture missing "prompts" array.`);
|
|
398
|
+
}
|
|
399
|
+
return obj;
|
|
400
|
+
}
|
|
401
|
+
function describe2(value) {
|
|
402
|
+
if (value === null) return "null";
|
|
403
|
+
if (value === void 0) return "undefined";
|
|
404
|
+
if (Array.isArray(value)) return "array";
|
|
405
|
+
return typeof value;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/eval/runSuite.ts
|
|
409
|
+
async function runEvalSuite(opts) {
|
|
410
|
+
const fixtureText = await readFile(opts.fixturesPath, "utf8");
|
|
411
|
+
const fixture = parseFixtureYaml(fixtureText);
|
|
412
|
+
const concurrency = opts.concurrency ?? DEFAULT_CONCURRENCY;
|
|
413
|
+
const suiteTimeoutMs = opts.suiteTimeoutMs ?? DEFAULT_SUITE_TIMEOUT_MS;
|
|
414
|
+
const start = Date.now();
|
|
415
|
+
const deadline = start + suiteTimeoutMs;
|
|
416
|
+
const queue = [...fixture.prompts];
|
|
417
|
+
const results = [];
|
|
418
|
+
let bailed = false;
|
|
419
|
+
async function worker() {
|
|
420
|
+
while (queue.length > 0 && !bailed) {
|
|
421
|
+
if (Date.now() > deadline) return;
|
|
422
|
+
const prompt = queue.shift();
|
|
423
|
+
if (!prompt) return;
|
|
424
|
+
const result = await runOnePrompt(opts, fixture, prompt);
|
|
425
|
+
results.push({
|
|
426
|
+
...result,
|
|
427
|
+
prompt: prompt.prompt,
|
|
428
|
+
expected: prompt
|
|
429
|
+
});
|
|
430
|
+
if (!result.ok && opts.bail) {
|
|
431
|
+
bailed = true;
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const workers = Array.from({ length: concurrency }, () => worker());
|
|
437
|
+
await Promise.race([
|
|
438
|
+
Promise.all(workers),
|
|
439
|
+
new Promise(
|
|
440
|
+
(_, reject) => setTimeout(
|
|
441
|
+
() => reject(new Error(`suite exceeded ${suiteTimeoutMs}ms`)),
|
|
442
|
+
suiteTimeoutMs
|
|
443
|
+
)
|
|
444
|
+
)
|
|
445
|
+
]).catch((err) => {
|
|
446
|
+
results.push({
|
|
447
|
+
ok: false,
|
|
448
|
+
actual: [],
|
|
449
|
+
text: "",
|
|
450
|
+
attempts: 0,
|
|
451
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
452
|
+
prompt: "(suite-level timeout)",
|
|
453
|
+
expected: { prompt: "(suite-level timeout)" }
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
const totalUsage = results.reduce(
|
|
457
|
+
(acc, r) => ({
|
|
458
|
+
input: acc.input + (r.usage?.input ?? 0),
|
|
459
|
+
output: acc.output + (r.usage?.output ?? 0)
|
|
460
|
+
}),
|
|
461
|
+
{ input: 0, output: 0 }
|
|
462
|
+
);
|
|
463
|
+
const passed = results.filter((r) => r.ok).length;
|
|
464
|
+
const total = results.length;
|
|
465
|
+
const passRate = total === 0 ? 0 : passed / total;
|
|
466
|
+
return {
|
|
467
|
+
total,
|
|
468
|
+
passed,
|
|
469
|
+
failed: total - passed,
|
|
470
|
+
passRate,
|
|
471
|
+
results,
|
|
472
|
+
totalUsage,
|
|
473
|
+
totalDurationMs: Date.now() - start,
|
|
474
|
+
allPassed: total > 0 && passed === total
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
async function runOnePrompt(opts, fixture, p) {
|
|
478
|
+
const model = opts.model ?? p.model ?? fixture.defaults?.model ?? fixture.model;
|
|
479
|
+
const retries = p.retries ?? fixture.defaults?.retries ?? 0;
|
|
480
|
+
const timeoutMs = p.timeoutMs ?? fixture.defaults?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
481
|
+
return evalAgentPrompt({
|
|
482
|
+
app: opts.app,
|
|
483
|
+
prompt: p.prompt,
|
|
484
|
+
expect: p.expect,
|
|
485
|
+
expectFirst: p.expectFirst,
|
|
486
|
+
expectNoToolCall: p.expectNoToolCall,
|
|
487
|
+
systemPrompt: fixture.systemPrompt,
|
|
488
|
+
model,
|
|
489
|
+
retries,
|
|
490
|
+
timeoutMs
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
export {
|
|
494
|
+
DEFAULT_CONCURRENCY,
|
|
495
|
+
DEFAULT_EVAL_MODEL,
|
|
496
|
+
DEFAULT_SUITE_TIMEOUT_MS,
|
|
497
|
+
DEFAULT_TIMEOUT_MS,
|
|
498
|
+
EvalAny,
|
|
499
|
+
EvalRegex,
|
|
500
|
+
callSatisfies,
|
|
501
|
+
evalAgentPrompt,
|
|
502
|
+
firstCallMatches,
|
|
503
|
+
isEvalRegex,
|
|
504
|
+
noToolCallMatches,
|
|
505
|
+
parseFixtureYaml,
|
|
506
|
+
runEvalSuite,
|
|
507
|
+
someCallMatches
|
|
508
|
+
};
|