@cursor/july 0.1.12 → 0.1.14

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 (162) hide show
  1. package/dist/bin/agent-serve.js +32 -9
  2. package/dist/channels/github/api.d.ts.map +1 -1
  3. package/dist/channels/github/api.js +11 -4
  4. package/dist/channels/github/cursor-account.d.ts +21 -1
  5. package/dist/channels/github/cursor-account.d.ts.map +1 -1
  6. package/dist/channels/github/cursor-account.js +29 -1
  7. package/dist/channels/github/github-channel.d.ts.map +1 -1
  8. package/dist/channels/github/github-channel.js +1 -1
  9. package/dist/channels/github/instrument.d.ts +20 -0
  10. package/dist/channels/github/instrument.d.ts.map +1 -0
  11. package/dist/channels/slack/api.d.ts +9 -0
  12. package/dist/channels/slack/api.d.ts.map +1 -1
  13. package/dist/channels/slack/api.js +40 -7
  14. package/dist/channels/slack/eval-directive.d.ts +3 -0
  15. package/dist/channels/slack/eval-directive.d.ts.map +1 -1
  16. package/dist/channels/slack/eval-directive.js +20 -1
  17. package/dist/docs/404.html +2 -2
  18. package/dist/docs/ab.html +3 -3
  19. package/dist/docs/assets/{app.Dh_JkJjI.js → app.CKFFLL4Z.js} +1 -1
  20. package/dist/docs/assets/chunks/@localSearchIndexroot.DGWkvXJc.js +1 -0
  21. package/dist/docs/assets/chunks/{VPLocalSearchBox.CptQNLkR.js → VPLocalSearchBox.BXk19JBN.js} +1 -1
  22. package/dist/docs/assets/chunks/{theme.DWflH-LQ.js → theme.BXSPBMyn.js} +2 -2
  23. package/dist/docs/assets/{evals.md.DAgEc_hL.js → evals.md.DYOjkRCX.js} +10 -4
  24. package/dist/docs/assets/{evals.md.DAgEc_hL.lean.js → evals.md.DYOjkRCX.lean.js} +1 -1
  25. package/dist/docs/assets/{reference_cli.md.B74NrVzb.js → reference_cli.md.DnYfr5V2.js} +4 -1
  26. package/dist/docs/assets/{reference_http-api.md.Dx_nmDG6.js → reference_http-api.md.BncLd3PZ.js} +2 -2
  27. package/dist/docs/assets/{reference_http-api.md.Dx_nmDG6.lean.js → reference_http-api.md.BncLd3PZ.lean.js} +1 -1
  28. package/dist/docs/building-with-agents.html +3 -3
  29. package/dist/docs/concepts.html +3 -3
  30. package/dist/docs/deployment.html +3 -3
  31. package/dist/docs/evals.html +13 -7
  32. package/dist/docs/example-agents/approval-buddy.html +3 -3
  33. package/dist/docs/example-agents/benny.html +3 -3
  34. package/dist/docs/example-agents/bugbot.html +3 -3
  35. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  36. package/dist/docs/example-agents/codeowners-review.html +3 -3
  37. package/dist/docs/example-agents/concierge.html +3 -3
  38. package/dist/docs/example-agents/fsd.html +3 -3
  39. package/dist/docs/example-agents/index.html +3 -3
  40. package/dist/docs/example-agents/knowledge-base.html +3 -3
  41. package/dist/docs/example-agents/oncall.html +3 -3
  42. package/dist/docs/example-agents/security-reviewer.html +3 -3
  43. package/dist/docs/example-agents/slack-agent.html +3 -3
  44. package/dist/docs/example-agents/weather-agent.html +3 -3
  45. package/dist/docs/guides/agent-to-agent.html +3 -3
  46. package/dist/docs/guides/cloud-runtime.html +3 -3
  47. package/dist/docs/guides/github.html +3 -3
  48. package/dist/docs/guides/human-in-the-loop.html +3 -3
  49. package/dist/docs/guides/mcp-oauth.html +3 -3
  50. package/dist/docs/guides/slack.html +3 -3
  51. package/dist/docs/guides/webhooks.html +3 -3
  52. package/dist/docs/hashmap.json +1 -1
  53. package/dist/docs/hillclimbing.html +3 -3
  54. package/dist/docs/index.html +3 -3
  55. package/dist/docs/quickstart.html +3 -3
  56. package/dist/docs/reference/agent-config.html +3 -3
  57. package/dist/docs/reference/channels.html +3 -3
  58. package/dist/docs/reference/cli.html +8 -5
  59. package/dist/docs/reference/connections.html +3 -3
  60. package/dist/docs/reference/hooks.html +3 -3
  61. package/dist/docs/reference/http-api.html +5 -5
  62. package/dist/docs/reference/instructions.html +3 -3
  63. package/dist/docs/reference/playground.html +3 -3
  64. package/dist/docs/reference/project-layout.html +3 -3
  65. package/dist/docs/reference/prompt.html +3 -3
  66. package/dist/docs/reference/schedules.html +3 -3
  67. package/dist/docs/reference/sessions.html +3 -3
  68. package/dist/docs/reference/skills.html +3 -3
  69. package/dist/docs/reference/subagents.html +3 -3
  70. package/dist/docs/reference/tools.html +3 -3
  71. package/dist/docs/scaffolding-agents.html +3 -3
  72. package/dist/docs/storage.html +3 -3
  73. package/dist/docs/troubleshooting.html +3 -3
  74. package/dist/evals.d.ts +41 -2
  75. package/dist/evals.d.ts.map +1 -1
  76. package/dist/evals.js +53 -16
  77. package/dist/internal/cli-ax.d.ts +4 -0
  78. package/dist/internal/cli-ax.d.ts.map +1 -1
  79. package/dist/internal/cli-ax.js +163 -33
  80. package/dist/internal/cli-deploy.d.ts +8 -0
  81. package/dist/internal/cli-deploy.d.ts.map +1 -1
  82. package/dist/internal/cli-deploy.js +92 -12
  83. package/dist/internal/cli-mcp-oauth.d.ts +14 -2
  84. package/dist/internal/cli-mcp-oauth.d.ts.map +1 -1
  85. package/dist/internal/cli-mcp-oauth.js +95 -2
  86. package/dist/internal/cursor/backend-client.d.ts +34 -0
  87. package/dist/internal/cursor/backend-client.d.ts.map +1 -1
  88. package/dist/internal/cursor/backend-client.js +73 -9
  89. package/dist/internal/cursor/credentials.d.ts +7 -1
  90. package/dist/internal/cursor/credentials.d.ts.map +1 -1
  91. package/dist/internal/cursor/credentials.js +9 -3
  92. package/dist/internal/cursor-account-mcp-auth.d.ts +55 -0
  93. package/dist/internal/cursor-account-mcp-auth.d.ts.map +1 -0
  94. package/dist/internal/cursor-account-mcp-auth.js +175 -0
  95. package/dist/internal/deploy-client.d.ts +6 -0
  96. package/dist/internal/deploy-client.d.ts.map +1 -1
  97. package/dist/internal/deploy-client.js +13 -0
  98. package/dist/internal/eval-run-store.d.ts +15 -0
  99. package/dist/internal/eval-run-store.d.ts.map +1 -1
  100. package/dist/internal/eval-run-store.js +87 -8
  101. package/dist/internal/eval-runner.d.ts +12 -0
  102. package/dist/internal/eval-runner.d.ts.map +1 -1
  103. package/dist/internal/eval-runner.js +97 -22
  104. package/dist/internal/evals-client.d.ts +9 -1
  105. package/dist/internal/evals-client.d.ts.map +1 -1
  106. package/dist/internal/evals-client.js +43 -0
  107. package/dist/internal/http-channel.d.ts.map +1 -1
  108. package/dist/internal/http-channel.js +74 -7
  109. package/dist/internal/map-pool.d.ts +6 -1
  110. package/dist/internal/map-pool.d.ts.map +1 -1
  111. package/dist/internal/map-pool.js +8 -1
  112. package/dist/internal/mcp-host.d.ts +13 -0
  113. package/dist/internal/mcp-host.d.ts.map +1 -1
  114. package/dist/internal/mcp-host.js +68 -13
  115. package/dist/internal/run-client.d.ts +5 -1
  116. package/dist/internal/run-client.d.ts.map +1 -1
  117. package/dist/internal/run-client.js +26 -5
  118. package/dist/internal/server.d.ts.map +1 -1
  119. package/dist/internal/server.js +57 -4
  120. package/dist/internal/session-engine.d.ts +13 -0
  121. package/dist/internal/session-engine.d.ts.map +1 -1
  122. package/dist/internal/session-engine.js +49 -0
  123. package/dist/internal/storage-coordinator.d.ts +7 -0
  124. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  125. package/dist/internal/storage-coordinator.js +29 -4
  126. package/dist/playground/assets/index-CuXb4j4V.css +1 -0
  127. package/dist/playground/assets/{index-1dzluIcW.js → index-DeyYpOit.js} +46 -46
  128. package/dist/playground/index.html +2 -2
  129. package/dist/types.d.ts +10 -0
  130. package/dist/types.d.ts.map +1 -1
  131. package/docs/evals.md +46 -11
  132. package/docs/reference/cli.md +3 -0
  133. package/docs/reference/http-api.md +2 -1
  134. package/package.json +1 -1
  135. package/src/bin/agent-serve.ts +38 -8
  136. package/src/channels/github/api.ts +12 -1
  137. package/src/channels/github/cursor-account.ts +51 -1
  138. package/src/channels/github/github-channel.ts +4 -1
  139. package/src/channels/slack/api.ts +57 -8
  140. package/src/channels/slack/eval-directive.ts +24 -1
  141. package/src/evals.ts +104 -25
  142. package/src/internal/cli-ax.ts +192 -16
  143. package/src/internal/cli-deploy.ts +140 -13
  144. package/src/internal/cli-mcp-oauth.ts +122 -3
  145. package/src/internal/cursor/backend-client.ts +104 -1
  146. package/src/internal/cursor/credentials.ts +10 -2
  147. package/src/internal/cursor-account-mcp-auth.ts +241 -0
  148. package/src/internal/deploy-client.ts +16 -0
  149. package/src/internal/eval-run-store.ts +89 -6
  150. package/src/internal/eval-runner.ts +113 -18
  151. package/src/internal/evals-client.ts +81 -0
  152. package/src/internal/http-channel.ts +87 -6
  153. package/src/internal/map-pool.ts +8 -1
  154. package/src/internal/mcp-host.ts +74 -16
  155. package/src/internal/run-client.ts +32 -1
  156. package/src/internal/server.ts +69 -2
  157. package/src/internal/session-engine.ts +58 -0
  158. package/src/internal/storage-coordinator.ts +32 -6
  159. package/src/types.ts +10 -0
  160. package/dist/docs/assets/chunks/@localSearchIndexroot.Er_R50vY.js +0 -1
  161. package/dist/playground/assets/index-kZjeJs1v.css +0 -1
  162. /package/dist/docs/assets/{reference_cli.md.B74NrVzb.lean.js → reference_cli.md.DnYfr5V2.lean.js} +0 -0
