niceeval 0.4.3 → 0.4.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niceeval",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Agent-native eval tool — eval agents, services, functions, and coding-agent fixtures",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli.ts CHANGED
@@ -11,6 +11,7 @@ import { pathToFileURL } from "node:url";
11
11
  import { parseArgs as nodeParseArgs } from "node:util";
12
12
  import { discoverEvals, discoverExperiments, makeFilter } from "./runner/discover.ts";
13
13
  import { runEvals, type AgentRun } from "./runner/run.ts";
14
+ import { runWho } from "./runner/types.ts";
14
15
  import { stopAllSandboxes, liveSandboxCount } from "./sandbox/registry.ts";
15
16
  import { sandboxRecommendedConcurrency } from "./sandbox/resolve.ts";
16
17
  import { Console as ConsoleReporter } from "./runner/reporters/console.ts";
@@ -372,9 +373,9 @@ async function main(): Promise<void> {
372
373
  // TTY 模式:用 live display 替换 Console reporter,把 attempt log 路由到状态表行尾
373
374
  const liveRows: LiveRow[] = [];
374
375
  for (const agentRun of agentRuns) {
375
- const who = agentRun.model
376
- ? `${agentRun.agent.name}/${agentRun.model}`
377
- : agentRun.agent.name;
376
+ // who 必须与 attempt.ts 的进度上报同源(runWho):曾用 agent/model,同 agent 同 model
377
+ // 的实验变体(xxx 与 xxx--agents-md)会被折叠成一行,0/2 看起来像同一 eval 跑两次。
378
+ const who = runWho(agentRun);
378
379
  const matched = evals.filter((e) => agentRun.evalFilter(e.id));
379
380
  for (const evalDef of matched) {
380
381
  liveRows.push({ evalId: evalDef.id, who, total: agentRun.runs });
package/src/i18n/en.ts CHANGED
@@ -156,6 +156,7 @@ export const en = {
156
156
  "runner.timeout": "attempt timed out ({{timeoutMs}}ms)\nRecent progress:\n{{recentLogs}}",
157
157
  "runner.traceSelected": " -> kept {{count}} semantic spans",
158
158
  "runner.resumeCarry": " · reusing {{carried}} passing results from last run, re-running {{retry}} evals\n",
159
+ "runner.resumeCarryDetail": " carried [{{experiment}}] {{evals}}\n",
159
160
  "runner.useRemoteAgent": "using remote agent (no sandbox created)...",
160
161
  "sandbox.backendNotImplemented": "{{backend}} sandbox backend is not implemented; use docker, vercel, or e2b",
161
162
  "sandbox.missingSpec": "sandbox agent needs a sandbox, but none was given. niceeval no longer picks a default — set `sandbox` in defineExperiment()/defineConfig() to dockerSandbox() / vercelSandbox() / e2bSandbox() (import from \"niceeval/sandbox\").",
package/src/i18n/zh-CN.ts CHANGED
@@ -153,6 +153,7 @@ export const zhCN = {
153
153
  "runner.timeout": "attempt 超时({{timeoutMs}}ms)\n最近进度:\n{{recentLogs}}",
154
154
  "runner.traceSelected": " → 留 {{count}}(按语义)",
155
155
  "runner.resumeCarry": " · 复用上次 {{carried}} 个通过的结果,重跑 {{retry}} 个 eval\n",
156
+ "runner.resumeCarryDetail": " 复用 [{{experiment}}] {{evals}}\n",
156
157
  "runner.useRemoteAgent": "使用 remote agent(不创建沙箱)…",
157
158
  "sandbox.backendNotImplemented": "{{backend}} sandbox backend not implemented; use docker, vercel, or e2b",
158
159
  "sandbox.missingSpec": "沙箱型 agent 需要一个 sandbox,但没有提供。niceeval 不再自动选默认后端——请在 defineExperiment()/defineConfig() 里把 sandbox 设成 dockerSandbox() / vercelSandbox() / e2bSandbox()(从 \"niceeval/sandbox\" 导入)。",
@@ -36,6 +36,7 @@ import type {
36
36
  Telemetry,
37
37
  TraceSpan,
38
38
  } from "../types.ts";
39
+ import { runWho } from "./types.ts";
39
40
  import type { AgentRun, Attempt, RunOptions } from "./types.ts";
40
41
 
41
42
  export function runAttemptEffect(
@@ -72,7 +73,7 @@ export function runAttemptEffect(
72
73
 
73
74
  // 流式进度打到宿主 stderr(结果走 stdout,互不干扰)。容器主日志【不】放这些进度标记 ——
74
75
  // 那里留给 agent 的原始输出(adapter 给 agent 命令开 { stream: true })。
75
- const who = run.model ? `${run.agent.name}/${run.model}` : run.agent.name;
76
+ const who = runWho(run);
76
77
  // 同时保留最近 20 条进度消息,timeout 时嵌入 error 字段方便定位卡在哪一步。
77
78
  const recentLogs: string[] = [];
78
79
  const log = (m: string) => {
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { judgeProbeTargets } from "./run.ts";
3
+ import type { JudgeConfig } from "../types.ts";
4
+
5
+ // judge 预检的目标收敛:只探测「实际要跑、且源码里出现 judge 字样」的 eval 的生效配置。
6
+ // 这是对 memory/judge-config-precheck-hard-fails-without-key 的修复守护——
7
+ // 全局配了 judge 但选中的 eval 都不用时,不能再因 judge key / 端点问题拦下整次运行。
8
+ describe("judgeProbeTargets", () => {
9
+ const configJudge: JudgeConfig = { model: "gpt-5.4" };
10
+
11
+ it("skips probing when no selected eval mentions judge", () => {
12
+ const evals = [
13
+ { source: `t.check(t.reply, includes("2"));`, judge: undefined },
14
+ { source: `await t.sandbox.exec("pnpm test");`, judge: undefined },
15
+ ];
16
+ expect(judgeProbeTargets(evals, configJudge)).toEqual([]);
17
+ });
18
+
19
+ it("probes config-level judge when a selected eval mentions judge", () => {
20
+ const evals = [
21
+ { source: `t.judge.autoevals.closedQA("did it summarize?");`, judge: undefined },
22
+ { source: `t.check(t.reply, includes("ok"));`, judge: undefined },
23
+ ];
24
+ expect(judgeProbeTargets(evals, configJudge)).toEqual([configJudge]);
25
+ });
26
+
27
+ it("resolves eval-level judge over config-level, like attempt resolution", () => {
28
+ const evalJudge: JudgeConfig = { model: "deepseek-v4", baseUrl: "http://localhost:8787/v1" };
29
+ const evals = [{ source: `t.judge.autoevals.factuality("2")`, judge: evalJudge }];
30
+ expect(judgeProbeTargets(evals, configJudge)).toEqual([evalJudge]);
31
+ });
32
+
33
+ it("dedupes identical effective configs across evals", () => {
34
+ const evals = [
35
+ { source: `t.judge.autoevals.closedQA("a")`, judge: undefined },
36
+ { source: `t.judge.autoevals.closedQA("b")`, judge: undefined },
37
+ ];
38
+ expect(judgeProbeTargets(evals, configJudge)).toEqual([configJudge]);
39
+ });
40
+
41
+ it("returns nothing when judge is used but no config exists (runtime env fallback)", () => {
42
+ const evals = [{ source: `t.judge.autoevals.closedQA("a")`, judge: undefined }];
43
+ expect(judgeProbeTargets(evals, undefined)).toEqual([]);
44
+ });
45
+
46
+ it("does not match judge as part of a longer identifier", () => {
47
+ const evals = [{ source: `const prejudged = true;`, judge: undefined }];
48
+ expect(judgeProbeTargets(evals, configJudge)).toEqual([]);
49
+ });
50
+ });
package/src/runner/run.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // 职责只有编排:指纹缓存在 fingerprint.ts,单 attempt 生命周期在 attempt.ts,
3
3
  // reporter 编排 / 汇总在 report.ts,Sandbox 适配器在 remote-sandbox.ts。
4
4
 
5
+ import { readFile } from "node:fs/promises";
5
6
  import { Effect, Cause, Duration, Exit } from "effect";
6
7
  import { probeJudge } from "../scoring/judge.ts";
7
8
  import { t } from "../i18n/index.ts";
@@ -14,34 +15,47 @@ import type { Attempt, RunOptions } from "./types.ts";
14
15
 
15
16
  export type { AgentRun, RunOptions } from "./types.ts";
16
17
 
18
+ /** 收集本次要探测的 judge 配置:只看「实际要跑、且源码里出现 judge 字样」的 eval 的生效
19
+ * 配置(evalDef.judge ?? config.judge,与 attempt.ts 的 resolveJudge 一致),按
20
+ * model|baseUrl|apiKeyEnv 去重。要跑的 eval 都不用 judge 时返回空 —— 全局配了 judge
21
+ * 也不探测,纯确定性断言的运行不再被 judge key / 端点问题拦下。
22
+ * 源码扫描是启发式:judge 调用藏在 import 的 helper 里时会漏判,漏判只是退回旧行为
23
+ * (评分时才报 judge 错误,损失 fail fast),不影响正确性。 */
24
+ export function judgeProbeTargets(
25
+ evals: Array<{ source: string; judge: JudgeConfig | undefined }>,
26
+ configJudge: JudgeConfig | undefined,
27
+ ): JudgeConfig[] {
28
+ const seen = new Set<string>();
29
+ const toProbe: JudgeConfig[] = [];
30
+ for (const e of evals) {
31
+ const jc = e.judge ?? configJudge;
32
+ if (!jc || !/\bjudge\b/.test(e.source)) continue;
33
+ const key = `${jc.model ?? ""}|${jc.baseUrl ?? ""}|${jc.apiKeyEnv ?? ""}`;
34
+ if (seen.has(key)) continue;
35
+ seen.add(key);
36
+ toProbe.push(jc);
37
+ }
38
+ return toProbe;
39
+ }
40
+
17
41
  export async function runEvals(opts: RunOptions): Promise<RunSummary> {
18
42
  const startedAt = new Date().toISOString();
19
43
  const t0 = Date.now();
20
44
 
21
- // 预检 judge:有显式配置时在第一步验证 API key + 端点可达,避免跑完 agent 才发现 judge 不通。
22
- {
23
- const seen = new Set<string>();
24
- const toProbe: JudgeConfig[] = [];
25
- for (const jc of [opts.config.judge, ...opts.evals.map((e) => e.judge)]) {
26
- if (!jc) continue;
27
- const key = `${jc.model ?? ""}|${jc.baseUrl ?? ""}|${jc.apiKeyEnv ?? ""}`;
28
- if (seen.has(key)) continue;
29
- seen.add(key);
30
- toProbe.push(jc);
45
+ // sourcePath 缓存文件内容,fingerprint judge 预检共用:
46
+ // 矩阵大时(实验 × eval)规划阶段不做串行重复文件读。
47
+ const sourceCache = new Map<string, Promise<string>>();
48
+ const readSource = (path: string): Promise<string> => {
49
+ let p = sourceCache.get(path);
50
+ if (!p) {
51
+ p = readFile(path, "utf-8");
52
+ sourceCache.set(path, p);
31
53
  }
32
- if (toProbe.length > 0) {
33
- process.stderr.write(t("runner.judgePrecheck"));
34
- for (const jc of toProbe) {
35
- const err = await probeJudge(jc, opts.signal);
36
- if (err) throw new Error(err);
37
- }
38
- }
39
- }
54
+ return p;
55
+ };
40
56
 
41
57
  const plannedFingerprints = new Map<string, string>();
42
58
  {
43
- // 并行 + 按 sourcePath 缓存:矩阵大时(实验 × eval)规划阶段不做串行重复文件读。
44
- const sourceCache = new Map<string, Promise<string>>();
45
59
  const jobs: Promise<void>[] = [];
46
60
  for (const run of opts.agentRuns) {
47
61
  for (const evalDef of opts.evals.filter((e) => run.evalFilter(e.id))) {
@@ -100,9 +114,39 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
100
114
  }
101
115
  }
102
116
 
117
+ // 预检 judge:验证 API key + 端点可达,避免跑完 agent 才发现 judge 不通。
118
+ // 放在 attempts 展开之后,fail fast 只对会真正触发 judge 的运行生效
119
+ // (目标收集逻辑见 judgeProbeTargets;全部结果携入、attempts 为空时也自然跳过)。
120
+ {
121
+ const uniqueEvals = [...new Map(attempts.map((a) => [a.evalDef.id, a.evalDef])).values()];
122
+ const sources = await Promise.all(uniqueEvals.map((e) => readSource(e.sourcePath)));
123
+ const toProbe = judgeProbeTargets(
124
+ uniqueEvals.map((e, i) => ({ source: sources[i] ?? "", judge: e.judge })),
125
+ opts.config.judge,
126
+ );
127
+ if (toProbe.length > 0) {
128
+ process.stderr.write(t("runner.judgePrecheck"));
129
+ for (const jc of toProbe) {
130
+ const err = await probeJudge(jc, opts.signal);
131
+ if (err) throw new Error(err);
132
+ }
133
+ }
134
+ }
135
+
103
136
  if (carriedResults.length > 0) {
104
137
  const retryCount = new Set(attempts.map((a) => `${a.run.experimentId ?? ""}|${a.evalDef.id}`)).size;
105
138
  process.stderr.write(t("runner.resumeCarry", { carried: carriedResults.length, retry: retryCount }));
139
+ // 按 experiment 分组列出被复用(跳过)的 eval:不列清单的话,用户只看到数量,
140
+ // 无法核对「跳过的是不是我以为已经过了的那些」。同一 key 多个 run 去重。
141
+ const carriedByExperiment = new Map<string, Set<string>>();
142
+ for (const r of carriedResults) {
143
+ const ids = carriedByExperiment.get(r.experimentId!) ?? new Set<string>();
144
+ ids.add(r.id);
145
+ carriedByExperiment.set(r.experimentId!, ids);
146
+ }
147
+ for (const [experiment, ids] of [...carriedByExperiment].sort(([a], [b]) => a.localeCompare(b))) {
148
+ process.stderr.write(t("runner.resumeCarryDetail", { experiment, evals: [...ids].sort().join(", ") }));
149
+ }
106
150
  }
107
151
 
108
152
  // onRunStart 报「本次实际要跑的 eval」(过滤 + 去重),不是发现到的全部 —— 否则计数误导。
@@ -211,6 +211,16 @@ export interface PriceOverride {
211
211
 
212
212
  // ───────────────────────── 调度编排 ─────────────────────────
213
213
 
214
+ /**
215
+ * 进度行 / 日志里标识一个 run 配置的短名。有 experiment 时用其 basename(唯一,
216
+ * 能区分同 agent 同 model 的实验变体,如 xxx 与 xxx--agents-md;与汇总表口径一致);
217
+ * 无 experiment 时退回 agent/model。live display 以它作行聚合 key,两处必须同源。
218
+ */
219
+ export function runWho(run: Pick<AgentRun, "agent" | "model" | "experimentId">): string {
220
+ if (run.experimentId) return run.experimentId.split("/").pop()!;
221
+ return run.model ? `${run.agent.name}/${run.model}` : run.agent.name;
222
+ }
223
+
214
224
  /** 一个 (agent, model, flags) 的运行配置 —— 由 CLI / 实验展开。 */
215
225
  export interface AgentRun {
216
226
  agent: Agent;