niceeval 0.4.3 → 0.4.5

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.
Files changed (179) hide show
  1. package/README.md +21 -12
  2. package/README.zh.md +8 -9
  3. package/docs-site/.mintignore +13 -0
  4. package/docs-site/AGENTS.md +46 -0
  5. package/docs-site/concepts/adapter.mdx +287 -0
  6. package/docs-site/concepts/assert.mdx +243 -0
  7. package/docs-site/concepts/drive.mdx +118 -0
  8. package/docs-site/concepts/evals.mdx +108 -0
  9. package/docs-site/concepts/experiment.mdx +37 -0
  10. package/docs-site/concepts/hitl.mdx +83 -0
  11. package/docs-site/concepts/judge.mdx +129 -0
  12. package/docs-site/concepts/overview.mdx +98 -0
  13. package/docs-site/concepts/tier.mdx +42 -0
  14. package/docs-site/docs-ref/00-index.md +23 -0
  15. package/docs-site/docs-ref/01-page-types.md +43 -0
  16. package/docs-site/docs-ref/03-style-rules.md +45 -0
  17. package/docs-site/docs-ref/04-code-examples.md +37 -0
  18. package/docs-site/docs-ref/05-dx-failure-paths.md +45 -0
  19. package/docs-site/docs-ref/06-checklists.md +53 -0
  20. package/docs-site/docs.json +256 -0
  21. package/docs-site/example/ai-agent-application.mdx +148 -0
  22. package/docs-site/example/claude-code-codex-plugin.mdx +162 -0
  23. package/docs-site/example/claude-code-codex-skill.mdx +147 -0
  24. package/docs-site/example/showcase.mdx +39 -0
  25. package/docs-site/example/tier1-ai-sdk-v7.mdx +136 -0
  26. package/docs-site/example/tier1-claude-sdk.mdx +119 -0
  27. package/docs-site/example/tier1-codex-sdk.mdx +111 -0
  28. package/docs-site/example/tier1-langgraph.mdx +119 -0
  29. package/docs-site/example/tier1-pi-sdk.mdx +119 -0
  30. package/docs-site/favicon.svg +5 -0
  31. package/docs-site/github-diff.css +135 -0
  32. package/docs-site/github-diff.js +11 -0
  33. package/docs-site/guides/authoring.mdx +129 -0
  34. package/docs-site/guides/ci-integration.mdx +97 -0
  35. package/docs-site/guides/connect-otel.mdx +209 -0
  36. package/docs-site/guides/connect-your-agent.mdx +221 -0
  37. package/docs-site/guides/dataset-fanout.mdx +98 -0
  38. package/docs-site/guides/experiments.mdx +71 -0
  39. package/docs-site/guides/fixtures.mdx +147 -0
  40. package/docs-site/guides/reporters.mdx +113 -0
  41. package/docs-site/guides/runner.mdx +79 -0
  42. package/docs-site/guides/sandbox-agent.mdx +138 -0
  43. package/docs-site/guides/sandbox-backends.mdx +64 -0
  44. package/docs-site/guides/scoring-guide.mdx +92 -0
  45. package/docs-site/guides/viewing-results.mdx +195 -0
  46. package/docs-site/guides/write-experiment.mdx +81 -0
  47. package/docs-site/guides/write-send.mdx +347 -0
  48. package/docs-site/images/adapter-tiers-zh.svg +60 -0
  49. package/docs-site/images/agent-turn-roundtrip-en.svg +62 -0
  50. package/docs-site/images/agent-turn-roundtrip-zh.svg +77 -0
  51. package/docs-site/images/hitl-handshake-zh.svg +89 -0
  52. package/docs-site/images/logo.svg +6 -0
  53. package/docs-site/index.mdx +181 -0
  54. package/docs-site/introduction.mdx +141 -0
  55. package/docs-site/quickstart.mdx +136 -0
  56. package/docs-site/reference/builtin-agents.mdx +161 -0
  57. package/docs-site/reference/capabilities.mdx +76 -0
  58. package/docs-site/reference/cli.mdx +120 -0
  59. package/docs-site/reference/define-agent.mdx +168 -0
  60. package/docs-site/reference/define-config.mdx +41 -0
  61. package/docs-site/reference/define-eval.mdx +160 -0
  62. package/docs-site/reference/events.mdx +131 -0
  63. package/docs-site/reference/expect.mdx +112 -0
  64. package/docs-site/tracker.js +8 -0
  65. package/docs-site/zh/concepts/adapter.mdx +286 -0
  66. package/docs-site/zh/concepts/assert.mdx +243 -0
  67. package/docs-site/zh/concepts/drive.mdx +118 -0
  68. package/docs-site/zh/concepts/evals.mdx +124 -0
  69. package/docs-site/zh/concepts/experiment.mdx +37 -0
  70. package/docs-site/zh/concepts/hitl.mdx +83 -0
  71. package/docs-site/zh/concepts/judge.mdx +130 -0
  72. package/docs-site/zh/concepts/overview.mdx +109 -0
  73. package/docs-site/zh/concepts/tier.mdx +44 -0
  74. package/docs-site/zh/example/ai-agent-application.mdx +152 -0
  75. package/docs-site/zh/example/claude-code-codex-plugin.mdx +163 -0
  76. package/docs-site/zh/example/claude-code-codex-skill.mdx +147 -0
  77. package/docs-site/zh/example/showcase.mdx +39 -0
  78. package/docs-site/zh/example/tier1-ai-sdk-v7.mdx +140 -0
  79. package/docs-site/zh/example/tier1-claude-sdk.mdx +122 -0
  80. package/docs-site/zh/example/tier1-codex-sdk.mdx +117 -0
  81. package/docs-site/zh/example/tier1-langgraph.mdx +123 -0
  82. package/docs-site/zh/example/tier1-pi-sdk.mdx +122 -0
  83. package/docs-site/zh/guides/agent-feedback-loop.mdx +79 -0
  84. package/docs-site/zh/guides/authoring.mdx +129 -0
  85. package/docs-site/zh/guides/ci-integration.mdx +97 -0
  86. package/docs-site/zh/guides/connect-otel.mdx +208 -0
  87. package/docs-site/zh/guides/connect-your-agent.mdx +226 -0
  88. package/docs-site/zh/guides/dataset-fanout.mdx +83 -0
  89. package/docs-site/zh/guides/experiments.mdx +72 -0
  90. package/docs-site/zh/guides/fixtures.mdx +147 -0
  91. package/docs-site/zh/guides/official-adapters.mdx +132 -0
  92. package/docs-site/zh/guides/reporters.mdx +113 -0
  93. package/docs-site/zh/guides/runner.mdx +82 -0
  94. package/docs-site/zh/guides/sandbox-agent.mdx +139 -0
  95. package/docs-site/zh/guides/sandbox-backends.mdx +75 -0
  96. package/docs-site/zh/guides/scoring-guide.mdx +92 -0
  97. package/docs-site/zh/guides/viewing-results.mdx +195 -0
  98. package/docs-site/zh/guides/write-experiment.mdx +101 -0
  99. package/docs-site/zh/guides/write-send.mdx +353 -0
  100. package/docs-site/zh/index.mdx +180 -0
  101. package/docs-site/zh/introduction.mdx +140 -0
  102. package/docs-site/zh/quickstart.mdx +137 -0
  103. package/docs-site/zh/reference/builtin-agents.mdx +364 -0
  104. package/docs-site/zh/reference/capabilities.mdx +76 -0
  105. package/docs-site/zh/reference/cli.mdx +140 -0
  106. package/docs-site/zh/reference/define-agent.mdx +461 -0
  107. package/docs-site/zh/reference/define-config.mdx +107 -0
  108. package/docs-site/zh/reference/define-eval.mdx +674 -0
  109. package/docs-site/zh/reference/events.mdx +252 -0
  110. package/docs-site/zh/reference/expect.mdx +208 -0
  111. package/package.json +3 -2
  112. package/src/agents/bub.ts +1 -1
  113. package/src/agents/claude-code.ts +8 -5
  114. package/src/agents/codex.ts +9 -5
  115. package/src/agents/sdk-streams.test.ts +4 -4
  116. package/src/agents/sdk-streams.ts +14 -9
  117. package/src/agents/types.ts +40 -1
  118. package/src/agents/ui-message-stream.ts +1 -0
  119. package/src/cli.ts +40 -10
  120. package/src/context/types.ts +140 -0
  121. package/src/expect/index.ts +15 -8
  122. package/src/i18n/en.ts +20 -6
  123. package/src/i18n/zh-CN.ts +20 -6
  124. package/src/o11y/parsers/bub.test.ts +71 -0
  125. package/src/o11y/parsers/bub.ts +5 -0
  126. package/src/o11y/types.ts +27 -2
  127. package/src/runner/attempt.ts +2 -1
  128. package/src/runner/reporters/artifacts.ts +11 -3
  129. package/src/runner/reporters/live.ts +45 -8
  130. package/src/runner/run.test.ts +50 -0
  131. package/src/runner/run.ts +135 -41
  132. package/src/runner/types.ts +55 -2
  133. package/src/sandbox/types.ts +18 -0
  134. package/src/scoring/types.ts +5 -0
  135. package/src/shared/types.ts +3 -0
  136. package/src/util.test.ts +26 -1
  137. package/src/util.ts +16 -0
  138. package/src/view/app/App.tsx +2 -2
  139. package/src/view/app/components/AttemptModal.tsx +2 -0
  140. package/src/view/app/components/CopyControls.tsx +87 -28
  141. package/src/view/app/i18n.ts +3 -3
  142. package/src/view/client-dist/app.css +1 -1
  143. package/src/view/client-dist/app.js +18 -18
  144. package/docs/README.md +0 -120
  145. package/docs/adapters/README.md +0 -60
  146. package/docs/adapters/authoring.md +0 -179
  147. package/docs/adapters/coding-agent-skills-plugins.md +0 -324
  148. package/docs/adapters/collection.md +0 -128
  149. package/docs/adapters/contract.md +0 -264
  150. package/docs/adapters/reference/agent-eval.md +0 -215
  151. package/docs/adapters/reference/agent-loop-apis.md +0 -69
  152. package/docs/adapters/reference/claude-code-otel-telemetry.md +0 -100
  153. package/docs/adapters/reference/eve-protocol.md +0 -127
  154. package/docs/adapters/reference/otel-genai.md +0 -107
  155. package/docs/adapters/reference/otel-instrumentation.md +0 -65
  156. package/docs/adapters/targets.md +0 -99
  157. package/docs/architecture.md +0 -140
  158. package/docs/assertions.md +0 -387
  159. package/docs/capabilities-by-construction.md +0 -48
  160. package/docs/cli.md +0 -173
  161. package/docs/concepts.md +0 -106
  162. package/docs/e2e-ci.md +0 -332
  163. package/docs/eval-authoring.md +0 -319
  164. package/docs/experiments.md +0 -157
  165. package/docs/getting-started.md +0 -224
  166. package/docs/multi-agent.md +0 -145
  167. package/docs/observability.md +0 -337
  168. package/docs/origin-integration.md +0 -195
  169. package/docs/references.md +0 -35
  170. package/docs/reports.md +0 -551
  171. package/docs/results-format.md +0 -228
  172. package/docs/results-lib.md +0 -191
  173. package/docs/runner.md +0 -104
  174. package/docs/sandbox.md +0 -276
  175. package/docs/scoring.md +0 -119
  176. package/docs/source-map.md +0 -126
  177. package/docs/tier-sync.md +0 -193
  178. package/docs/view.md +0 -194
  179. package/docs/vision.md +0 -88
