niceeval 0.10.3-canary.7 → 0.11.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/dist/agents/types.d.ts +5 -4
- package/dist/context/turn-errors.d.ts +27 -23
- package/dist/i18n/en.d.ts +14 -0
- package/dist/i18n/en.js +15 -1
- package/dist/i18n/zh-CN.d.ts +15 -1
- package/dist/i18n/zh-CN.js +15 -1
- package/dist/o11y/derive.js +28 -24
- package/dist/o11y/types.d.ts +8 -5
- package/dist/report/components/attempt-detail/UsageTable.js +4 -7
- package/dist/report/components/attempt-detail/compute.d.ts +2 -2
- package/dist/report/components/attempt-detail/compute.js +2 -6
- package/dist/report/components/attempt-detail/faces.js +7 -7
- package/dist/report/components/attempt-detail/index.js +0 -3
- package/dist/report/components/entity-lists/EvalList.js +0 -0
- package/dist/report/components/metric-views/compute.js +1 -1
- package/dist/report/model/types.d.ts +3 -4
- package/dist/results/locator.js +0 -0
- package/dist/results/select.d.ts +6 -0
- package/dist/results/select.js +8 -0
- package/dist/runner/feedback/sink.d.ts +26 -1
- package/dist/runner/fingerprint.d.ts +23 -0
- package/dist/runner/types.d.ts +105 -7
- package/dist/sandbox/errors.d.ts +29 -0
- package/dist/sandbox/resolve.d.ts +9 -0
- package/dist/shared/failure-class.d.ts +91 -0
- package/dist/types.d.ts +1 -0
- package/dist/util.d.ts +3 -2
- package/dist/util.js +31 -5
- package/docs-site/zh/explanation/runner.mdx +35 -0
- package/docs-site/zh/reference/cli.mdx +3 -3
- package/docs-site/zh/reference/events.mdx +4 -4
- package/docs-site/zh/troubleshooting/debugging.mdx +2 -2
- package/docs-site/zh/tutorials/viewing-results.mdx +4 -5
- package/package.json +4 -12
- package/src/agents/ai-sdk.test.ts +26 -0
- package/src/agents/ai-sdk.ts +7 -4
- package/src/agents/index.ts +5 -3
- package/src/agents/langgraph.test.ts +30 -0
- package/src/agents/langgraph.ts +5 -2
- package/src/agents/openai-compat.test.ts +35 -0
- package/src/agents/openai-compat.ts +16 -4
- package/src/agents/sdk-streams.test.ts +66 -0
- package/src/agents/sdk-streams.ts +3 -1
- package/src/agents/types.ts +5 -4
- package/src/cli.ts +55 -14
- package/src/context/context.test.ts +34 -0
- package/src/context/context.ts +14 -5
- package/src/context/send-retry.test.ts +86 -0
- package/src/context/send-retry.ts +37 -12
- package/src/context/session.ts +24 -0
- package/src/context/turn-errors.test.ts +124 -17
- package/src/context/turn-errors.ts +60 -50
- package/src/define.ts +5 -0
- package/src/i18n/en.ts +20 -1
- package/src/i18n/zh-CN.ts +20 -1
- package/src/index.ts +8 -0
- package/src/o11y/cost.ts +4 -2
- package/src/o11y/derive.test.ts +40 -0
- package/src/o11y/derive.ts +28 -22
- package/src/o11y/otlp/sandbox-receiver.test.ts +201 -0
- package/src/o11y/otlp/sandbox-receiver.ts +73 -27
- package/src/o11y/parsers/bub.test.ts +30 -0
- package/src/o11y/parsers/bub.ts +5 -2
- package/src/o11y/parsers/codex.test.ts +19 -0
- package/src/o11y/parsers/codex.ts +5 -2
- package/src/o11y/types.ts +8 -5
- package/src/report/components/attempt-detail/UsageTable.tsx +4 -6
- package/src/report/components/attempt-detail/attempt-components.test.tsx +5 -8
- package/src/report/components/attempt-detail/compute.ts +2 -7
- package/src/report/components/attempt-detail/faces.ts +7 -7
- package/src/report/components/attempt-detail/index.tsx +0 -3
- package/src/report/components/entity-lists/EvalList.tsx +0 -0
- package/src/report/components/metric-views/compute.ts +1 -1
- package/src/report/model/types.ts +3 -4
- package/src/results/format.ts +9 -2
- package/src/results/index.ts +2 -0
- package/src/results/locator.ts +0 -0
- package/src/results/open.ts +132 -21
- package/src/results/select.ts +12 -0
- package/src/results/skipped-notice.ts +0 -0
- package/src/runner/attempt.test.ts +116 -0
- package/src/runner/attempt.ts +89 -8
- package/src/runner/discover.ts +21 -3
- package/src/runner/feedback/coordinator.ts +24 -2
- package/src/runner/feedback/eval-conclusions.ts +6 -3
- package/src/runner/feedback/human.test.ts +300 -6
- package/src/runner/feedback/human.ts +132 -30
- package/src/runner/feedback/json.test.ts +127 -2
- package/src/runner/feedback/json.ts +51 -3
- package/src/runner/feedback/reducer.test.ts +328 -29
- package/src/runner/feedback/reducer.ts +84 -9
- package/src/runner/feedback/sink.ts +39 -1
- package/src/runner/fingerprint.ts +49 -19
- package/src/runner/gate-lease.test.ts +510 -0
- package/src/runner/gate-lease.ts +350 -0
- package/src/runner/lock.test.ts +454 -0
- package/src/runner/lock.ts +288 -0
- package/src/runner/report.test.ts +1 -0
- package/src/runner/run.test.ts +2044 -9
- package/src/runner/run.ts +825 -61
- package/src/runner/teardown-registry.ts +20 -78
- package/src/runner/types.ts +103 -7
- package/src/sandbox/errors.test.ts +72 -0
- package/src/sandbox/errors.ts +83 -0
- package/src/sandbox/keep-registry.ts +22 -46
- package/src/sandbox/resolve.test.ts +100 -0
- package/src/sandbox/resolve.ts +84 -27
- package/src/shared/entry-file-store.test.ts +149 -0
- package/src/shared/entry-file-store.ts +117 -0
- package/src/shared/failure-class.test.ts +137 -0
- package/src/shared/failure-class.ts +175 -0
- package/src/show/index.ts +5 -6
- package/src/show/render.test.ts +116 -15
- package/src/show/render.ts +124 -35
- package/src/types.ts +9 -0
- package/src/util.ts +31 -4
- package/src/view/app/App.tsx +5 -1
- package/src/view/client-dist/app.js +1 -1
- package/src/view/data.ts +5 -9
- package/src/view/shared/types.ts +7 -1
- package/src/view/view-report.test.ts +54 -0
package/src/runner/attempt.ts
CHANGED
|
@@ -30,6 +30,11 @@ import { createChangeLedger, type ChangeLedger } from "./ledger.ts";
|
|
|
30
30
|
import { deriveDiffData, emptyDiffData } from "../scoring/diff.ts";
|
|
31
31
|
import { createRemoteSandbox, withEvalLocalPaths } from "./remote-sandbox.ts";
|
|
32
32
|
import { recordFact, type FactValue } from "../shared/facts.ts";
|
|
33
|
+
import {
|
|
34
|
+
attemptFailureInfo,
|
|
35
|
+
resolveAttemptFailureClass,
|
|
36
|
+
type FailureClass,
|
|
37
|
+
} from "../shared/failure-class.ts";
|
|
33
38
|
import type { CapturedEvalSource } from "./eval-source.ts";
|
|
34
39
|
import type {
|
|
35
40
|
AgentContext,
|
|
@@ -66,6 +71,18 @@ import type {
|
|
|
66
71
|
RunOptions,
|
|
67
72
|
} from "./types.ts";
|
|
68
73
|
|
|
74
|
+
/**
|
|
75
|
+
* 一次终局失败的空间轴回执:止损闸的消费点(见 docs/feature/error-classification/architecture.md
|
|
76
|
+
* 「止损执行体」)。`class` 是分类链决议出的 `FailureClass`,`text` 与报错文案同源(作者的修复
|
|
77
|
+
* 提示),`phase` 是失败所在的生命周期阶段。回执是旁路:不改 verdict、不改 `AttemptError` 形状,
|
|
78
|
+
* 也不走错误通道传播(attempt fiber 的 `E` 仍恒为 `never`)。
|
|
79
|
+
*/
|
|
80
|
+
export interface AttemptFailureDeclaration {
|
|
81
|
+
readonly class: FailureClass;
|
|
82
|
+
readonly phase: LifecyclePhase;
|
|
83
|
+
readonly text: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
69
86
|
export interface RunAttemptEffectOptions {
|
|
70
87
|
/** 父级调度器的中断信号;测试直调时省略。 */
|
|
71
88
|
parentSignal?: AbortSignal;
|
|
@@ -79,13 +96,19 @@ export interface RunAttemptEffectOptions {
|
|
|
79
96
|
* 省略时(如测试直调)退避不释放槽位。
|
|
80
97
|
*/
|
|
81
98
|
concurrencySlot?: ConcurrencySlot;
|
|
99
|
+
/**
|
|
100
|
+
* 终局失败的空间轴回执(每条终局失败各回调一次,含 per-attempt teardown 的失败——teardown
|
|
101
|
+
* 声明照常落闸、不改 verdict,见 architecture.md「生命周期边界」)。省略(如测试直调)时
|
|
102
|
+
* 分类照常决议、只是无人消费。回调必须不抛错:它跑在 attempt 的失败路径上,不得掩盖原始失败。
|
|
103
|
+
*/
|
|
104
|
+
onFailureClass?: (declaration: AttemptFailureDeclaration) => void;
|
|
82
105
|
}
|
|
83
106
|
|
|
84
107
|
export function runAttemptEffect(
|
|
85
108
|
a: Attempt,
|
|
86
109
|
opts: RunOptions,
|
|
87
110
|
sandboxSem: Effect.Semaphore,
|
|
88
|
-
{ parentSignal, onPhase, concurrencySlot }: RunAttemptEffectOptions = {},
|
|
111
|
+
{ parentSignal, onPhase, concurrencySlot, onFailureClass }: RunAttemptEffectOptions = {},
|
|
89
112
|
): Effect.Effect<EvalResult> {
|
|
90
113
|
const config = opts.config;
|
|
91
114
|
const { evalDef, run, attempt } = a;
|
|
@@ -155,6 +178,21 @@ export function runAttemptEffect(
|
|
|
155
178
|
onPhase?.(phase);
|
|
156
179
|
reportAttemptLifecycle({ type: "attempt:phase", at: Date.now(), identity, phase });
|
|
157
180
|
};
|
|
181
|
+
/**
|
|
182
|
+
* 终局失败的分类与回执 —— **必须在把错误折成纯数据 `AttemptError` 之前调**:折完之后原错误
|
|
183
|
+
* 对象(连同它携带的 `_tag`/`class` 与 cause 链)就不再是任何人的引用,空间轴声明再也读不出来。
|
|
184
|
+
* 走生命周期链的三道决议(抛出点声明 → 实验分类器 → 缺省不可重试,见
|
|
185
|
+
* src/shared/failure-class.ts 的 `resolveAttemptFailureClass`);turn 失败的时间轴已由 send 重试
|
|
186
|
+
* 执行体在 context 层消费完,浮出到这里的一定是终局失败,这里只读空间轴。
|
|
187
|
+
* 缺省档(`scope` 省略或 `"attempt"`)不回执:那是「死因只属于本次执行」,没有闸可落。
|
|
188
|
+
*/
|
|
189
|
+
const declareFailure = (phase: LifecyclePhase, e: unknown): void => {
|
|
190
|
+
if (!onFailureClass) return;
|
|
191
|
+
const info = attemptFailureInfo(phase, e);
|
|
192
|
+
const cls = resolveAttemptFailureClass(info, run.classifyFailure);
|
|
193
|
+
if (cls.scope !== "eval" && cls.scope !== "experiment") return;
|
|
194
|
+
onFailureClass({ class: cls, phase, text: info.text });
|
|
195
|
+
};
|
|
158
196
|
// 本 attempt 累计的运行事实(与 verdict/diagnostics 独立):sandbox hook / eval.setup·teardown /
|
|
159
197
|
// agent setup·send·teardown 经 ctx.fact() 上报的都落这里(同一 attempt 内后写覆盖先写),
|
|
160
198
|
// 收尾时并入结果的 facts 字段(见 finally 末尾,与 diagnostics 同一种「累加器 + finally 并入」模式)。
|
|
@@ -188,12 +226,21 @@ export function runAttemptEffect(
|
|
|
188
226
|
if (input.dedupeKey !== undefined) dedupeIndex.set(input.dedupeKey, record);
|
|
189
227
|
diagnostics.push(record);
|
|
190
228
|
// 同时进运行级永久事件流(human 撤下 dashboard 后追加、agent/ci 各追加一条,去重按 key)。
|
|
229
|
+
// `key` 只管折叠到多细(作者没给 dedupeKey 时折到「这一条 attempt 的这种诊断」,身份因此
|
|
230
|
+
// 编进去);对外稳定词法始终单独给 `code`,不让消费方从 key 反推(见 sink.ts 的
|
|
231
|
+
// DiagnosticInput.code、docs/feature/experiments/cli.md 的 WarningEvent)。
|
|
191
232
|
reportDiagnostic({
|
|
192
233
|
key: input.dedupeKey ?? `${input.code}:${encodeAttemptKey(identity)}`,
|
|
234
|
+
code: input.code,
|
|
193
235
|
severity: input.level,
|
|
194
236
|
message: input.message,
|
|
195
237
|
identity,
|
|
196
|
-
data
|
|
238
|
+
// phase 由运行器给,且**压过**作者 `data` 里的同名字段:`WarningEvent.phase` 是
|
|
239
|
+
// `LifecyclePhase` 闭集,取值只能由「这条诊断报上来时运行器正处在哪一步」决定。
|
|
240
|
+
// 作者的 `data` 是开放词表,让它盖住这个字段等于允许从 eval / adapter 代码里冒充一个
|
|
241
|
+
// 别的(甚至不在闭集里的)阶段,消费方按 phase 分支就此失效——与 ScopedFeedback
|
|
242
|
+
// 「两个方法都不接受 phase」是同一条纪律(见 src/shared/types.ts 的接口注释)。
|
|
243
|
+
data: { ...input.data, phase },
|
|
197
244
|
});
|
|
198
245
|
};
|
|
199
246
|
// 作用域反馈:progress 走 attempt:progress(短命状态,归因由 runner 的当前阶段决定),
|
|
@@ -355,7 +402,12 @@ export function runAttemptEffect(
|
|
|
355
402
|
const proto = run.agent.tracing?.protocol;
|
|
356
403
|
log(t("runner.otlpReceiver", { endpoint, proto: proto ? ` (${proto})` : "" }));
|
|
357
404
|
} else {
|
|
358
|
-
// 远程沙箱(e2b / vercel):在沙箱内起 collector,agent 往 localhost
|
|
405
|
+
// 远程沙箱(e2b / vercel):在沙箱内起 collector,agent 往 localhost 端口发。
|
|
406
|
+
// 这一步是「创建本次 tracing 出口」(telemetry.configure 的定义,见
|
|
407
|
+
// docs/feature/experiments/cli.md 的阶段表),而且是唯一一条要往沙箱里传脚本、
|
|
408
|
+
// 起进程、等端口的 tracing 出口——秒级耗时与失败都归它自己,不能顺着上一个还开着的
|
|
409
|
+
// 阶段记成 sandbox.create(那样 collector 起不来会伪装成「起沙箱失败」)。
|
|
410
|
+
enterPhase("telemetry.configure");
|
|
359
411
|
receiver = yield* createInSandboxTraceReceiver(sandbox);
|
|
360
412
|
const endpoint = receiver.endpoint("");
|
|
361
413
|
const env = run.agent.tracing?.env?.(endpoint);
|
|
@@ -429,6 +481,7 @@ export function runAttemptEffect(
|
|
|
429
481
|
facts,
|
|
430
482
|
commands,
|
|
431
483
|
concurrencySlot,
|
|
484
|
+
declareFailure,
|
|
432
485
|
registerEvidence: (getEvents, getUsage) => {
|
|
433
486
|
liveEvents = getEvents;
|
|
434
487
|
liveUsage = getUsage;
|
|
@@ -543,11 +596,18 @@ export function runAttemptEffect(
|
|
|
543
596
|
Effect.catchAllCause((cause) =>
|
|
544
597
|
Cause.isInterrupted(cause)
|
|
545
598
|
? Effect.failCause(cause)
|
|
546
|
-
: Effect.
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
599
|
+
: Effect.suspend(() => {
|
|
600
|
+
// 资源获取 / Scope 层的意外(起沙箱失败、provisioning 的确定性配置死因)同样是终局
|
|
601
|
+
// 失败:先读空间轴回执,再折成纯数据 AttemptError(顺序不可换,见 declareFailure)。
|
|
602
|
+
const raw = Cause.squash(cause);
|
|
603
|
+
const phase = (sendActive ? "agent.run" : lastPhase) ?? "eval.run";
|
|
604
|
+
declareFailure(phase, raw);
|
|
605
|
+
return Effect.succeed({
|
|
606
|
+
...base,
|
|
607
|
+
durationMs: Date.now() - t0,
|
|
608
|
+
error: errorFromThrown(raw, sendActive ? "agent.run" : lastPhase),
|
|
609
|
+
...(commands.length > 0 ? { commands: [...commands] } : {}),
|
|
610
|
+
});
|
|
551
611
|
}),
|
|
552
612
|
),
|
|
553
613
|
// 结果封口在 Scope release 完成之后:sandbox.stop 已由 finalizer 写进 recorder,
|
|
@@ -624,6 +684,9 @@ interface AttemptResources {
|
|
|
624
684
|
commands: FailedCommandEvidence[];
|
|
625
685
|
/** turn 级重试退避期间释放/收回的全局并发槽位;透传给 createEvalContext。 */
|
|
626
686
|
concurrencySlot?: ConcurrencySlot;
|
|
687
|
+
/** 终局失败的空间轴回执(runAttemptEffect 持有的同一个闭包):body 的失败路径与 finally 里的
|
|
688
|
+
* per-attempt teardown 失败都经它上报,止损闸据此落闸(见 runAttemptEffect 的 declareFailure)。 */
|
|
689
|
+
declareFailure: (phase: LifecyclePhase, e: unknown) => void;
|
|
627
690
|
/** SessionManager 一建好就登记事件/用量的读取句柄回外层(超时证据保全用,见
|
|
628
691
|
* runAttemptEffect 顶部 liveEvents/liveUsage 的注释与 docs/runner.md「超时:双层保护」)。 */
|
|
629
692
|
registerEvidence: (getEvents: () => readonly StreamEvent[], getUsage: () => Usage) => void;
|
|
@@ -660,6 +723,7 @@ async function runAttemptBody(
|
|
|
660
723
|
facts,
|
|
661
724
|
commands,
|
|
662
725
|
concurrencySlot,
|
|
726
|
+
declareFailure,
|
|
663
727
|
registerEvidence,
|
|
664
728
|
registerLedger,
|
|
665
729
|
} = res;
|
|
@@ -826,6 +890,10 @@ async function runAttemptBody(
|
|
|
826
890
|
feedback,
|
|
827
891
|
fact,
|
|
828
892
|
concurrencySlot,
|
|
893
|
+
// 实验分类器:turn 链上排在 adapter 的 classifyTurnError 之前(见
|
|
894
|
+
// docs/feature/error-classification/architecture.md「分类链」)。与本文件 declareFailure
|
|
895
|
+
// 走的生命周期链是同一个函数,两条链的决议序各自单源在 turn-errors.ts / failure-class.ts。
|
|
896
|
+
experimentClassifier: run.classifyFailure,
|
|
829
897
|
// 题型:计分制下句柄上的 .gate() 是前置(就地求值 + 挂了中止 test()),见 collector。
|
|
830
898
|
scoring: evalDef.scoring ?? "pass",
|
|
831
899
|
// 前置断言就地求值要看当前已提交窗口的 agent diff(非沙箱型没有分类账,省略)。
|
|
@@ -876,10 +944,15 @@ async function runAttemptBody(
|
|
|
876
944
|
// TurnFailed 是 eval 驱动 agent 时的可读失败(message 首行是一层摘要,后续行是
|
|
877
945
|
// diagnose 的 output tail 等下钻证据,单行面各自取首行);稳定 code
|
|
878
946
|
// `turn-failed`,不带控制流 stack(那指向 control-flow.ts,对定位无益)。
|
|
947
|
+
// 分类先读:turn 链决议出的 FailureClass 由 expectOk() 铸造 TurnFailed 时挂在错误上,
|
|
948
|
+
// 下一行折成纯数据后就读不到了(见 declareFailure)。
|
|
949
|
+
declareFailure(getPhase() ?? "eval.run", e);
|
|
879
950
|
error = { code: "turn-failed", message: e.message, phase: getPhase() ?? "eval.run" };
|
|
880
951
|
} else {
|
|
881
952
|
// eval 脚本(比如引用了已改名/删掉的 API)抛出的 TypeError:message 是一层原因,完整 stack
|
|
882
953
|
// 单独进 `error.stack`,niceeval show 展开时才看得到 eval 文件的 file:line。
|
|
954
|
+
// 作者从 test(t) 体内抛的 ExperimentFatalError / EvalFatalError 也走这条分支。
|
|
955
|
+
declareFailure(getPhase() ?? "eval.run", e);
|
|
883
956
|
error = errorFromThrown(e, getPhase());
|
|
884
957
|
}
|
|
885
958
|
}
|
|
@@ -1043,6 +1116,9 @@ async function runAttemptBody(
|
|
|
1043
1116
|
return value;
|
|
1044
1117
|
} catch (e) {
|
|
1045
1118
|
recorder.failCurrent();
|
|
1119
|
+
// sandbox 钩子 / agent.setup / eval.setup / 评分链路抛出的终局失败:同样先读空间轴回执,
|
|
1120
|
+
// 再折成纯数据 AttemptError(顺序不可换,见 declareFailure)。
|
|
1121
|
+
declareFailure(getPhase() ?? "eval.run", e);
|
|
1046
1122
|
const value: EvalResult = {
|
|
1047
1123
|
...base,
|
|
1048
1124
|
durationMs: Date.now() - t0,
|
|
@@ -1069,6 +1145,9 @@ async function runAttemptBody(
|
|
|
1069
1145
|
await withCleanupTimeout(() => evalDef.teardown!(withEvalLocalPaths(sandbox, evalDef.baseDir), hookCtx));
|
|
1070
1146
|
} catch (e) {
|
|
1071
1147
|
// 收尾失败只是 diagnostic,不改判定 —— 挂到 attempt.diagnostics(见 finally 末尾并入)。
|
|
1148
|
+
// 但空间轴声明照常落闸:知识就是知识,兄弟 attempt 还在派发中(architecture.md
|
|
1149
|
+
// 「生命周期边界」)。落闸不改 verdict,两件事互不影响。
|
|
1150
|
+
declareFailure("eval.teardown", e);
|
|
1072
1151
|
diagnostics.push(teardownDiagnostic("eval.teardown", e));
|
|
1073
1152
|
throw e; // 让 measureClosing 把这段标 failed
|
|
1074
1153
|
}
|
|
@@ -1082,6 +1161,7 @@ async function runAttemptBody(
|
|
|
1082
1161
|
try {
|
|
1083
1162
|
await withCleanupTimeout(() => run.agent.teardown!(sandbox, attemptCtx));
|
|
1084
1163
|
} catch (e) {
|
|
1164
|
+
declareFailure("agent.teardown", e);
|
|
1085
1165
|
diagnostics.push(teardownDiagnostic("agent.teardown", e));
|
|
1086
1166
|
throw e;
|
|
1087
1167
|
}
|
|
@@ -1101,6 +1181,7 @@ async function runAttemptBody(
|
|
|
1101
1181
|
try {
|
|
1102
1182
|
await withCleanupTimeout(() => sandboxTeardownHooks[i](sandbox, hookCtx));
|
|
1103
1183
|
} catch (e) {
|
|
1184
|
+
declareFailure("sandbox.teardown", e);
|
|
1104
1185
|
diagnostics.push(teardownDiagnostic("sandbox.teardown", e));
|
|
1105
1186
|
}
|
|
1106
1187
|
}
|
package/src/runner/discover.ts
CHANGED
|
@@ -12,6 +12,24 @@ import type { DiscoveredEval, DiscoveredExperiment, EvalDef, ExperimentDef } fro
|
|
|
12
12
|
|
|
13
13
|
const SKIP_DIRS = new Set(["node_modules", ".git", ".niceeval", "dist", ".next"]);
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* 发现阶段的动态 import 会执行被加载文件的**顶层代码**(配置文件里现拉 registry、读 .env、
|
|
17
|
+
* 连服务都很常见)。裸抛出去的话用户只看到一个不知从何而来的 `TypeError: fetch failed`——
|
|
18
|
+
* 发现要遍历整棵 `evals/` / `experiments/` 树,一个文件炸了并不会告诉你是哪一个。
|
|
19
|
+
* 这里把文件路径钉进 message,原错误挂 `cause`(`formatThrown` 会展开成 `caused by:` 链)。
|
|
20
|
+
*/
|
|
21
|
+
async function importDiscovered<T>(file: string, root: string, kind: "eval" | "experiment"): Promise<T> {
|
|
22
|
+
try {
|
|
23
|
+
return (await import(pathToFileURL(file).href)) as T;
|
|
24
|
+
} catch (e) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Failed to load ${kind} file ${relative(root, file)}: its top-level code threw while being imported. ` +
|
|
27
|
+
`Fix the error below, or move the work into the ${kind} body so it only runs when this ${kind} is selected.`,
|
|
28
|
+
{ cause: e },
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
async function walkFiles(dir: string, match: (name: string) => boolean): Promise<string[]> {
|
|
16
34
|
const out: string[] = [];
|
|
17
35
|
async function walk(current: string): Promise<void> {
|
|
@@ -40,9 +58,9 @@ export async function discoverEvals(root: string): Promise<DiscoveredEval[]> {
|
|
|
40
58
|
const files = (await walkFiles(dir, (n) => n.endsWith(".eval.ts") || n.endsWith(".eval.tsx"))).sort();
|
|
41
59
|
const out: DiscoveredEval[] = [];
|
|
42
60
|
for (const file of files) {
|
|
43
|
-
const mod =
|
|
61
|
+
const mod = await importDiscovered<{
|
|
44
62
|
default?: EvalDef | EvalDef[] | Record<string, EvalDef>;
|
|
45
|
-
};
|
|
63
|
+
}>(file, root, "eval");
|
|
46
64
|
const def = mod.default;
|
|
47
65
|
if (!def) continue;
|
|
48
66
|
const baseId = relative(dir, file).replace(/\.eval\.tsx?$/, "").split(sep).join("/");
|
|
@@ -107,7 +125,7 @@ export async function discoverExperiments(root: string): Promise<DiscoveredExper
|
|
|
107
125
|
const files = (await walkFiles(dir, (n) => n.endsWith(".ts") && !n.endsWith(".d.ts"))).sort();
|
|
108
126
|
const out: DiscoveredExperiment[] = [];
|
|
109
127
|
for (const file of files) {
|
|
110
|
-
const mod =
|
|
128
|
+
const mod = await importDiscovered<{ default?: ExperimentDef }>(file, root, "experiment");
|
|
111
129
|
const def = mod.default;
|
|
112
130
|
if (!def || !def.agent) continue;
|
|
113
131
|
const id = relative(dir, file)
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type ExperimentHookInput,
|
|
29
29
|
type ExperimentProgressInput,
|
|
30
30
|
type PrecheckInput,
|
|
31
|
+
type LockWaitInput,
|
|
31
32
|
type FailureInput,
|
|
32
33
|
type FeedbackSink,
|
|
33
34
|
type KeptInput,
|
|
@@ -198,6 +199,7 @@ export function createFeedbackCoordinator(options: FeedbackCoordinatorOptions):
|
|
|
198
199
|
type: "diagnostic",
|
|
199
200
|
at: io.clock.now(),
|
|
200
201
|
key: input.key,
|
|
202
|
+
...(input.code !== undefined ? { code: input.code } : {}),
|
|
201
203
|
severity: input.severity,
|
|
202
204
|
message: input.message,
|
|
203
205
|
identity: input.identity,
|
|
@@ -272,6 +274,22 @@ export function createFeedbackCoordinator(options: FeedbackCoordinatorOptions):
|
|
|
272
274
|
});
|
|
273
275
|
}
|
|
274
276
|
|
|
277
|
+
function lockWait(input: LockWaitInput): void {
|
|
278
|
+
emit({
|
|
279
|
+
type: "lock-wait",
|
|
280
|
+
at: io.clock.now(),
|
|
281
|
+
experimentId: input.experimentId,
|
|
282
|
+
evalId: input.evalId,
|
|
283
|
+
status: input.status,
|
|
284
|
+
...(input.holderPid !== undefined ? { holderPid: input.holderPid } : {}),
|
|
285
|
+
...(input.holderHost !== undefined ? { holderHost: input.holderHost } : {}),
|
|
286
|
+
...(input.attempts !== undefined ? { attempts: input.attempts } : {}),
|
|
287
|
+
...(input.carried !== undefined ? { carried: input.carried } : {}),
|
|
288
|
+
...(input.dispatched !== undefined ? { dispatched: input.dispatched } : {}),
|
|
289
|
+
...(input.waitedMs !== undefined ? { waitedMs: input.waitedMs } : {}),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
275
293
|
function reporterError(input: { reporter: string; required: boolean; message: string }): void {
|
|
276
294
|
emit({
|
|
277
295
|
type: "reporter-error",
|
|
@@ -313,6 +331,7 @@ export function createFeedbackCoordinator(options: FeedbackCoordinatorOptions):
|
|
|
313
331
|
experimentHook,
|
|
314
332
|
experimentProgress,
|
|
315
333
|
precheck,
|
|
334
|
+
lockWait,
|
|
316
335
|
lifecycle,
|
|
317
336
|
});
|
|
318
337
|
emit({ type: "plan", at: startedAtMs, plan });
|
|
@@ -378,6 +397,7 @@ export function createFeedbackCoordinator(options: FeedbackCoordinatorOptions):
|
|
|
378
397
|
experimentHook,
|
|
379
398
|
experimentProgress,
|
|
380
399
|
precheck,
|
|
400
|
+
lockWait,
|
|
381
401
|
lifecycle,
|
|
382
402
|
stopDynamic,
|
|
383
403
|
finish,
|
|
@@ -404,8 +424,10 @@ function fallbackTextFor(event: DurableFeedbackEvent): string | undefined {
|
|
|
404
424
|
case "saved":
|
|
405
425
|
case "experiment-hook":
|
|
406
426
|
case "precheck":
|
|
407
|
-
|
|
408
|
-
//
|
|
427
|
+
case "lock-wait":
|
|
428
|
+
// 钩子/预检/锁等待起止都不是失败证据:setup 失败的每条 attempt 另有 "failure" 事件
|
|
429
|
+
// 兜底,预检失败以既有错误路径中止运行,锁等待解决与否不改变任何 verdict——
|
|
430
|
+
// renderer 崩溃丢一行起止不丢数据。
|
|
409
431
|
return undefined;
|
|
410
432
|
}
|
|
411
433
|
}
|
|
@@ -40,12 +40,15 @@ export interface EvalConclusionRow {
|
|
|
40
40
|
rate?: number;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/** `diagnostics` 里
|
|
44
|
-
* 的记录(理论上不应出现——fail-fast 诊断恒带 identity,见 run.ts)不计入任何 key。
|
|
43
|
+
/** `diagnostics` 里 fail-fast 记录按 (experiment, eval) 求和的未派发次数;没有 `identity`
|
|
44
|
+
* 的记录(理论上不应出现——fail-fast 诊断恒带 identity,见 run.ts)不计入任何 key。
|
|
45
|
+
* 归类按稳定词法 `code`(省略时回落到 key 首段,缺省 key 恒是 `${code}:${identity}`),不按
|
|
46
|
+
* `key` 前缀:key 里编着折叠身份,拿它匹配会在身份编码变动时静默失配,减不掉的那部分
|
|
47
|
+
* fail-fast 份额会当成真实的 earlyExit 省略数报出去。 */
|
|
45
48
|
function failFastByEval(diagnostics: readonly DiagnosticNotice[]): ReadonlyMap<string, number> {
|
|
46
49
|
const result = new Map<string, number>();
|
|
47
50
|
for (const d of diagnostics) {
|
|
48
|
-
if (
|
|
51
|
+
if ((d.code ?? d.key.split(":", 1)[0]) !== "fail-fast" || !d.identity) continue;
|
|
49
52
|
const key = evalConclusionKey(d.identity);
|
|
50
53
|
result.set(key, (result.get(key) ?? 0) + d.count);
|
|
51
54
|
}
|