@cursor/july 0.1.12 → 0.1.15

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 (168) 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/slack/api.d.ts +9 -0
  10. package/dist/channels/slack/api.d.ts.map +1 -1
  11. package/dist/channels/slack/api.js +40 -7
  12. package/dist/channels/slack/eval-directive.d.ts +3 -0
  13. package/dist/channels/slack/eval-directive.d.ts.map +1 -1
  14. package/dist/channels/slack/eval-directive.js +20 -1
  15. package/dist/channels/slack/post-update-delivery.d.ts +85 -0
  16. package/dist/channels/slack/post-update-delivery.d.ts.map +1 -0
  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.DpdGZTLe.js} +1 -1
  20. package/dist/docs/assets/chunks/@localSearchIndexroot.D2d8CfFS.js +1 -0
  21. package/dist/docs/assets/chunks/{VPLocalSearchBox.CptQNLkR.js → VPLocalSearchBox.IB-0QqIT.js} +1 -1
  22. package/dist/docs/assets/chunks/{theme.DWflH-LQ.js → theme.-fYBoNtR.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 +49 -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 +166 -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 +94 -8
  101. package/dist/internal/eval-runner.d.ts +14 -0
  102. package/dist/internal/eval-runner.d.ts.map +1 -1
  103. package/dist/internal/eval-runner.js +111 -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 +44 -1
  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/persistence-coordinator.d.ts +127 -0
  116. package/dist/internal/persistence-coordinator.d.ts.map +1 -0
  117. package/dist/internal/run-client.d.ts +5 -1
  118. package/dist/internal/run-client.d.ts.map +1 -1
  119. package/dist/internal/run-client.js +26 -5
  120. package/dist/internal/server.d.ts.map +1 -1
  121. package/dist/internal/server.js +57 -4
  122. package/dist/internal/session-engine.d.ts +13 -0
  123. package/dist/internal/session-engine.d.ts.map +1 -1
  124. package/dist/internal/session-engine.js +49 -0
  125. package/dist/internal/storage-coordinator.d.ts +7 -0
  126. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  127. package/dist/internal/storage-coordinator.js +29 -4
  128. package/dist/persistence.d.ts +184 -0
  129. package/dist/persistence.d.ts.map +1 -0
  130. package/dist/playground/assets/{index-1dzluIcW.js → index-BVMOoiek.js} +44 -44
  131. package/dist/playground/assets/index-DYmP4b--.css +1 -0
  132. package/dist/playground/index.html +2 -2
  133. package/dist/types.d.ts +10 -0
  134. package/dist/types.d.ts.map +1 -1
  135. package/docs/evals.md +46 -11
  136. package/docs/reference/cli.md +3 -0
  137. package/docs/reference/http-api.md +2 -1
  138. package/package.json +1 -1
  139. package/skills/evals/SKILL.md +10 -0
  140. package/src/bin/agent-serve.ts +38 -8
  141. package/src/channels/github/api.ts +12 -1
  142. package/src/channels/github/cursor-account.ts +51 -1
  143. package/src/channels/github/github-channel.ts +4 -1
  144. package/src/channels/slack/api.ts +57 -8
  145. package/src/channels/slack/eval-directive.ts +24 -1
  146. package/src/evals.ts +112 -25
  147. package/src/internal/cli-ax.ts +195 -16
  148. package/src/internal/cli-deploy.ts +140 -13
  149. package/src/internal/cli-mcp-oauth.ts +122 -3
  150. package/src/internal/cursor/backend-client.ts +104 -1
  151. package/src/internal/cursor/credentials.ts +10 -2
  152. package/src/internal/cursor-account-mcp-auth.ts +241 -0
  153. package/src/internal/deploy-client.ts +16 -0
  154. package/src/internal/eval-run-store.ts +97 -6
  155. package/src/internal/eval-runner.ts +129 -18
  156. package/src/internal/evals-client.ts +82 -0
  157. package/src/internal/http-channel.ts +87 -6
  158. package/src/internal/map-pool.ts +8 -1
  159. package/src/internal/mcp-host.ts +74 -16
  160. package/src/internal/run-client.ts +32 -1
  161. package/src/internal/server.ts +69 -2
  162. package/src/internal/session-engine.ts +58 -0
  163. package/src/internal/storage-coordinator.ts +32 -6
  164. package/src/types.ts +10 -0
  165. package/dist/docs/assets/chunks/@localSearchIndexroot.Er_R50vY.js +0 -1
  166. package/dist/internal/json-dir-store.js +0 -100
  167. package/dist/playground/assets/index-kZjeJs1v.css +0 -1
  168. /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`. */
@@ -115,8 +129,17 @@ export interface EvalTestContext {
115
129
  check(value: unknown, assertion: Expectation): void;
116
130
  /** Debug line (surfaced under `agent-serve eval --verbose`). */
117
131
  log(message: string): void;
132
+ /**
133
+ * Structured score for the playground (and CLI). Values appear on the
134
+ * case card; agents can further customize rendering via
135
+ * `agent/playground/evals.tsx`.
136
+ */
137
+ metric(name: string, value: string | number): void;
118
138
  }
119
139
 
140
+ /** Hard ceiling for {@link EvalConfig.iterations} / case `iterations`. */
141
+ export const EVAL_MAX_ITERATIONS = 100;
142
+
120
143
  /** One runnable datapoint inside a multi-case {@link defineEval} file. */
121
144
  export interface EvalDatapoint {
122
145
  /**
@@ -128,6 +151,13 @@ export interface EvalDatapoint {
128
151
  description?: string;
129
152
  tags?: string[];
130
153
  timeoutMs?: number;
154
+ /**
155
+ * How many times to run this datapoint (default `1`, or the file-level
156
+ * {@link EvalConfig.iterations}). When greater than 1, discovery expands
157
+ * to `<fileId>/<id>/1` … `<fileId>/<id>/<n>` so each iteration is its own
158
+ * concurrent case.
159
+ */
160
+ iterations?: number;
131
161
  test: (t: EvalTestContext) => Promise<void>;
132
162
  }
133
163
 
@@ -139,6 +169,11 @@ export interface EvalConfig {
139
169
  description?: string;
140
170
  tags?: string[];
141
171
  timeoutMs?: number;
172
+ /**
173
+ * Default iterations for every datapoint in this file (default `1`).
174
+ * Case-level {@link EvalDatapoint.iterations} overrides this.
175
+ */
176
+ iterations?: number;
142
177
  /** Single-datapoint shorthand. Case id = file path under `evals/`. */
143
178
  test?: (t: EvalTestContext) => Promise<void>;
144
179
  /** Multiple datapoints in this file. Each id = `<fileId>/<case.id>`. */
@@ -150,6 +185,10 @@ export interface EvalCaseDefinition {
150
185
  description?: string;
151
186
  tags?: string[];
152
187
  timeoutMs?: number;
188
+ /** Total iterations configured for the authored datapoint. */
189
+ iterations?: number;
190
+ /** 1-based iteration index for this expanded runnable case. */
191
+ iteration?: number;
153
192
  test: (t: EvalTestContext) => Promise<void>;
154
193
  readonly __agentServe: "eval";
155
194
  }
@@ -161,6 +200,13 @@ export type EvalDefinition = EvalConfig & { readonly __agentServe: "eval" };
161
200
  * `evals/weather.eval.ts` → `weather`. With a single `test`, that is also
162
201
  * the case id. With `cases`, each datapoint is `<fileId>/<case.id>`.
163
202
  */
203
+ export function normalizeEvalIterations(value: unknown): number {
204
+ return normalizePositiveInt(value, {
205
+ label: "defineEval: iterations",
206
+ max: EVAL_MAX_ITERATIONS,
207
+ });
208
+ }
209
+
164
210
  export function defineEval(config: EvalConfig): EvalDefinition {
165
211
  const hasTest = typeof config.test === "function";
166
212
  const hasCases = config.cases !== undefined;
@@ -170,6 +216,9 @@ export function defineEval(config: EvalConfig): EvalDefinition {
170
216
  if (!hasTest && !hasCases) {
171
217
  throw new Error("defineEval requires test(t) or a non-empty cases array");
172
218
  }
219
+ if (config.iterations !== undefined) {
220
+ normalizeEvalIterations(config.iterations);
221
+ }
173
222
  if (hasCases) {
174
223
  if (!Array.isArray(config.cases) || config.cases.length === 0) {
175
224
  throw new Error("defineEval: cases must be a non-empty array");
@@ -188,6 +237,9 @@ export function defineEval(config: EvalConfig): EvalDefinition {
188
237
  `defineEval: cases[${JSON.stringify(datapoint.id)}] requires test(t)`
189
238
  );
190
239
  }
240
+ if (datapoint.iterations !== undefined) {
241
+ normalizeEvalIterations(datapoint.iterations);
242
+ }
191
243
  }
192
244
  }
193
245
  return brandDefinition("eval", config);
@@ -207,44 +259,75 @@ function validateDatapointId(id: unknown): asserts id is string {
207
259
  /**
208
260
  * Expand a file-level definition into runnable datapoints.
209
261
  * Exported for tests and tooling.
262
+ *
263
+ * When `iterations` > 1, each authored datapoint expands to
264
+ * `<fileId>/<case.id>/1` … `<fileId>/<case.id>/<n>` (single-test files
265
+ * expand to `<fileId>/1` … `<fileId>/<n>`).
210
266
  */
211
267
  export function expandEvalDefinition(
212
268
  fileId: string,
213
269
  definition: EvalDefinition
214
270
  ): Array<{ id: string; definition: EvalCaseDefinition }> {
215
271
  if (definition.cases !== undefined) {
216
- return definition.cases.map((datapoint) => {
272
+ return definition.cases.flatMap((datapoint) => {
217
273
  const description = datapoint.description ?? definition.description;
218
274
  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
- };
275
+ const iterations = normalizeEvalIterations(
276
+ datapoint.iterations ?? definition.iterations ?? 1
277
+ );
278
+ return expandIterations({
279
+ baseId: `${fileId}/${datapoint.id}`,
280
+ description,
281
+ tags: datapoint.tags ?? definition.tags,
282
+ timeoutMs,
283
+ iterations,
284
+ test: datapoint.test,
285
+ });
228
286
  });
229
287
  }
230
288
  if (typeof definition.test !== "function") {
231
289
  throw new Error(`defineEval(${fileId}): missing test(t)`);
232
290
  }
233
- return [
234
- {
235
- id: fileId,
291
+ const iterations = normalizeEvalIterations(definition.iterations ?? 1);
292
+ return expandIterations({
293
+ baseId: fileId,
294
+ description: definition.description,
295
+ tags: definition.tags,
296
+ timeoutMs: definition.timeoutMs,
297
+ iterations,
298
+ test: definition.test,
299
+ });
300
+ }
301
+
302
+ function expandIterations(args: {
303
+ baseId: string;
304
+ description?: string;
305
+ tags?: string[];
306
+ timeoutMs?: number;
307
+ iterations: number;
308
+ test: (t: EvalTestContext) => Promise<void>;
309
+ }): Array<{ id: string; definition: EvalCaseDefinition }> {
310
+ const out: Array<{ id: string; definition: EvalCaseDefinition }> = [];
311
+ for (let iteration = 1; iteration <= args.iterations; iteration++) {
312
+ const id =
313
+ args.iterations === 1 ? args.baseId : `${args.baseId}/${iteration}`;
314
+ const description =
315
+ args.iterations === 1 || args.description === undefined
316
+ ? args.description
317
+ : `${args.description} (iteration ${iteration}/${args.iterations})`;
318
+ out.push({
319
+ id,
236
320
  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,
321
+ ...(description !== undefined ? { description } : {}),
322
+ ...(args.tags !== undefined ? { tags: args.tags } : {}),
323
+ ...(args.timeoutMs !== undefined ? { timeoutMs: args.timeoutMs } : {}),
324
+ iterations: args.iterations,
325
+ iteration,
326
+ test: args.test,
245
327
  }),
246
- },
247
- ];
328
+ });
329
+ }
330
+ return out;
248
331
  }
249
332
 
250
333
  /**
@@ -272,7 +355,7 @@ export const EVAL_DEFAULT_MAX_PLAYGROUND_RUNS = 20;
272
355
  export const EVAL_MAX_PLAYGROUND_RUNS = 500;
273
356
 
274
357
  export type EvalCaseRunStatus = "pending" | "running" | "done";
275
- export type EvalBatchStatus = "running" | "completed" | "failed";
358
+ export type EvalBatchStatus = "running" | "completed" | "failed" | "cancelled";
276
359
 
277
360
  /** One case inside a playground / HTTP eval batch snapshot. */
278
361
  export interface EvalCaseSnapshot {
@@ -287,6 +370,8 @@ export interface EvalCaseSnapshot {
287
370
  durationMs?: number;
288
371
  error?: string;
289
372
  logs?: string[];
373
+ /** Structured scores from {@link EvalTestContext.metric}. */
374
+ metrics?: Record<string, string | number>;
290
375
  inputs?: string[];
291
376
  finalText?: string;
292
377
  tools?: string[];
@@ -356,6 +441,8 @@ export interface HostEvalsApi {
356
441
  activeRunId?: string;
357
442
  }>;
358
443
  start(input?: HostStartEvalRunInput): Promise<EvalRunSnapshot>;
444
+ /** Cancel a running batch by Eval ID (`runId`). */
445
+ cancel(runId: string): Promise<EvalRunSnapshot>;
359
446
  }
360
447
 
361
448
  export interface EvalConfigFile {