@agentv/core 3.6.0 → 3.8.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"),
@@ -1058,11 +1246,11 @@ function validateUnknownSettings(target, provider, absolutePath, location, error
1058
1246
  }
1059
1247
  async function validateTargetsFile(filePath) {
1060
1248
  const errors = [];
1061
- const absolutePath = import_node_path4.default.resolve(filePath);
1249
+ const absolutePath = import_node_path5.default.resolve(filePath);
1062
1250
  let parsed;
1063
1251
  try {
1064
- const content = await (0, import_promises3.readFile)(absolutePath, "utf8");
1065
- parsed = (0, import_yaml3.parse)(content);
1252
+ const content = await (0, import_promises4.readFile)(absolutePath, "utf8");
1253
+ parsed = (0, import_yaml4.parse)(content);
1066
1254
  } catch (error) {
1067
1255
  errors.push({
1068
1256
  severity: "error",
@@ -1261,13 +1449,13 @@ async function validateTargetsFile(filePath) {
1261
1449
  }
1262
1450
 
1263
1451
  // src/evaluation/validation/config-validator.ts
1264
- var import_promises4 = require("fs/promises");
1265
- var import_yaml4 = require("yaml");
1452
+ var import_promises5 = require("fs/promises");
1453
+ var import_yaml5 = require("yaml");
1266
1454
  async function validateConfigFile(filePath) {
1267
1455
  const errors = [];
1268
1456
  try {
1269
- const content = await (0, import_promises4.readFile)(filePath, "utf8");
1270
- const parsed = (0, import_yaml4.parse)(content);
1457
+ const content = await (0, import_promises5.readFile)(filePath, "utf8");
1458
+ const parsed = (0, import_yaml5.parse)(content);
1271
1459
  if (typeof parsed !== "object" || parsed === null) {
1272
1460
  errors.push({
1273
1461
  severity: "error",
@@ -1370,31 +1558,31 @@ async function validateConfigFile(filePath) {
1370
1558
  }
1371
1559
 
1372
1560
  // 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");
1561
+ var import_promises7 = require("fs/promises");
1562
+ var import_node_path7 = __toESM(require("path"), 1);
1563
+ var import_yaml6 = require("yaml");
1376
1564
 
1377
1565
  // src/evaluation/file-utils.ts
1378
1566
  var import_node_fs = require("fs");
1379
- var import_promises5 = require("fs/promises");
1380
- var import_node_path5 = __toESM(require("path"), 1);
1567
+ var import_promises6 = require("fs/promises");
1568
+ var import_node_path6 = __toESM(require("path"), 1);
1381
1569
  async function fileExists(filePath) {
1382
1570
  try {
1383
- await (0, import_promises5.access)(filePath, import_node_fs.constants.F_OK);
1571
+ await (0, import_promises6.access)(filePath, import_node_fs.constants.F_OK);
1384
1572
  return true;
1385
1573
  } catch {
1386
1574
  return false;
1387
1575
  }
1388
1576
  }
1389
1577
  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;
1578
+ let currentDir = import_node_path6.default.dirname(import_node_path6.default.resolve(startPath));
1579
+ const root = import_node_path6.default.parse(currentDir).root;
1392
1580
  while (currentDir !== root) {
1393
- const gitPath = import_node_path5.default.join(currentDir, ".git");
1581
+ const gitPath = import_node_path6.default.join(currentDir, ".git");
1394
1582
  if (await fileExists(gitPath)) {
1395
1583
  return currentDir;
1396
1584
  }
1397
- const parentDir = import_node_path5.default.dirname(currentDir);
1585
+ const parentDir = import_node_path6.default.dirname(currentDir);
1398
1586
  if (parentDir === currentDir) {
1399
1587
  break;
1400
1588
  }
@@ -1405,16 +1593,16 @@ async function findGitRoot(startPath) {
1405
1593
  function buildSearchRoots(evalPath, repoRoot) {
1406
1594
  const uniqueRoots = [];
1407
1595
  const addRoot = (root) => {
1408
- const normalized = import_node_path5.default.resolve(root);
1596
+ const normalized = import_node_path6.default.resolve(root);
1409
1597
  if (!uniqueRoots.includes(normalized)) {
1410
1598
  uniqueRoots.push(normalized);
1411
1599
  }
1412
1600
  };
1413
- let currentDir = import_node_path5.default.dirname(evalPath);
1601
+ let currentDir = import_node_path6.default.dirname(evalPath);
1414
1602
  let reachedBoundary = false;
1415
1603
  while (!reachedBoundary) {
1416
1604
  addRoot(currentDir);
1417
- const parentDir = import_node_path5.default.dirname(currentDir);
1605
+ const parentDir = import_node_path6.default.dirname(currentDir);
1418
1606
  if (currentDir === repoRoot || parentDir === currentDir) {
1419
1607
  reachedBoundary = true;
1420
1608
  } else {
@@ -1432,16 +1620,16 @@ function trimLeadingSeparators(value) {
1432
1620
  async function resolveFileReference(rawValue, searchRoots) {
1433
1621
  const displayPath = trimLeadingSeparators(rawValue);
1434
1622
  const potentialPaths = [];
1435
- if (import_node_path5.default.isAbsolute(rawValue)) {
1436
- potentialPaths.push(import_node_path5.default.normalize(rawValue));
1623
+ if (import_node_path6.default.isAbsolute(rawValue)) {
1624
+ potentialPaths.push(import_node_path6.default.normalize(rawValue));
1437
1625
  }
1438
1626
  for (const base of searchRoots) {
1439
- potentialPaths.push(import_node_path5.default.resolve(base, displayPath));
1627
+ potentialPaths.push(import_node_path6.default.resolve(base, displayPath));
1440
1628
  }
1441
1629
  const attempted = [];
1442
1630
  const seen = /* @__PURE__ */ new Set();
1443
1631
  for (const candidate of potentialPaths) {
1444
- const absoluteCandidate = import_node_path5.default.resolve(candidate);
1632
+ const absoluteCandidate = import_node_path6.default.resolve(candidate);
1445
1633
  if (seen.has(absoluteCandidate)) {
1446
1634
  continue;
1447
1635
  }
@@ -1460,7 +1648,7 @@ function isObject3(value) {
1460
1648
  }
1461
1649
  async function validateFileReferences(evalFilePath) {
1462
1650
  const errors = [];
1463
- const absolutePath = import_node_path6.default.resolve(evalFilePath);
1651
+ const absolutePath = import_node_path7.default.resolve(evalFilePath);
1464
1652
  const gitRoot = await findGitRoot(absolutePath);
1465
1653
  if (!gitRoot) {
1466
1654
  errors.push({
@@ -1473,8 +1661,8 @@ async function validateFileReferences(evalFilePath) {
1473
1661
  const searchRoots = buildSearchRoots(absolutePath, gitRoot);
1474
1662
  let parsed;
1475
1663
  try {
1476
- const content = await (0, import_promises6.readFile)(absolutePath, "utf8");
1477
- parsed = (0, import_yaml5.parse)(content);
1664
+ const content = await (0, import_promises7.readFile)(absolutePath, "utf8");
1665
+ parsed = (0, import_yaml6.parse)(content);
1478
1666
  } catch {
1479
1667
  return errors;
1480
1668
  }
@@ -1561,7 +1749,7 @@ async function validateMessagesFileRefs(messages, location, searchRoots, filePat
1561
1749
  });
1562
1750
  } else {
1563
1751
  try {
1564
- const fileContent = await (0, import_promises6.readFile)(resolvedPath, "utf8");
1752
+ const fileContent = await (0, import_promises7.readFile)(resolvedPath, "utf8");
1565
1753
  if (fileContent.trim().length === 0) {
1566
1754
  errors.push({
1567
1755
  severity: "warning",