package/src/runner/run.ts CHANGED
@@ -2,46 +2,60 @@
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";
8
9
  import { cacheKey, computeFingerprint } from "./fingerprint.ts";
9
10
  import { OtelReceiverPool } from "../o11y/otlp/turn-otel.ts";
10
11
  import { runAttemptEffect } from "./attempt.ts";
11
- import { runReporter, emitReporterEvent, summarize } from "./report.ts";
12
- import type { Agent, EvalResult, JudgeConfig, RunShape, RunSummary } from "../types.ts";
13
- import type { Attempt, RunOptions } from "./types.ts";
12
+ import { runReporter, emitReporterEvent, scopeReporter, summarize } from "./report.ts";
13
+ import type { Agent, EvalResult, JudgeConfig, Reporter, RunShape, RunSummary } from "../types.ts";
14
+ import type { AgentRun, 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);
31
- }
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
- }
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);
38
53
  }
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))) {
@@ -88,7 +102,11 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
88
102
  for (let i = 0; i < run.runs; i++) {
89
103
  for (const evalDef of evals) {
90
104
  if (run.experimentId && priorRunKeys.has(`${run.experimentId}|${evalDef.id}`)) continue;
91
- const key = `${run.agent.name}|${run.model ?? ""}|${evalDef.id}`;
105
+ // key 标识「同一个运行配置下的同一条 eval」,earlyExit 的跳过/abort 只应作用于
106
+ // 同 key 的重试轮。experimentId 必须进 key:两个实验可以同 agent 同 model、只差
107
+ // flags(feature A/B 正是这种形状),漏掉它会让先过的实验把其它实验的同名 eval
108
+ // 整个跳掉——花了钱还丢结果。
109
+ const key = `${run.experimentId ?? ""}|${run.agent.name}|${run.model ?? ""}|${evalDef.id}`;
92
110
  attempts.push({
93
111
  evalDef,
94
112
  run,
@@ -100,9 +118,39 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
100
118
  }
101
119
  }
102
120
 
121
+ // 预检 judge:验证 API key + 端点可达,避免跑完 agent 才发现 judge 不通。
122
+ // 放在 attempts 展开之后,fail fast 只对会真正触发 judge 的运行生效
123
+ // (目标收集逻辑见 judgeProbeTargets;全部结果携入、attempts 为空时也自然跳过)。
124
+ {
125
+ const uniqueEvals = [...new Map(attempts.map((a) => [a.evalDef.id, a.evalDef])).values()];
126
+ const sources = await Promise.all(uniqueEvals.map((e) => readSource(e.sourcePath)));
127
+ const toProbe = judgeProbeTargets(
128
+ uniqueEvals.map((e, i) => ({ source: sources[i] ?? "", judge: e.judge })),
129
+ opts.config.judge,
130
+ );
131
+ if (toProbe.length > 0) {
132
+ process.stderr.write(t("runner.judgePrecheck"));
133
+ for (const jc of toProbe) {
134
+ const err = await probeJudge(jc, opts.signal);
135
+ if (err) throw new Error(err);
136
+ }
137
+ }
138
+ }
139
+
103
140
  if (carriedResults.length > 0) {
104
141
  const retryCount = new Set(attempts.map((a) => `${a.run.experimentId ?? ""}|${a.evalDef.id}`)).size;
105
142
  process.stderr.write(t("runner.resumeCarry", { carried: carriedResults.length, retry: retryCount }));
143
+ // 按 experiment 分组列出被复用(跳过)的 eval:不列清单的话,用户只看到数量,
144
+ // 无法核对「跳过的是不是我以为已经过了的那些」。同一 key 多个 run 去重。
145
+ const carriedByExperiment = new Map<string, Set<string>>();
146
+ for (const r of carriedResults) {
147
+ const ids = carriedByExperiment.get(r.experimentId!) ?? new Set<string>();
148
+ ids.add(r.id);
149
+ carriedByExperiment.set(r.experimentId!, ids);
150
+ }
151
+ for (const [experiment, ids] of [...carriedByExperiment].sort(([a], [b]) => a.localeCompare(b))) {
152
+ process.stderr.write(t("runner.resumeCarryDetail", { experiment, evals: [...ids].sort().join(", ") }));
153
+ }
106
154
  }
107
155
 
108
156
  // onRunStart 报「本次实际要跑的 eval」(过滤 + 去重),不是发现到的全部 —— 否则计数误导。
@@ -115,11 +163,37 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
115
163
  totalRuns: attempts.length,
116
164
  maxConcurrency: opts.maxConcurrency,
117
165
  };
118
- for (const r of opts.reporters) {
166
+ // eval 级 reporters:实例只观测引用它的 eval( scopeReporter 过滤转发)
167
+ // 已经挂在全局 reporters 里的同一实例不重复挂;同一实例被多个 eval 引用时合并观测集
168
+ // (共享一个目的地,如同一个 Braintrust 实验)。本次没有任何被观测 eval 要跑时整个跳过。
169
+ const scopedSets = new Map<Reporter, Set<string>>();
170
+ for (const e of opts.evals) {
171
+ for (const r of e.reporters ?? []) {
172
+ if (opts.reporters.includes(r)) continue;
173
+ let ids = scopedSets.get(r);
174
+ if (!ids) scopedSets.set(r, (ids = new Set()));
175
+ ids.add(e.id);
176
+ }
177
+ }
178
+ const reporters: Reporter[] = [...opts.reporters];
179
+ for (const [r, ids] of scopedSets) {
180
+ const scopedRuns = attempts.filter((a) => ids.has(a.evalDef.id)).length;
181
+ if (scopedRuns === 0) continue;
182
+ reporters.push(
183
+ scopeReporter(r, ids, {
184
+ evals: [...ids].filter((id) => runningIds.has(id)).length,
185
+ configs: opts.agentRuns.length,
186
+ totalRuns: scopedRuns,
187
+ maxConcurrency: opts.maxConcurrency,
188
+ }),
189
+ );
190
+ }
191
+
192
+ for (const r of reporters) {
119
193
  // reporter 只是结果消费方:单个 reporter 抛错记 diagnostic,不能让整次调度崩(P2)。
120
194
  await runReporter("onRunStart", () => r.onRunStart?.(runningEvals, firstAgent as Agent, shape));
121
195
  }
122
- await emitReporterEvent(opts.reporters, {
196
+ await emitReporterEvent(reporters, {
123
197
  type: "run:start",
124
198
  evals: runningEvals,
125
199
  agent: firstAgent as Agent,
@@ -165,6 +239,18 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
165
239
  // 未显式指定时跟 maxConcurrency 走——各 backend 的推荐值已在 cli 层写进 maxConcurrency 默认值。
166
240
  const sandboxSem = Effect.runSync(Effect.makeSemaphore(opts.maxConcurrency));
167
241
 
242
+ // 两级并发闸:全局(opts.maxConcurrency)+ 实验级(AgentRun.maxConcurrency,可选)。
243
+ // 实验级信号量让「有共享状态、必须串行」的实验(如跨 eval 累积记忆,maxConcurrency: 1)
244
+ // 只在自己内部排队,同批其它实验照常并发——旧行为是 CLI 取所有选中实验的最小值钳全局,
245
+ // 一个串行实验会把整批基线拖成串行。等于全局上限的实验级值不建闸(与全局闸重复)。
246
+ const globalSem = Effect.runSync(Effect.makeSemaphore(opts.maxConcurrency));
247
+ const runSems = new Map<AgentRun, Effect.Semaphore>();
248
+ for (const run of opts.agentRuns) {
249
+ if (run.maxConcurrency !== undefined && run.maxConcurrency < opts.maxConcurrency) {
250
+ runSems.set(run, Effect.runSync(Effect.makeSemaphore(Math.max(1, run.maxConcurrency))));
251
+ }
252
+ }
253
+
168
254
  // 非沙箱 tracing agent 的共享 OTLP 接收池:被测应用是长驻进程,端点不能随
169
255
  // attempt 换 —— receiver 粒度跟被测进程走(每 agent 一个,整个 run 复用),run 结束回收。
170
256
  if (!opts.otelPool) {
@@ -180,8 +266,11 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
180
266
  }
181
267
  }
182
268
 
183
- // 有界并发调度:Effect.forEach({ concurrency }) 取代手写 queue / inFlight / Promise.race。
184
- // 每个 attempt 跑在自己的 fiber;runAttemptEffect 只把「执行错误」收进 EvalResult.error(不 fail),
269
+ // 有界并发调度:forEach 本身 unbounded(每个 attempt 立刻有自己的 fiber),真正的
270
+ // 并发上限由上面两级信号量把守——执行体先过实验级闸(若有)再占全局 permit 才开跑。
271
+ // 获取定序恒为 runSem → globalSem,无环等待;实验级闸的持有者在等全局 permit 时
272
+ // 不占别的实验的并发位(并发位就是 globalSem 的 permit,不再是 forEach 的 fiber 槽)。
273
+ // runAttemptEffect 只把「执行错误」收进 EvalResult.error(不 fail),
185
274
  // 但中断(Ctrl+C / kill)照常向上传播 —— 所以一条挂掉不会中断其它 attempt,而中断能停掉全部。
186
275
  //
187
276
  // signal:把 opts.signal 喂给 run → abort 触发根 fiber 中断 → forEach 中断所有子 fiber
@@ -196,14 +285,15 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
196
285
  const exit = await Effect.runPromiseExit(
197
286
  Effect.forEach(
198
287
  attempts,
199
- (a) =>
200
- Effect.gen(function* () {
288
+ (a) => {
289
+ const body = Effect.gen(function* () {
201
290
  // 早停:同 key 已通过,或已 errored(重跑只会重复同一个框架错误)且开了 earlyExit
202
- // → 跳过未启动的 attempt
291
+ // → 跳过未启动的 attempt。检查必须在拿到 permit 之后(fiber 是 unbounded 一次性
292
+ // 全建的,建时结果还没出来);跳过路径短暂占一个 permit,可忽略。
203
293
  if (a.run.earlyExit && (passedKeys.has(a.key) || erroredKeys.has(a.key))) {
204
294
  yield* reportMutex.withPermits(1)(
205
295
  Effect.promise(() =>
206
- emitReporterEvent(opts.reporters, {
296
+ emitReporterEvent(reporters, {
207
297
  type: "run:earlyExit",
208
298
  evalId: a.evalDef.id,
209
299
  experimentId: a.run.experimentId,
@@ -226,7 +316,7 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
226
316
  budgetReported.add(budgetKey);
227
317
  yield* reportMutex.withPermits(1)(
228
318
  Effect.promise(() =>
229
- emitReporterEvent(opts.reporters, { type: "run:budgetExceeded", budget, spent: s.spent }),
319
+ emitReporterEvent(reporters, { type: "run:budgetExceeded", budget, spent: s.spent }),
230
320
  ),
231
321
  );
232
322
  }
@@ -261,7 +351,7 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
261
351
 
262
352
  yield* reportMutex.withPermits(1)(
263
353
  Effect.promise(() =>
264
- emitReporterEvent(opts.reporters, {
354
+ emitReporterEvent(reporters, {
265
355
  type: "eval:start",
266
356
  eval: { id: a.evalDef.id },
267
357
  agent: a.run.agent,
@@ -310,7 +400,7 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
310
400
  // 停掉后续 attempt(P2)。
311
401
  Effect.promise(() =>
312
402
  Promise.all(
313
- opts.reporters.map((r) =>
403
+ reporters.map((r) =>
314
404
  runReporter("onEvalComplete", () => r.onEvalComplete?.(result)),
315
405
  ),
316
406
  ),
@@ -320,10 +410,14 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
320
410
  // attempt 各自触发的 eval:complete 会绕开 permit=1 直接并发跑,和文档承诺的
321
411
  // 「报告回调串行化」不一致。
322
412
  yield* reportMutex.withPermits(1)(
323
- Effect.promise(() => emitReporterEvent(opts.reporters, { type: "eval:complete", result })),
413
+ Effect.promise(() => emitReporterEvent(reporters, { type: "eval:complete", result })),
324
414
  );
325
- }),
326
- { concurrency: opts.maxConcurrency, discard: true },
415
+ });
416
+ const gated = globalSem.withPermits(1)(body);
417
+ const runSem = runSems.get(a.run);
418
+ return runSem ? runSem.withPermits(1)(gated) : gated;
419
+ },
420
+ { concurrency: "unbounded", discard: true },
327
421
  ).pipe(
328
422
  // 中断(用户 Ctrl+C):finalizer 已在中断过程中跑完(容器已停),这里只是把它咽下,
329
423
  // 好让流程走到 summarize / onRunComplete,用已完成的 results 出一份部分汇总,而不是抛栈。
@@ -359,10 +453,10 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
359
453
  );
360
454
 
361
455
  const summary = summarize(allResults, firstAgent?.name ?? "", startedAt, Date.now() - t0, opts.config.name);
362
- await emitReporterEvent(opts.reporters, { type: "run:summary", summary });
363
- for (const r of opts.reporters) {
456
+ await emitReporterEvent(reporters, { type: "run:summary", summary });
457
+ for (const r of reporters) {
364
458
  await runReporter("onRunComplete", () => r.onRunComplete?.(summary));
365
459
  }
366
- await emitReporterEvent(opts.reporters, { type: "run:saved", summary });
460
+ await emitReporterEvent(reporters, { type: "run:saved", summary });
367
461
  return summary;
368
462
  }
@@ -97,7 +97,8 @@ export interface RunShape {
97
97
  configs: number;
98
98
  /** 总 attempt 数(evals × configs × runs);逐行输出与汇总计数都按它。 */
99
99
  totalRuns: number;
100
- /** 本次运行实际生效的并发数(flag/env/experiment/config/sandbox 默认值解析后的结果)。 */
100
+ /** 本次运行实际生效的全局并发数(flag/env/config/sandbox 默认值解析后的结果);
101
+ * 实验级 maxConcurrency 只在该实验内部限流,不改这个全局值。 */
101
102
  maxConcurrency: number;
102
103
  }
103
104
 
@@ -122,12 +123,17 @@ export type ReporterEvent =
122
123
  export interface EvalDef {
123
124
  /** 路径推导,定义里禁止手写。 */
124
125
  id?: string;
126
+ /** 一句话描述,展示在 `niceeval list` 和 view 里;纯说明,不影响调度或打分。 */
125
127
  description?: string;
126
- agent?: string;
128
+ /** 标签,供 CLI `--tag` 过滤和 view 分类;与 id 前缀过滤是两套独立的筛选维度。 */
127
129
  tags?: string[];
130
+ /** 覆盖项目级 Config.judge,只对这一个 eval 生效(如换个更贵的评审模型)。 */
128
131
  judge?: JudgeConfig;
132
+ /** 覆盖 / 追加项目级 Config.reporters,只对这一个 eval 生效。 */
129
133
  reporters?: Reporter[];
134
+ /** 覆盖项目级 / CLI 的单次 attempt 超时(毫秒),只对这一个 eval 生效。 */
130
135
  timeoutMs?: number;
136
+ /** 任意附加元数据,原样透传进 EvalResult,不参与调度或打分;供自定义 reporter 消费。 */
131
137
  metadata?: Record<string, unknown>;
132
138
  /**
133
139
  * eval 级预置:拿到沙箱(已上传 workspace + git 基线 + 装好依赖前)。
@@ -135,6 +141,7 @@ export interface EvalDef {
135
141
  * (如 `runCommand("apt-get", ["install", …], { root: true })`),跨后端语义一致。
136
142
  */
137
143
  setup?: (sandbox: Sandbox) => Promise<void | Cleanup> | void | Cleanup;
144
+ /** eval 主体:拿到 TestContext,驱动对话 / 沙箱操作并就地断言。 */
138
145
  test(t: TestContext): Promise<void> | void;
139
146
  }
140
147
 
@@ -148,20 +155,43 @@ export interface DiscoveredEval extends EvalDef {
148
155
  }
149
156
 
150
157
  export interface ExperimentDef {
158
+ /** 路径推导,定义里禁止手写(defineExperiment 会拒绝显式传入)。 */
151
159
  id?: string;
160
+ /** 一句话描述,展示在 view / CLI 里;纯说明,不影响调度或打分。 */
152
161
  description?: string;
162
+ /**
163
+ * 必填:这个实验跑哪个 agent(defineSandboxAgent / defineAgent 的产物)。运行配置的
164
+ * agent 归属完全由这里决定——EvalDef.agent 不参与(见其字段注释)。
165
+ */
153
166
  agent: Agent;
154
167
  /** 单个模型(agent 留空时实验决定);省略=用 agent 原生默认。跨模型对比写多个实验文件,别用数组。 */
155
168
  model?: string;
156
169
  /** 模型推理努力程度(如 "low"/"medium"/"high",取值由具体模型/adapter 决定);省略=用 agent 原生默认。经 ctx.reasoningEffort 透给 adapter 与 eval。 */
157
170
  reasoningEffort?: string;
171
+ /** 传给每次 attempt 的 flags,经 t.flags 暴露给 eval;与 CLI flag 合并(CLI 优先)。 */
158
172
  flags?: Record<string, unknown>;
173
+ /** 同一 eval 重复跑几次(结果各计一条 attempt);省略/CLI `--runs` 覆盖时默认 1。 */
159
174
  runs?: number;
175
+ /** 一次重复(runs > 1)里某次 attempt 失败后是否跳过剩余重复;省略默认 true(提前退出省钱)。 */
160
176
  earlyExit?: boolean;
177
+ /** 这个实验覆盖哪些 eval:"*" 全部、字符串数组按 id 前缀、或自定义谓词;省略等价于 "*"。 */
161
178
  evals?: "*" | string[] | ((id: string) => boolean);
179
+ /** 覆盖项目级 / CLI 的单次 attempt 超时(毫秒),只对这个实验生效。 */
162
180
  timeoutMs?: number;
181
+ /** 覆盖项目级 Config.sandbox,只对这个实验生效。 */
163
182
  sandbox?: SandboxOption;
183
+ /**
184
+ * 本实验的花费上限(USD)。调度器按「已花 + 在飞预估」的护栏口径逼近上限时限流,
185
+ * 累计花费到顶后跳过这个实验剩下未起飞的 attempt 并上报一次 `run:budgetExceeded`
186
+ * (已在飞的 attempt 仍会跑完)。
187
+ */
164
188
  budget?: number;
189
+ /**
190
+ * 本实验自己的并发上限:调度器只对这个实验的 attempt 限流,同批其它实验不受影响,
191
+ * 仍按全局并发(CLI / env / config / 沙箱默认)跑。用于串行化有共享状态的实验
192
+ * (如跨 eval 累积记忆:`maxConcurrency: 1` 保证 attempt 按 eval 顺序一个个跑),
193
+ * 或给撞后端限额的实验单独降速。
194
+ */
165
195
  maxConcurrency?: number;
166
196
  }
167
197
 
@@ -176,11 +206,17 @@ export interface Config {
176
206
  * 可传字符串,或按 locale 提供多语言(如 `{ en: "...", "zh-CN": "..." }`),随 view 语言切换。
177
207
  */
178
208
  name?: LocalizedText;
209
+ /** 项目级默认沙箱后端(docker / vercel / e2b / custom);experiment / CLI flag 可覆盖。 */
179
210
  sandbox?: SandboxOption;
211
+ /** 上传进沙箱的工作区根目录,省略则用项目根;eval 的 sandbox 视图从这里起步。 */
180
212
  workspace?: string;
213
+ /** 项目级默认 judge 配置(model / baseUrl / apiKeyEnv);EvalDef.judge 可按 eval 覆盖。 */
181
214
  judge?: JudgeConfig;
215
+ /** 项目级默认 reporter 列表(如落盘 / 上传结果);EvalDef.reporters 会与它合并。 */
182
216
  reporters?: Reporter[];
217
+ /** 项目级默认并发上限;CLI flag / env / experiment 的同名设置优先级更高。 */
183
218
  maxConcurrency?: number;
219
+ /** 项目级默认单次 attempt 超时(毫秒);CLI flag / experiment / EvalDef 的同名设置优先级更高。 */
184
220
  timeoutMs?: number;
185
221
  /**
186
222
  * OTLP 接收配置,niceeval 项目内唯一入口(不读 NICEEVAL_OTLP_* 环境变量)。
@@ -203,14 +239,28 @@ export interface Config {
203
239
 
204
240
  /** 每百万 token 的美元单价;省略的桶退回 `inputPerMTok`(cache token 本质也是 input)。 */
205
241
  export interface PriceOverride {
242
+ /** 普通输入 token 单价。 */
206
243
  inputPerMTok: number;
244
+ /** 输出 token 单价。 */
207
245
  outputPerMTok: number;
246
+ /** cache 命中(读)token 单价,省略则退回 inputPerMTok。 */
208
247
  cacheReadPerMTok?: number;
248
+ /** cache 写入 token 单价,省略则退回 inputPerMTok。 */
209
249
  cacheWritePerMTok?: number;
210
250
  }
211
251
 
212
252
  // ───────────────────────── 调度编排 ─────────────────────────
213
253
 
254
+ /**
255
+ * 进度行 / 日志里标识一个 run 配置的短名。有 experiment 时用其 basename(唯一,
256
+ * 能区分同 agent 同 model 的实验变体,如 xxx 与 xxx--agents-md;与汇总表口径一致);
257
+ * 无 experiment 时退回 agent/model。live display 以它作行聚合 key,两处必须同源。
258
+ */
259
+ export function runWho(run: Pick<AgentRun, "agent" | "model" | "experimentId">): string {
260
+ if (run.experimentId) return run.experimentId.split("/").pop()!;
261
+ return run.model ? `${run.agent.name}/${run.model}` : run.agent.name;
262
+ }
263
+
214
264
  /** 一个 (agent, model, flags) 的运行配置 —— 由 CLI / 实验展开。 */
215
265
  export interface AgentRun {
216
266
  agent: Agent;
@@ -225,6 +275,9 @@ export interface AgentRun {
225
275
  evalFilter: (id: string) => boolean;
226
276
  experimentId?: string;
227
277
  strict?: boolean;
278
+ /** 本配置自己的并发上限(来自 ExperimentDef.maxConcurrency):调度器为它单建信号量,
279
+ * attempt 先过这道闸再占全局并发位;省略则只受全局并发约束。 */
280
+ maxConcurrency?: number;
228
281
  }
229
282
 
230
283
  export interface RunOptions {
@@ -93,7 +93,9 @@ export type SandboxSpec = DockerSandboxSpec | VercelSandboxSpec | E2BSandboxSpec
93
93
  export type SandboxOption = SandboxSpec;
94
94
 
95
95
  export interface CommandOptions {
96
+ /** 追加/覆盖本命令的环境变量(与沙箱默认环境叠加,不清空默认值;各后端会保留自己固定的 `PATH` 等变量,不保证能被这里覆盖)。 */
96
97
  env?: Record<string, string>;
98
+ /** 本命令的工作目录;省略时落到 `Sandbox.workdir`。相对路径按 workdir 解析,绝对路径原样使用。 */
97
99
  cwd?: string;
98
100
  /**
99
101
  * 把本命令的输出也送进沙箱的「原生日志流」(于是 `docker logs` / Docker UI 的 Logs
@@ -115,20 +117,36 @@ export interface CommandOptions {
115
117
  }
116
118
 
117
119
  export interface Sandbox {
120
+ /** 沙箱内项目/工作区根目录的绝对路径(agent 命令的默认 cwd,也是 git baseline 提交的位置)。各方法的相对路径都以此为基准解析,省略 `cwd`/`targetDir` 时也落到这里。 */
118
121
  readonly workdir: string;
122
+ /**
123
+ * 执行单个命令,`args` 作为独立 argv 传递、不经 shell 解释(无 `&&`、管道、通配符展开)。
124
+ * 只想跑一个可执行文件、参数来自外部输入、担心注入时优先用它。
125
+ */
119
126
  runCommand(cmd: string, args?: string[], opts?: CommandOptions): Promise<CommandResult>;
127
+ /**
128
+ * 执行一整段脚本,经 shell(bash)解释,支持 `&&`、管道、`$()`、重定向等。
129
+ * 需要拼多条命令或做条件判断时用它。
130
+ */
120
131
  runShell(script: string, opts?: CommandOptions): Promise<CommandResult>;
132
+ /** 读取沙箱内文件的文本内容(UTF-8)。文件不存在时抛错,不返回空字符串——需要容错请自行 `.catch()`。 */
121
133
  readFile(path: string): Promise<string>;
134
+ /** 检查沙箱内路径是否存在。跨后端语义不完全一致:仅保证对普通文件可靠,对目录路径的行为不同后端不保证一致。 */
122
135
  fileExists(path: string): Promise<boolean>;
123
136
  /**
124
137
  * 一次 shell 往返读全部源码文件(按扩展名收、按目录/文件名忽略)。
125
138
  * 取代每个 eval 目录里手写的 find + 逐文件 readFile。
126
139
  */
127
140
  readSourceFiles(opts?: ReadSourceFilesOptions): Promise<SourceFiles>;
141
+ /** 写入若干文本文件(内容已在内存里的字符串);是 `uploadFiles` 的文本特化,省略 `targetDir` 落到 workdir。 */
128
142
  writeFiles(files: Record<string, string>, targetDir?: string): Promise<void>;
143
+ /** 批量写入若干文件,内容可以是文本或二进制 Buffer;省略 `targetDir` 落到 workdir。 */
129
144
  uploadFiles(files: SandboxFile[], targetDir?: string): Promise<void>;
145
+ /** 把本地磁盘上的一个目录整体上传进沙箱(递归读取本地文件后按 `uploadFiles` 写入);`opts.ignore` 是排除规则,省略 `targetDir` 落到 workdir。 */
130
146
  uploadDirectory(localDir: string, targetDir?: string, opts?: { ignore?: string[] }): Promise<void>;
147
+ /** 销毁沙箱占用的计算资源(容器/microVM)。调用后沙箱不可再用;是否可安全重复调用因后端而异,不要依赖这一点。 */
131
148
  stop(): Promise<void>;
149
+ /** 本沙箱的稳定标识(各后端原生 ID,如 Docker 容器 ID 前缀);用于跨调用关联同一沙箱的会话状态,也用于日志展示。 */
132
150
  readonly sandboxId: string;
133
151
  /**
134
152
  * 本地 OTLP 接收器的目标 host。
@@ -9,7 +9,12 @@ export interface ValueAssertion {
9
9
  readonly severity: Severity;
10
10
  readonly threshold?: number;
11
11
  score(value: unknown): number | Promise<number>;
12
+ /** 转成硬门槛断言:未达阈值(省略 threshold 则按 score > 0 判定)整条 eval 判为 failed。返回新实例,不改原对象。 */
12
13
  gate(threshold?: number): ValueAssertion;
14
+ /**
15
+ * 转成软阈值断言:未达 threshold 时该条记为 failed,但默认不拖累整条 eval 的 outcome;
16
+ * `--strict` 运行下,软阈值失败也会把整条 eval 的 outcome 计为 failed。返回新实例,不改原对象。
17
+ */
13
18
  atLeast(threshold: number): ValueAssertion;
14
19
  }
15
20
 
@@ -2,6 +2,7 @@
2
2
  // 各域的类型住在各自目录的 types.ts(o11y / sandbox / agents / scoring / context / runner),
3
3
  // src/types.ts 是聚合 facade —— 模块代码统一从那里 import,不必记住每个类型的家。
4
4
 
5
+ /** JSON 可表达的任意值(递归定义),用于事件流 / 工具输入输出等跨进程/跨语言传递的数据。 */
5
6
  export type JsonValue =
6
7
  | string
7
8
  | number
@@ -10,6 +11,7 @@ export type JsonValue =
10
11
  | JsonValue[]
11
12
  | { [key: string]: JsonValue };
12
13
 
14
+ /** 断言的严重级:"gate" 失败必判整轮 failed;"soft" 默认只记录不拦截,仅在 `--strict` 模式或显式设阈值未达标时才计入失败。 */
13
15
  export type Severity = "gate" | "soft";
14
16
 
15
17
  /**
@@ -28,6 +30,7 @@ export interface SourceArtifact {
28
30
  content: string;
29
31
  }
30
32
 
33
+ /** 通用清理闭包(setup 返回值 / teardown 的形状),异步同步皆可,统一在 finally 里执行。 */
31
34
  export type Cleanup = () => Promise<void> | void;
32
35
 
33
36
  /**
package/src/util.test.ts CHANGED
@@ -1,5 +1,30 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { formatThrown } from "./util.ts";
2
+ import { formatThrown, upsertManagedBlock } from "./util.ts";
3
+
4
+ describe("upsertManagedBlock", () => {
5
+ const begin = "<!-- BEGIN:x -->";
6
+ const end = "<!-- END:x -->";
7
+
8
+ it("appends the block to existing content, separated by a blank line", () => {
9
+ const out = upsertManagedBlock("# My project\n", begin, end, "rules");
10
+ expect(out).toBe(`# My project\n\n${begin}\nrules\n${end}\n`);
11
+ });
12
+
13
+ it("creates just the block when the file is empty", () => {
14
+ expect(upsertManagedBlock("", begin, end, "rules")).toBe(`${begin}\nrules\n${end}\n`);
15
+ });
16
+
17
+ it("replaces only the content between existing markers, preserving what surrounds them", () => {
18
+ const before = `above\n\n${begin}\nold rules\n${end}\n\nbelow\n`;
19
+ const out = upsertManagedBlock(before, begin, end, "new rules");
20
+ expect(out).toBe(`above\n\n${begin}\nnew rules\n${end}\n\nbelow\n`);
21
+ });
22
+
23
+ it("is idempotent for the same content", () => {
24
+ const once = upsertManagedBlock("# hi\n", begin, end, "rules");
25
+ expect(upsertManagedBlock(once, begin, end, "rules")).toBe(once);
26
+ });
27
+ });
3
28
 
4
29
  describe("formatThrown", () => {
5
30
  it("uses the stack trace when available, so the report can locate the throw site", () => {
package/src/util.ts CHANGED
@@ -37,6 +37,22 @@ export function pad4(n: number): string {
37
37
  return String(n).padStart(4, "0");
38
38
  }
39
39
 
40
+ /**
41
+ * 在文本里维护一个带 BEGIN/END 标记的托管区块(AGENTS.md 的 niceeval 区块用)。
42
+ * 标记已存在 → 只替换两个标记之间的内容(升级时刷新指引,区块外的用户内容不动);
43
+ * 不存在 → 追加到末尾(与已有内容之间空一行)。
44
+ */
45
+ export function upsertManagedBlock(source: string, begin: string, end: string, content: string): string {
46
+ const block = `${begin}\n${content}\n${end}`;
47
+ const beginIdx = source.indexOf(begin);
48
+ const endIdx = source.indexOf(end);
49
+ if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
50
+ return source.slice(0, beginIdx) + block + source.slice(endIdx + end.length);
51
+ }
52
+ if (source.trim() === "") return `${block}\n`;
53
+ return `${source.replace(/\n*$/, "")}\n\n${block}\n`;
54
+ }
55
+
40
56
  /** 把任意值安全地转成简短字符串(报告 / 日志用)。 */
41
57
  export function brief(value: unknown, max = 200): string {
42
58
  let s: string;
@@ -9,7 +9,7 @@ import { Metric } from "./components/primitives.tsx";
9
9
  import { GroupSelector } from "./components/GroupSelector.tsx";
10
10
  import { CostScoreChart } from "./components/CostScoreChart.tsx";
11
11
  import { ExperimentTable } from "./components/ExperimentTable.tsx";
12
- import { CopyAllErrors } from "./components/CopyControls.tsx";
12
+ import { CopyFixPrompt } from "./components/CopyControls.tsx";
13
13
  import { AttemptModal } from "./components/AttemptModal.tsx";
14
14
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs.tsx";
15
15
  import { RunsView } from "./pages/RunsPage.tsx";
@@ -239,7 +239,7 @@ export function App({ data }: { data: ViewData }) {
239
239
  value={query}
240
240
  onChange={(e) => setQuery(e.target.value)}
241
241
  />
242
- <CopyAllErrors rows={filtered} t={t} />
242
+ <CopyFixPrompt rows={filtered} t={t} />
243
243
  </div>
244
244
  </div>
245
245
  {rows.length ? (
@@ -5,6 +5,7 @@ import { artifactUrl } from "../lib/artifact-url.ts";
5
5
  import { asEvents, asSources } from "../lib/guards.ts";
6
6
  import { outcomeClass, outcomeLabel } from "../lib/outcome.ts";
7
7
  import { CodeView, NoSourceBody } from "./CodeView.tsx";
8
+ import { CopyAttemptPrompt } from "./CopyControls.tsx";
8
9
  import { LazyArtifact } from "./LazyArtifact.tsx";
9
10
  import { Dialog, DialogClose, DialogContent, DialogTitle } from "./ui/dialog.tsx";
10
11
  import { Badge } from "./ui/badge.tsx";
@@ -44,6 +45,7 @@ export function AttemptModal({ result, onClose, t }: { result: ViewResult; onClo
44
45
  </DialogTitle>
45
46
  {result.description ? <span className="truncate text-xs text-muted">{result.description}</span> : null}
46
47
  </div>
48
+ <CopyAttemptPrompt result={result} t={t} />
47
49
  <DialogClose
48
50
  aria-label={t("action.close")}
49
51
  className="grid h-7 w-7 shrink-0 place-items-center rounded-md border border-transparent text-sm text-muted transition-colors hover:border-line hover:bg-panel-2 hover:text-text"