@agentv/core 3.7.0 → 3.9.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.
@@ -103,13 +103,56 @@ function getExpectedSchema(fileType) {
103
103
  }
104
104
 
105
105
  // src/evaluation/validation/eval-validator.ts
106
+ var import_promises3 = require("fs/promises");
107
+ var import_node_path3 = __toESM(require("path"), 1);
108
+ var import_yaml3 = require("yaml");
109
+
110
+ // src/evaluation/interpolation.ts
111
+ var ENV_VAR_PATTERN = /\$\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
112
+ function interpolateEnv(value, env) {
113
+ if (typeof value === "string") {
114
+ return value.replace(ENV_VAR_PATTERN, (_, varName) => env[varName] ?? "");
115
+ }
116
+ if (Array.isArray(value)) {
117
+ return value.map((item) => interpolateEnv(item, env));
118
+ }
119
+ if (value !== null && typeof value === "object") {
120
+ const result = {};
121
+ for (const [key, val] of Object.entries(value)) {
122
+ result[key] = interpolateEnv(val, env);
123
+ }
124
+ return result;
125
+ }
126
+ return value;
127
+ }
128
+
129
+ // src/evaluation/loaders/case-file-loader.ts
106
130
  var import_promises2 = require("fs/promises");
107
131
  var import_node_path2 = __toESM(require("path"), 1);
132
+ var import_fast_glob = __toESM(require("fast-glob"), 1);
108
133
  var import_yaml2 = require("yaml");
109
134
 
110
135
  // src/evaluation/types.ts
111
136
  var TEST_MESSAGE_ROLE_VALUES = ["system", "user", "assistant", "tool"];
112
137
  var TEST_MESSAGE_ROLE_SET = new Set(TEST_MESSAGE_ROLE_VALUES);