@@ -331,10 +331,16 @@ export async function postMessage(args: {
331
331
  text: string;
332
332
  threadTs?: string;
333
333
  blocks?: unknown[];
334
- }): Promise<{ ts: string }> {
334
+ /**
335
+ * Skip `markdown_text` and post with plain `text` only (Deploy Buddy /
336
+ * host-report pattern). Prefer this for large structured bodies — Slack
337
+ * `markdown_text` hard-caps at 12k while `text` allows ~40k.
338
+ */
339
+ plainText?: boolean;
340
+ }): Promise<{ ts: string; channel: string }> {
335
341
  // Slack treats markdown_text as mutually exclusive with text and blocks
336
342
  // (error: markdown_text_conflict). Prefer markdown when we have no
337
- // blocks; with Block Kit, use text as body/fallback.
343
+ // blocks; with Block Kit or plainText, use text as body/fallback.
338
344
  const base: Record<string, unknown> = {
339
345
  channel: args.channel,
340
346
  unfurl_links: false,
@@ -344,16 +350,17 @@ export async function postMessage(args: {
344
350
  base.thread_ts = args.threadTs;
345
351
  }
346
352
  const hasBlocks = args.blocks !== undefined && args.blocks.length > 0;
353
+ const usePlainText = args.plainText === true || hasBlocks;
347
354
 
348
355
  let response: Record<string, unknown>;
349
- if (hasBlocks) {
356
+ if (usePlainText) {
350
357
  response = await callSlackApi({
351
358
  botToken: args.botToken,
352
359
  operation: "chat.postMessage",
353
360
  body: {
354
361
  ...base,
355
362
  text: args.text,
356
- blocks: args.blocks,
363
+ ...(hasBlocks ? { blocks: args.blocks } : {}),
357
364
  },
358
365
  });
359
366
  } else {
@@ -366,11 +373,14 @@ export async function postMessage(args: {
366
373
  },
367
374
  });
368
375
  // Older workspaces / scopes may not accept markdown_text — retry with text.
376
+ // Also fall back when markdown_text hits its 12k limit (msg_too_long) while
377
+ // plain text still allows up to ~40k.
369
378
  if (
370
379
  response.ok !== true &&
371
380
  (response.error === "markdown_text_conflict" ||
372
381
  response.error === "invalid_arguments" ||
373
382
  response.error === "invalid_blocks" ||
383
+ response.error === "msg_too_long" ||
374
384
  SLACK_UNSUPPORTED_ERRORS.has(String(response.error ?? "")))
375
385
  ) {
376
386
  response = await callSlackApi({
@@ -388,8 +398,43 @@ export async function postMessage(args: {
388
398
  `Slack chat.postMessage failed: ${String(response.error ?? "unknown_error")}`
389
399
  );
390
400
  }
401
+ // Slack requires thread_ts as a string. A numeric ts (JSON number) silently
402
+ // drops threading server-side — never coerce via Number.
391
403
  const ts = typeof response.ts === "string" ? response.ts : "";
392
- return { ts };
404
+ if (ts === "") {
405
+ throw new Error(
406
+ `Slack chat.postMessage ok but missing string ts (got ${typeof response.ts})`
407
+ );
408
+ }
409
+ // When we asked to thread, confirm Slack honored it — but only when the
410
+ // response includes a thread_ts. Some proxy / customize-username payloads
411
+ // omit `message.thread_ts` even for successful threaded posts; treating
412
+ // that as failure retries (or deletes) replies and leaves empty threads.
413
+ const requestedThreadTs = args.threadTs;
414
+ if (requestedThreadTs !== undefined && requestedThreadTs !== "") {
415
+ const message = response.message;
416
+ if (
417
+ typeof message === "object" &&
418
+ message !== null &&
419
+ !Array.isArray(message)
420
+ ) {
421
+ const applied =
422
+ typeof (message as { thread_ts?: unknown }).thread_ts === "string"
423
+ ? (message as { thread_ts: string }).thread_ts
424
+ : "";
425
+ if (applied !== "" && applied !== requestedThreadTs) {
426
+ throw new Error(
427
+ `Slack chat.postMessage ignored thread_ts (requested ${requestedThreadTs}, got ${applied})`
428
+ );
429
+ }
430
+ }
431
+ }
432
+ // Slack returns the resolved channel ID (C…) even when the request used a name.
433
+ const channel =
434
+ typeof response.channel === "string" && response.channel !== ""
435
+ ? response.channel
436
+ : args.channel;
437
+ return { ts, channel };
393
438
  }
394
439
 
395
440
  /** Replace an existing message (e.g. strip Approve/Deny after a decision). */
@@ -399,20 +444,22 @@ export async function updateMessage(args: {
399
444
  ts: string;
400
445
  text: string;
401
446
  blocks?: unknown[];
447
+ /** Skip `markdown_text` and update with plain `text` only. */
448
+ plainText?: boolean;
402
449
  }): Promise<SoftSlackResult> {
403
450
  const base: Record<string, unknown> = {
404
451
  channel: args.channel,
405
452
  ts: args.ts,
406
453
  };
407
- if (args.blocks !== undefined) {
408
- // Blocks force the legacy text path (same as postMessage).
454
+ if (args.blocks !== undefined || args.plainText === true) {
455
+ // Blocks / plainText force the legacy text path (same as postMessage).
409
456
  return callSlackApiSoft({
410
457
  botToken: args.botToken,
411
458
  operation: "chat.update",
412
459
  body: {
413
460
  ...base,
414
461
  text: args.text,
415
- blocks: args.blocks,
462
+ ...(args.blocks !== undefined ? { blocks: args.blocks } : {}),
416
463
  },
417
464
  });
418
465
  }
@@ -425,11 +472,13 @@ export async function updateMessage(args: {
425
472
  },
426
473
  });
427
474
  // Older workspaces / scopes may not accept markdown_text — retry with text.
475
+ // Also fall back when markdown_text hits its 12k limit (msg_too_long).
428
476
  if (
429
477
  !result.ok &&
430
478
  (result.error === "markdown_text_conflict" ||
431
479
  result.error === "invalid_arguments" ||
432
480
  result.error === "invalid_blocks" ||
481
+ result.error === "msg_too_long" ||
433
482
  SLACK_UNSUPPORTED_ERRORS.has(String(result.error ?? "")))
434
483
  ) {
435
484
  result = await callSlackApiSoft({
@@ -26,6 +26,7 @@ export type SlackEvalDirective =
26
26
  | { kind: "help" }
27
27
  | { kind: "list" }
28
28
  | { kind: "status" }
29
+ | { kind: "cancel"; runId: string }
29
30
  | { kind: "run"; filterIds?: string[]; tags?: string[] };
30
31
 
31
32
  function escapeRegExp(value: string): string {
@@ -89,6 +90,13 @@ export function parseSlackEvalDirective(
89
90
  if (rest.length === 1 && head === "status") {
90
91
  return { kind: "status" };
91
92
  }
93
+ if (head === "cancel") {
94
+ const runId = rest[1];
95
+ if (rest.length !== 2 || runId === undefined || runId.length === 0) {
96
+ return { kind: "help" };
97
+ }
98
+ return { kind: "cancel", runId };
99
+ }
92
100
 
93
101
  const filterIds: string[] = [];
94
102
  const tags: string[] = [];
@@ -152,6 +160,7 @@ export function formatSlackEvalHelp(): string {
152
160
  "• `eval --tag <tag>` — run evals with a tag (`-t` also works)",
153
161
  "• `eval list` — list available evals",
154
162
  "• `eval status` — show the active or latest eval run",
163
+ "• `eval cancel <evalId>` — cancel a running eval batch",
155
164
  "• `eval help` — this message",
156
165
  ].join("\n");
157
166
  }
@@ -291,6 +300,20 @@ export async function handleSlackEvalDirective(args: {
291
300
  return;
292
301
  }
293
302
 
303
+ if (directive.kind === "cancel") {
304
+ try {
305
+ const run = await evals.cancel(directive.runId);
306
+ await safePost(
307
+ binding,
308
+ `Cancelled Eval ID \`${run.runId}\`.\n${formatSlackEvalRunSummary(run)}`
309
+ );
310
+ } catch (error) {
311
+ const message = error instanceof Error ? error.message : String(error);
312
+ await safePost(binding, `Failed to cancel eval: ${message}`);
313
+ }
314
+ return;
315
+ }
316
+
294
317
  await safeTyping(binding, "Starting evals…");
295
318
  let run: EvalRunSnapshot;
296
319
  try {
@@ -323,7 +346,7 @@ export async function handleSlackEvalDirective(args: {
323
346
 
324
347
  await safePost(
325
348
  binding,
326
- `Started eval run \`${run.runId}\` (${run.summary.total} case(s)).`
349
+ `Started Eval ID \`${run.runId}\` (${run.summary.total} case(s)). Cancel with \`eval cancel ${run.runId}\`.`
327
350
  );
328
351
 
329
352
  while (run.status === "running") {
package/src/evals.ts CHANGED
@@ -48,7 +48,7 @@
48
48
 
49
49
  import { normalizePositiveInt } from "./internal/bounded-int.js";
50
50
  import { brandDefinition } from "./internal/brand.js";
51
- import type { SessionEvent } from "./types.js";
51
+ import type { AgentCloudOptions, SessionEvent } from "./types.js";
52
52
 
53
53
  export type Expectation =
54
54
  | { kind: "includes"; value: string | RegExp }
@@ -93,6 +93,12 @@ export interface EvalSendOptions {
93
93
  workspaceDir?: string;
94
94
  /** Files seeded into the session workspace before the first turn. */
95
95
  workspaceFiles?: Record<string, string>;
96
+ /**
97
+ * Per-session cloud options merged over the agent's static `cloud` config
98
+ * (create only). Use this to attach a pinned fixture repo for cloud evals
99
+ * without putting it on the agent's default `cloud.repos`.
100
+ */
101
+ cloud?: AgentCloudOptions;
96
102
  }
97
103
 
98
104
  export interface EvalTestContext {
@@ -103,6 +109,14 @@ export interface EvalTestContext {
103
109
  readonly sessionId: string | undefined;
104
110
  /** All events captured so far across turns. */
105
111
  readonly events: SessionEvent[];
112
+ /**
113
+ * 1-based index of this run when {@link EvalConfig.iterations} /
114
+ * {@link EvalDatapoint.iterations} expands a datapoint. Always `1` when
115
+ * iterations is unset or `1`.
116
+ */
117
+ readonly iteration: number;
118
+ /** Total iterations for this datapoint (default `1`). */
119
+ readonly iterations: number;
106
120
  /** Gate: run did not fail. */
107
121
  succeeded(): void;
108
122
  /** Gate: at least one completed call to `name`. */
@@ -117,6 +131,9 @@ export interface EvalTestContext {
117
131
  log(message: string): void;
118
132
  }
119
133
 
134
+ /** Hard ceiling for {@link EvalConfig.iterations} / case `iterations`. */
135
+ export const EVAL_MAX_ITERATIONS = 100;
136
+
120
137
  /** One runnable datapoint inside a multi-case {@link defineEval} file. */
121
138
  export interface EvalDatapoint {
122
139
  /**
@@ -128,6 +145,13 @@ export interface EvalDatapoint {
128
145
  description?: string;
129
146
  tags?: string[];
130
147
  timeoutMs?: number;
148
+ /**
149
+ * How many times to run this datapoint (default `1`, or the file-level
150
+ * {@link EvalConfig.iterations}). When greater than 1, discovery expands
151
+ * to `<fileId>/<id>/1` … `<fileId>/<id>/<n>` so each iteration is its own
152
+ * concurrent case.
153
+ */
154
+ iterations?: number;
131
155
  test: (t: EvalTestContext) => Promise<void>;
132
156
  }
133
157
 
@@ -139,6 +163,11 @@ export interface EvalConfig {
139
163
  description?: string;
140
164
  tags?: string[];
141
165
  timeoutMs?: number;
166
+ /**
167
+ * Default iterations for every datapoint in this file (default `1`).
168
+ * Case-level {@link EvalDatapoint.iterations} overrides this.
169
+ */
170
+ iterations?: number;
142
171
  /** Single-datapoint shorthand. Case id = file path under `evals/`. */
143
172
  test?: (t: EvalTestContext) => Promise<void>;
144
173
  /** Multiple datapoints in this file. Each id = `<fileId>/<case.id>`. */
@@ -150,6 +179,10 @@ export interface EvalCaseDefinition {
150
179
  description?: string;
151
180
  tags?: string[];
152
181
  timeoutMs?: number;
182
+ /** Total iterations configured for the authored datapoint. */
183
+ iterations?: number;
184
+ /** 1-based iteration index for this expanded runnable case. */
185
+ iteration?: number;
153
186
  test: (t: EvalTestContext) => Promise<void>;
154
187
  readonly __agentServe: "eval";
155
188
  }
@@ -161,6 +194,13 @@ export type EvalDefinition = EvalConfig & { readonly __agentServe: "eval" };
161
194
  * `evals/weather.eval.ts` → `weather`. With a single `test`, that is also
162
195
  * the case id. With `cases`, each datapoint is `<fileId>/<case.id>`.
163
196
  */
197
+ export function normalizeEvalIterations(value: unknown): number {
198
+ return normalizePositiveInt(value, {
199
+ label: "defineEval: iterations",
200
+ max: EVAL_MAX_ITERATIONS,
201
+ });
202
+ }
203
+
164
204
  export function defineEval(config: EvalConfig): EvalDefinition {
165
205
  const hasTest = typeof config.test === "function";
166
206
  const hasCases = config.cases !== undefined;
@@ -170,6 +210,9 @@ export function defineEval(config: EvalConfig): EvalDefinition {
170
210
  if (!hasTest && !hasCases) {
171
211
  throw new Error("defineEval requires test(t) or a non-empty cases array");
172
212
  }
213
+ if (config.iterations !== undefined) {
214
+ normalizeEvalIterations(config.iterations);
215
+ }
173
216
  if (hasCases) {
174
217
  if (!Array.isArray(config.cases) || config.cases.length === 0) {
175
218
  throw new Error("defineEval: cases must be a non-empty array");
@@ -188,6 +231,9 @@ export function defineEval(config: EvalConfig): EvalDefinition {
188
231
  `defineEval: cases[${JSON.stringify(datapoint.id)}] requires test(t)`
189
232
  );
190
233
  }
234
+ if (datapoint.iterations !== undefined) {
235
+ normalizeEvalIterations(datapoint.iterations);
236
+ }
191
237
  }
192
238
  }
193
239
  return brandDefinition("eval", config);
@@ -207,44 +253,75 @@ function validateDatapointId(id: unknown): asserts id is string {
207
253
  /**
208
254
  * Expand a file-level definition into runnable datapoints.
209
255
  * Exported for tests and tooling.
256
+ *
257
+ * When `iterations` > 1, each authored datapoint expands to
258
+ * `<fileId>/<case.id>/1` … `<fileId>/<case.id>/<n>` (single-test files
259
+ * expand to `<fileId>/1` … `<fileId>/<n>`).
210
260
  */
211
261
  export function expandEvalDefinition(
212
262
  fileId: string,
213
263
  definition: EvalDefinition
214
264
  ): Array<{ id: string; definition: EvalCaseDefinition }> {
215
265
  if (definition.cases !== undefined) {
216
- return definition.cases.map((datapoint) => {
266
+ return definition.cases.flatMap((datapoint) => {
217
267
  const description = datapoint.description ?? definition.description;
218
268
  const timeoutMs = datapoint.timeoutMs ?? definition.timeoutMs;
219
- return {
220
- id: `${fileId}/${datapoint.id}`,
221
- definition: brandDefinition("eval", {
222
- ...(description !== undefined ? { description } : {}),
223
- tags: datapoint.tags ?? definition.tags,
224
- ...(timeoutMs !== undefined ? { timeoutMs } : {}),
225
- test: datapoint.test,
226
- }),
227
- };
269
+ const iterations = normalizeEvalIterations(
270
+ datapoint.iterations ?? definition.iterations ?? 1
271
+ );
272
+ return expandIterations({
273
+ baseId: `${fileId}/${datapoint.id}`,
274
+ description,
275
+ tags: datapoint.tags ?? definition.tags,
276
+ timeoutMs,
277
+ iterations,
278
+ test: datapoint.test,
279
+ });
228
280
  });
229
281
  }
230
282
  if (typeof definition.test !== "function") {
231
283
  throw new Error(`defineEval(${fileId}): missing test(t)`);
232
284
  }
233
- return [
234
- {
235
- id: fileId,
285
+ const iterations = normalizeEvalIterations(definition.iterations ?? 1);
286
+ return expandIterations({
287
+ baseId: fileId,
288
+ description: definition.description,
289
+ tags: definition.tags,
290
+ timeoutMs: definition.timeoutMs,
291
+ iterations,
292
+ test: definition.test,
293
+ });
294
+ }
295
+
296
+ function expandIterations(args: {
297
+ baseId: string;
298
+ description?: string;
299
+ tags?: string[];
300
+ timeoutMs?: number;
301
+ iterations: number;
302
+ test: (t: EvalTestContext) => Promise<void>;
303
+ }): Array<{ id: string; definition: EvalCaseDefinition }> {
304
+ const out: Array<{ id: string; definition: EvalCaseDefinition }> = [];
305
+ for (let iteration = 1; iteration <= args.iterations; iteration++) {
306
+ const id =
307
+ args.iterations === 1 ? args.baseId : `${args.baseId}/${iteration}`;
308
+ const description =
309
+ args.iterations === 1 || args.description === undefined
310
+ ? args.description
311
+ : `${args.description} (iteration ${iteration}/${args.iterations})`;
312
+ out.push({
313
+ id,
236
314
  definition: brandDefinition("eval", {
237
- ...(definition.description !== undefined
238
- ? { description: definition.description }
239
- : {}),
240
- ...(definition.tags !== undefined ? { tags: definition.tags } : {}),
241
- ...(definition.timeoutMs !== undefined
242
- ? { timeoutMs: definition.timeoutMs }
243
- : {}),
244
- test: definition.test,
315
+ ...(description !== undefined ? { description } : {}),
316
+ ...(args.tags !== undefined ? { tags: args.tags } : {}),
317
+ ...(args.timeoutMs !== undefined ? { timeoutMs: args.timeoutMs } : {}),
318
+ iterations: args.iterations,
319
+ iteration,
320
+ test: args.test,
245
321
  }),
246
- },
247
- ];
322
+ });
323
+ }
324
+ return out;
248
325
  }
249
326
 
250
327
  /**
@@ -272,7 +349,7 @@ export const EVAL_DEFAULT_MAX_PLAYGROUND_RUNS = 20;
272
349
  export const EVAL_MAX_PLAYGROUND_RUNS = 500;
273
350
 
274
351
  export type EvalCaseRunStatus = "pending" | "running" | "done";
275
- export type EvalBatchStatus = "running" | "completed" | "failed";
352
+ export type EvalBatchStatus = "running" | "completed" | "failed" | "cancelled";
276
353
 
277
354
  /** One case inside a playground / HTTP eval batch snapshot. */
278
355
  export interface EvalCaseSnapshot {
@@ -356,6 +433,8 @@ export interface HostEvalsApi {
356
433
  activeRunId?: string;
357
434
  }>;
358
435
  start(input?: HostStartEvalRunInput): Promise<EvalRunSnapshot>;
436
+ /** Cancel a running batch by Eval ID (`runId`). */
437
+ cancel(runId: string): Promise<EvalRunSnapshot>;
359
438
  }
360
439
 
361
440
  export interface EvalConfigFile {