138
+ function isJsonObject(value) {
139
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
140
+ return false;
141
+ }
142
+ return Object.values(value).every(isJsonValue);
143
+ }
144
+ function isJsonValue(value) {
145
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
146
+ return true;
147
+ }
148
+ if (Array.isArray(value)) {
149
+ return value.every(isJsonValue);
150
+ }
151
+ if (typeof value === "object") {
152
+ return isJsonObject(value);
153
+ }
154
+ return false;
155
+ }
113
156
  var EVALUATOR_KIND_VALUES = [
114
157
  "code-grader",
115
158
  "llm-grader",
@@ -143,6 +186,74 @@ function isEvaluatorKind(value) {
143
186
  return typeof value === "string" && EVALUATOR_KIND_SET.has(value);
144
187
  }
145
188
 
189
+ // src/evaluation/loaders/case-file-loader.ts
190
+ var ANSI_YELLOW = "\x1B[33m";
191
+ var ANSI_RESET = "\x1B[0m";
192
+ function parseYamlCases(content, filePath) {
193
+ const raw = (0, import_yaml2.parse)(content);
194
+ const parsed = interpolateEnv(raw, process.env);
195
+ if (!Array.isArray(parsed)) {
196
+ throw new Error(
197
+ `External test file must contain a YAML array, got ${typeof parsed}: ${filePath}`
198
+ );
199
+ }
200
+ const results = [];
201
+ for (const item of parsed) {
202
+ if (!isJsonObject(item)) {
203
+ throw new Error(`External test file contains non-object entry: ${filePath}`);
204
+ }
205
+ results.push(item);
206
+ }
207
+ return results;
208
+ }
209
+ function parseJsonlCases(content, filePath) {
210
+ const lines = content.split("\n");
211
+ const results = [];
212
+ for (let i = 0; i < lines.length; i++) {
213
+ const line = lines[i].trim();
214
+ if (line === "") continue;
215
+ try {
216
+ const raw = JSON.parse(line);
217
+ const parsed = interpolateEnv(raw, process.env);
218
+ if (!isJsonObject(parsed)) {
219
+ throw new Error("Expected JSON object");
220
+ }
221
+ results.push(parsed);
222
+ } catch (error) {
223
+ const message = error instanceof Error ? error.message : String(error);
224
+ throw new Error(`Malformed JSONL at line ${i + 1}: ${message}
225
+ File: ${filePath}`);
226
+ }
227
+ }
228
+ return results;
229
+ }
230
+ async function loadCasesFromFile(filePath) {
231
+ const ext = import_node_path2.default.extname(filePath).toLowerCase();
232
+ let content;
233
+ try {
234
+ content = await (0, import_promises2.readFile)(filePath, "utf8");
235
+ } catch (error) {
236
+ const message = error instanceof Error ? error.message : String(error);
237
+ throw new Error(`Cannot read external test file: ${filePath}
238
+ ${message}`);
239
+ }
240
+ if (content.trim() === "") {
241
+ console.warn(
242
+ `${ANSI_YELLOW}Warning: External test file is empty, skipping: ${filePath}${ANSI_RESET}`
243
+ );
244
+ return [];
245
+ }
246
+ if (ext === ".yaml" || ext === ".yml") {
247
+ return parseYamlCases(content, filePath);
248
+ }
249
+ if (ext === ".jsonl") {
250
+ return parseJsonlCases(content, filePath);
251
+ }
252
+ throw new Error(
253
+ `Unsupported external test file format '${ext}': ${filePath}. Supported: .yaml, .yml, .jsonl`
254
+ );
255
+ }
256
+
146
257
  // src/evaluation/validation/eval-validator.ts
147
258
  var ASSERTION_TYPES_WITH_STRING_VALUE = /* @__PURE__ */ new Set([
148
259
  "contains",
@@ -165,11 +276,11 @@ function isObject(value) {
165
276
  }
166
277
  async function validateEvalFile(filePath) {
167
278
  const errors = [];
168
- const absolutePath = import_node_path2.default.resolve(filePath);
279
+ const absolutePath = import_node_path3.default.resolve(filePath);
169
280
  let parsed;
170
281
  try {
171
- const content = await (0, import_promises2.readFile)(absolutePath, "utf8");
172
- parsed = (0, import_yaml2.parse)(content);
282
+ const content = await (0, import_promises3.readFile)(absolutePath, "utf8");
283
+ parsed = interpolateEnv((0, import_yaml3.parse)(content), process.env);
173
284
  } catch (error) {
174
285
  errors.push({
175
286
  severity: "error",
@@ -232,6 +343,31 @@ async function validateEvalFile(filePath) {
232
343
  }
233
344
  if (typeof cases === "string") {
234
345
  validateTestsStringPath(cases, absolutePath, errors);
346
+ await validateWorkspaceConfig(parsed.workspace, absolutePath, errors, "workspace");
347
+ const ext = import_node_path3.default.extname(cases).toLowerCase();
348
+ if (VALID_TEST_FILE_EXTENSIONS.has(ext)) {
349
+ const externalCasesPath = import_node_path3.default.resolve(import_node_path3.default.dirname(absolutePath), cases);
350
+ try {
351
+ const externalCases = await loadCasesFromFile(externalCasesPath);
352
+ for (let i = 0; i < externalCases.length; i++) {
353
+ const externalCase = externalCases[i];
354
+ await validateWorkspaceConfig(
355
+ externalCase.workspace,
356
+ absolutePath,
357
+ errors,
358
+ `tests[${i}].workspace`
359
+ );
360
+ }
361
+ } catch (error) {
362
+ const message = error instanceof Error ? error.message : String(error);
363
+ errors.push({
364
+ severity: "error",
365
+ filePath: absolutePath,
366
+ location: "tests",
367
+ message
368
+ });
369
+ }
370
+ }
235
371
  return {
236
372
  valid: errors.filter((e) => e.severity === "error").length === 0,
237
373
  filePath: absolutePath,
@@ -339,10 +475,14 @@ async function validateEvalFile(filePath) {
339
475
  if (assertField !== void 0) {
340
476
  validateAssertArray(assertField, location, absolutePath, errors);
341
477
  }
478
+ await validateWorkspaceConfig(
479
+ evalCase.workspace,
480
+ absolutePath,
481
+ errors,
482
+ `${location}.workspace`
483
+ );
342
484
  }
343
- if (isObject(parsed.workspace)) {
344
- validateWorkspaceRepoConfig(parsed.workspace, absolutePath, errors);
345
- }
485
+ await validateWorkspaceConfig(parsed.workspace, absolutePath, errors, "workspace");
346
486
  return {
347
487
  valid: errors.filter((e) => e.severity === "error").length === 0,
348
488
  filePath: absolutePath,
@@ -350,6 +490,41 @@ async function validateEvalFile(filePath) {
350
490
  errors
351
491
  };
352
492
  }
493
+ async function validateWorkspaceConfig(workspace, evalFilePath, errors, location) {
494
+ if (workspace === void 0) {
495
+ return;
496
+ }
497
+ if (isObject(workspace)) {
498
+ validateWorkspaceRepoConfig(workspace, evalFilePath, errors);
499
+ return;
500
+ }
501
+ if (typeof workspace !== "string") {
502
+ return;
503
+ }
504
+ const workspacePath = import_node_path3.default.resolve(import_node_path3.default.dirname(evalFilePath), workspace);
505
+ try {
506
+ const workspaceContent = await (0, import_promises3.readFile)(workspacePath, "utf8");
507
+ const parsedWorkspace = interpolateEnv((0, import_yaml3.parse)(workspaceContent), process.env);
508
+ if (!isObject(parsedWorkspace)) {
509
+ errors.push({
510
+ severity: "error",
511
+ filePath: evalFilePath,
512
+ location,
513
+ message: `External workspace file must contain a YAML object: ${workspace}`
514
+ });
515
+ return;
516
+ }
517
+ validateWorkspaceRepoConfig(parsedWorkspace, workspacePath, errors);
518
+ } catch (error) {
519
+ const message = error instanceof Error ? error.message : String(error);
520
+ errors.push({
521
+ severity: "error",
522
+ filePath: evalFilePath,
523
+ location,
524
+ message: `Failed to load external workspace file '${workspace}': ${message}`
525
+ });
526
+ }
527
+ }
353
528
  function validateWorkspaceRepoConfig(workspace, filePath, errors) {
354
529
  const repos = workspace.repos;
355
530
  const hooks = workspace.hooks;
@@ -358,8 +533,21 @@ function validateWorkspaceRepoConfig(workspace, filePath, errors) {
358
533
  if (Array.isArray(repos)) {
359
534
  for (const repo of repos) {
360
535
  if (!isObject(repo)) continue;
536
+ const source = repo.source;
361
537
  const checkout = repo.checkout;
362
538
  const clone = repo.clone;
539
+ if (isObject(source) && isObject(checkout)) {
540
+ const sourceType = source.type;
541
+ const resolve = checkout.resolve;
542
+ if (sourceType === "local" && typeof resolve === "string") {
543
+ errors.push({
544
+ severity: "warning",
545
+ filePath,
546
+ location: `workspace.repos[path=${repo.path}]`,
547
+ message: "checkout.resolve has no effect for a local source. Use source.type to choose where the repo comes from; keep checkout.ref or checkout.ancestor only when pinning a local source."
548
+ });
549
+ }
550
+ }
363
551
  if (isObject(checkout) && isObject(clone)) {
364
552
  const ancestor = checkout.ancestor;
365
553
  const depth = clone.depth;
@@ -491,7 +679,7 @@ function validateMetadata(parsed, filePath, errors) {
491
679
  }
492
680
  }
493
681
  function validateTestsStringPath(testsPath, filePath, errors) {
494
- const ext = import_node_path2.default.extname(testsPath);
682
+ const ext = import_node_path3.default.extname(testsPath);
495
683
  if (!VALID_TEST_FILE_EXTENSIONS.has(ext)) {
496
684
  errors.push({
497
685
  severity: "warning",
@@ -637,12 +825,12 @@ function validateContentForRoleMarkers(content, location, filePath, errors) {
637
825
  }
638
826
 
639
827
  // src/evaluation/validation/targets-validator.ts
640
- var import_promises3 = require("fs/promises");
641
- var import_node_path4 = __toESM(require("path"), 1);
642
- var import_yaml3 = require("yaml");
828
+ var import_promises4 = require("fs/promises");
829
+ var import_node_path5 = __toESM(require("path"), 1);
830
+ var import_yaml4 = require("yaml");
643
831
 
644
832
  // src/evaluation/providers/targets.ts
645
- var import_node_path3 = __toESM(require("path"), 1);
833
+ var import_node_path4 = __toESM(require("path"), 1);
646
834
  var import_zod = require("zod");
647
835
  var CliHealthcheckHttpInputSchema = import_zod.z.object({
648
836
  url: import_zod.z.string().min(1, "healthcheck URL is required"),
@@ -722,7 +910,6 @@ var CliTargetConfigSchema = import_zod.z.object({
722
910
  var CLI_PLACEHOLDERS = /* @__PURE__ */ new Set([
723
911
  "PROMPT",
724
912
  "PROMPT_FILE",
725
- "GUIDELINES",
726
913
  "EVAL_ID",
727
914
  "ATTEMPT",
728
915
  "FILES",
@@ -1058,11 +1245,11 @@ function validateUnknownSettings(target, provider, absolutePath, location, error
1058
1245
  }
1059
1246
  async function validateTargetsFile(filePath) {
1060
1247
  const errors = [];
1061
- const absolutePath = import_node_path4.default.resolve(filePath);
1248
+ const absolutePath = import_node_path5.default.resolve(filePath);
1062
1249
  let parsed;
1063
1250
  try {
1064
- const content = await (0, import_promises3.readFile)(absolutePath, "utf8");
1065
- parsed = (0, import_yaml3.parse)(content);
1251
+ const content = await (0, import_promises4.readFile)(absolutePath, "utf8");
1252
+ parsed = (0, import_yaml4.parse)(content);
1066
1253
  } catch (error) {
1067
1254
  errors.push({
1068
1255
  severity: "error",
@@ -1261,13 +1448,13 @@ async function validateTargetsFile(filePath) {
1261
1448
  }
1262
1449
 
1263
1450
  // src/evaluation/validation/config-validator.ts
1264
- var import_promises4 = require("fs/promises");
1265
- var import_yaml4 = require("yaml");
1451
+ var import_promises5 = require("fs/promises");
1452
+ var import_yaml5 = require("yaml");
1266
1453
  async function validateConfigFile(filePath) {
1267
1454
  const errors = [];
1268
1455
  try {
1269
- const content = await (0, import_promises4.readFile)(filePath, "utf8");
1270
- const parsed = (0, import_yaml4.parse)(content);
1456
+ const content = await (0, import_promises5.readFile)(filePath, "utf8");
1457
+ const parsed = (0, import_yaml5.parse)(content);
1271
1458
  if (typeof parsed !== "object" || parsed === null) {
1272
1459
  errors.push({
1273
1460
  severity: "error",
@@ -1277,31 +1464,6 @@ async function validateConfigFile(filePath) {
1277
1464
  return { valid: false, filePath, fileType: "config", errors };
1278
1465
  }
1279
1466
  const config = parsed;
1280
- const guidelinePatterns = config.guideline_patterns;
1281
- if (guidelinePatterns !== void 0) {
1282
- if (!Array.isArray(guidelinePatterns)) {
1283
- errors.push({
1284
- severity: "error",
1285
- filePath,
1286
- location: "guideline_patterns",
1287
- message: "Field 'guideline_patterns' must be an array"
1288
- });
1289
- } else if (!guidelinePatterns.every((p) => typeof p === "string")) {
1290
- errors.push({
1291
- severity: "error",
1292
- filePath,
1293
- location: "guideline_patterns",
1294
- message: "All entries in 'guideline_patterns' must be strings"
1295
- });
1296
- } else if (guidelinePatterns.length === 0) {
1297
- errors.push({
1298
- severity: "warning",
1299
- filePath,
1300
- location: "guideline_patterns",
1301
- message: "Field 'guideline_patterns' is empty. Consider removing it or adding patterns."
1302
- });
1303
- }
1304
- }
1305
1467
  const evalPatterns = config.eval_patterns;
1306
1468
  if (evalPatterns !== void 0) {
1307
1469
  if (!Array.isArray(evalPatterns)) {
@@ -1338,13 +1500,7 @@ async function validateConfigFile(filePath) {
1338
1500
  });
1339
1501
  }
1340
1502
  }
1341
- const allowedFields = /* @__PURE__ */ new Set([
1342
- "$schema",
1343
- "guideline_patterns",
1344
- "eval_patterns",
1345
- "required_version",
1346
- "execution"
1347
- ]);
1503
+ const allowedFields = /* @__PURE__ */ new Set(["$schema", "eval_patterns", "required_version", "execution"]);
1348
1504
  const unexpectedFields = Object.keys(config).filter((key) => !allowedFields.has(key));
1349
1505
  if (unexpectedFields.length > 0) {
1350
1506
  errors.push({
@@ -1370,31 +1526,31 @@ async function validateConfigFile(filePath) {
1370
1526
  }
1371
1527
 
1372
1528
  // src/evaluation/validation/file-reference-validator.ts
1373
- var import_promises6 = require("fs/promises");
1374
- var import_node_path6 = __toESM(require("path"), 1);
1375
- var import_yaml5 = require("yaml");
1529
+ var import_promises7 = require("fs/promises");
1530
+ var import_node_path7 = __toESM(require("path"), 1);
1531
+ var import_yaml6 = require("yaml");
1376
1532
 
1377
1533
  // src/evaluation/file-utils.ts
1378
1534
  var import_node_fs = require("fs");
1379
- var import_promises5 = require("fs/promises");
1380
- var import_node_path5 = __toESM(require("path"), 1);
1535
+ var import_promises6 = require("fs/promises");
1536
+ var import_node_path6 = __toESM(require("path"), 1);
1381
1537
  async function fileExists(filePath) {
1382
1538
  try {
1383
- await (0, import_promises5.access)(filePath, import_node_fs.constants.F_OK);
1539
+ await (0, import_promises6.access)(filePath, import_node_fs.constants.F_OK);
1384
1540
  return true;
1385
1541
  } catch {
1386
1542
  return false;
1387
1543
  }
1388
1544
  }
1389
1545
  async function findGitRoot(startPath) {
1390
- let currentDir = import_node_path5.default.dirname(import_node_path5.default.resolve(startPath));
1391
- const root = import_node_path5.default.parse(currentDir).root;
1546
+ let currentDir = import_node_path6.default.dirname(import_node_path6.default.resolve(startPath));
1547
+ const root = import_node_path6.default.parse(currentDir).root;
1392
1548
  while (currentDir !== root) {
1393
- const gitPath = import_node_path5.default.join(currentDir, ".git");
1549
+ const gitPath = import_node_path6.default.join(currentDir, ".git");
1394
1550
  if (await fileExists(gitPath)) {
1395
1551
  return currentDir;
1396
1552
  }
1397
- const parentDir = import_node_path5.default.dirname(currentDir);
1553
+ const parentDir = import_node_path6.default.dirname(currentDir);
1398
1554
  if (parentDir === currentDir) {
1399
1555
  break;
1400
1556
  }
@@ -1405,16 +1561,16 @@ async function findGitRoot(startPath) {
1405
1561
  function buildSearchRoots(evalPath, repoRoot) {
1406
1562
  const uniqueRoots = [];
1407
1563
  const addRoot = (root) => {
1408
- const normalized = import_node_path5.default.resolve(root);
1564
+ const normalized = import_node_path6.default.resolve(root);
1409
1565
  if (!uniqueRoots.includes(normalized)) {
1410
1566
  uniqueRoots.push(normalized);
1411
1567
  }
1412
1568
  };
1413
- let currentDir = import_node_path5.default.dirname(evalPath);
1569
+ let currentDir = import_node_path6.default.dirname(evalPath);
1414
1570
  let reachedBoundary = false;
1415
1571
  while (!reachedBoundary) {
1416
1572
  addRoot(currentDir);
1417
- const parentDir = import_node_path5.default.dirname(currentDir);
1573
+ const parentDir = import_node_path6.default.dirname(currentDir);
1418
1574
  if (currentDir === repoRoot || parentDir === currentDir) {
1419
1575
  reachedBoundary = true;
1420
1576
  } else {
@@ -1432,16 +1588,16 @@ function trimLeadingSeparators(value) {
1432
1588
  async function resolveFileReference(rawValue, searchRoots) {
1433
1589
  const displayPath = trimLeadingSeparators(rawValue);
1434
1590
  const potentialPaths = [];
1435
- if (import_node_path5.default.isAbsolute(rawValue)) {
1436
- potentialPaths.push(import_node_path5.default.normalize(rawValue));
1591
+ if (import_node_path6.default.isAbsolute(rawValue)) {
1592
+ potentialPaths.push(import_node_path6.default.normalize(rawValue));
1437
1593
  }
1438
1594
  for (const base of searchRoots) {
1439
- potentialPaths.push(import_node_path5.default.resolve(base, displayPath));
1595
+ potentialPaths.push(import_node_path6.default.resolve(base, displayPath));
1440
1596
  }
1441
1597
  const attempted = [];
1442
1598
  const seen = /* @__PURE__ */ new Set();
1443
1599
  for (const candidate of potentialPaths) {
1444
- const absoluteCandidate = import_node_path5.default.resolve(candidate);
1600
+ const absoluteCandidate = import_node_path6.default.resolve(candidate);
1445
1601
  if (seen.has(absoluteCandidate)) {
1446
1602
  continue;
1447
1603
  }
@@ -1460,7 +1616,7 @@ function isObject3(value) {
1460
1616
  }
1461
1617
  async function validateFileReferences(evalFilePath) {
1462
1618
  const errors = [];
1463
- const absolutePath = import_node_path6.default.resolve(evalFilePath);
1619
+ const absolutePath = import_node_path7.default.resolve(evalFilePath);
1464
1620
  const gitRoot = await findGitRoot(absolutePath);
1465
1621
  if (!gitRoot) {
1466
1622
  errors.push({
@@ -1473,8 +1629,8 @@ async function validateFileReferences(evalFilePath) {
1473
1629
  const searchRoots = buildSearchRoots(absolutePath, gitRoot);
1474
1630
  let parsed;
1475
1631
  try {
1476
- const content = await (0, import_promises6.readFile)(absolutePath, "utf8");
1477
- parsed = (0, import_yaml5.parse)(content);
1632
+ const content = await (0, import_promises7.readFile)(absolutePath, "utf8");
1633
+ parsed = (0, import_yaml6.parse)(content);
1478
1634
  } catch {
1479
1635
  return errors;
1480
1636
  }
@@ -1561,7 +1717,7 @@ async function validateMessagesFileRefs(messages, location, searchRoots, filePat
1561
1717
  });
1562
1718
  } else {
1563
1719
  try {
1564
- const fileContent = await (0, import_promises6.readFile)(resolvedPath, "utf8");
1720
+ const fileContent = await (0, import_promises7.readFile)(resolvedPath, "utf8");
1565
1721
  if (fileContent.trim().length === 0) {
1566
1722
  errors.push({
1567
1723
  severity: "warning",