@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
@@ -63,6 +63,8 @@ export interface EvalRunResult {
63
63
  }>;
64
64
  error?: string;
65
65
  logs: string[];
66
+ /** Structured scores from {@link import("../evals.js").EvalTestContext.metric}. */
67
+ metrics: Record<string, string | number>;
66
68
  durationMs: number;
67
69
  }
68
70
 
@@ -95,6 +97,8 @@ export interface RunEvalsOptions extends EvalSelectionOptions {
95
97
  evals: readonly DiscoveredEval[];
96
98
  config: Partial<EvalConfigFile>;
97
99
  };
100
+ /** When aborted, stop starting new cases and cancel in-flight turns. */
101
+ signal?: AbortSignal;
98
102
  }
99
103
 
100
104
  async function defaultImport(
@@ -233,24 +237,86 @@ export async function runDiscoveredEvals(
233
237
  const maxConcurrency = resolveEvalMaxConcurrency(config);
234
238
  const defaultTimeoutMs = options.timeoutMs ?? config.timeoutMs ?? 180_000;
235
239
 
236
- return mapPool(selected, maxConcurrency, async (discovered) => {
237
- if (options.verbose) {
238
- process.stderr.write(` eval ${discovered.id}…\n`);
240
+ // Iterations of the same authored datapoint share one concurrency slot and
241
+ // run sequentially. maxConcurrency is therefore the max number of distinct
242
+ // datapoints in flight, not datapoints × iterations.
243
+ const groups = groupEvalsForIterationSchedule(selected);
244
+ const nested = await mapPool(
245
+ groups,
246
+ maxConcurrency,
247
+ async (group) => {
248
+ const results: EvalRunResult[] = [];
249
+ for (const discovered of group) {
250
+ if (options.signal?.aborted) {
251
+ break;
252
+ }
253
+ if (options.verbose) {
254
+ process.stderr.write(` eval ${discovered.id}…\n`);
255
+ }
256
+ options.onCaseStart?.(discovered.id);
257
+ const result = await runOneEval(discovered, {
258
+ baseUrl: options.baseUrl,
259
+ defaultTimeoutMs,
260
+ bearerToken: options.bearerToken,
261
+ headers: options.headers,
262
+ verbose: Boolean(options.verbose),
263
+ signal: options.signal,
264
+ onEvent: (event) => {
265
+ options.onCaseEvent?.(discovered.id, event);
266
+ options.onEvent?.(event);
267
+ },
268
+ });
269
+ options.onCaseDone?.(result);
270
+ results.push(result);
271
+ }
272
+ return results;
273
+ },
274
+ { signal: options.signal }
275
+ );
276
+ return nested.flat();
277
+ }
278
+
279
+ /**
280
+ * Schedule key for {@link groupEvalsForIterationSchedule}: iteration siblings
281
+ * (`weather/nyc/1`…`/3`) share a key; single-iteration ids keep their full id.
282
+ */
283
+ export function iterationScheduleKey(discovered: DiscoveredEval): string {
284
+ const iterations = discovered.definition.iterations ?? 1;
285
+ const iteration = discovered.definition.iteration ?? 1;
286
+ if (iterations <= 1) {
287
+ return discovered.id;
288
+ }
289
+ const suffix = `/${iteration}`;
290
+ if (discovered.id.endsWith(suffix)) {
291
+ return discovered.id.slice(0, -suffix.length);
292
+ }
293
+ return discovered.id;
294
+ }
295
+
296
+ /**
297
+ * Group expanded iteration cases so each authored datapoint's iterations run
298
+ * back-to-back under one {@link mapPool} slot (sorted by iteration index).
299
+ */
300
+ export function groupEvalsForIterationSchedule(
301
+ evals: readonly DiscoveredEval[]
302
+ ): DiscoveredEval[][] {
303
+ const groups = new Map<string, DiscoveredEval[]>();
304
+ const order: string[] = [];
305
+ for (const discovered of evals) {
306
+ const key = iterationScheduleKey(discovered);
307
+ let group = groups.get(key);
308
+ if (group === undefined) {
309
+ group = [];
310
+ groups.set(key, group);
311
+ order.push(key);
239
312
  }
240
- options.onCaseStart?.(discovered.id);
241
- const result = await runOneEval(discovered, {
242
- baseUrl: options.baseUrl,
243
- defaultTimeoutMs,
244
- bearerToken: options.bearerToken,
245
- headers: options.headers,
246
- verbose: Boolean(options.verbose),
247
- onEvent: (event) => {
248
- options.onCaseEvent?.(discovered.id, event);
249
- options.onEvent?.(event);
250
- },
251
- });
252
- options.onCaseDone?.(result);
253
- return result;
313
+ group.push(discovered);
314
+ }
315
+ return order.map((key) => {
316
+ const group = groups.get(key)!;
317
+ return group.sort(
318
+ (a, b) => (a.definition.iteration ?? 1) - (b.definition.iteration ?? 1)
319
+ );
254
320
  });
255
321
  }
256
322
 
@@ -274,9 +340,11 @@ async function runOneEval(
274
340
  headers?: Record<string, string>;
275
341
  verbose: boolean;
276
342
  onEvent?: (event: SessionEvent) => void;
343
+ signal?: AbortSignal;
277
344
  }
278
345
  ): Promise<EvalRunResult> {
279
346
  const logs: string[] = [];
347
+ const metrics: Record<string, string | number> = {};
280
348
  const assertions: EvalAssertionResult[] = [];
281
349
  const def = discovered.definition;
282
350
  const timeoutMs = def.timeoutMs ?? opts.defaultTimeoutMs;
@@ -288,6 +356,25 @@ async function runOneEval(
288
356
  const inputs: string[] = [];
289
357
  let reply: string | null = null;
290
358
 
359
+ if (opts.signal?.aborted) {
360
+ return {
361
+ id: discovered.id,
362
+ path: discovered.path,
363
+ ...(def.description === undefined
364
+ ? {}
365
+ : { description: def.description }),
366
+ ok: false,
367
+ assertions,
368
+ error: "cancelled",
369
+ logs,
370
+ metrics,
371
+ inputs,
372
+ toolCalls: [],
373
+ tools: [],
374
+ durationMs: Date.now() - startedAt,
375
+ };
376
+ }
377
+
291
378
  const record = (name: string, passed: boolean, detail?: string): void => {
292
379
  assertions.push({
293
380
  name,
@@ -321,6 +408,12 @@ async function runOneEval(
321
408
  get events() {
322
409
  return allEvents;
323
410
  },
411
+ get iteration() {
412
+ return def.iteration ?? 1;
413
+ },
414
+ get iterations() {
415
+ return def.iterations ?? 1;
416
+ },
324
417
  async send(
325
418
  message: string,
326
419
  options: EvalSendOptions = {}
@@ -337,7 +430,9 @@ async function runOneEval(
337
430
  headers: opts.headers,
338
431
  workspaceDir: options.workspaceDir,
339
432
  workspaceFiles: options.workspaceFiles,
433
+ cloud: options.cloud,
340
434
  timeoutMs,
435
+ signal: opts.signal,
341
436
  onEvent: opts.onEvent,
342
437
  });
343
438
  sessionId = result.sessionId;
@@ -400,6 +495,16 @@ async function runOneEval(
400
495
  process.stderr.write(` ${message}\n`);
401
496
  }
402
497
  },
498
+ metric(name: string, value: string | number) {
499
+ const key = name.trim();
500
+ if (key === "") {
501
+ return;
502
+ }
503
+ metrics[key] = value;
504
+ if (opts.verbose) {
505
+ process.stderr.write(` metric ${key}=${String(value)}\n`);
506
+ }
507
+ },
403
508
  };
404
509
 
405
510
  try {
@@ -419,6 +524,7 @@ async function runOneEval(
419
524
  assertions,
420
525
  error: message,
421
526
  logs,
527
+ metrics,
422
528
  inputs,
423
529
  toolCalls: toolCallIo(),
424
530
  ...(sessionId === undefined ? {} : { sessionId }),
@@ -441,6 +547,7 @@ async function runOneEval(
441
547
  ok,
442
548
  assertions,
443
549
  logs,
550
+ metrics,
444
551
  inputs,
445
552
  toolCalls: toolCallIo(),
446
553
  ...(sessionId === undefined ? {} : { sessionId }),
@@ -472,9 +579,13 @@ function matchExpectation(
472
579
  }
473
580
  case "equals": {
474
581
  const passed = deepEqual(value, assertion.value);
582
+ // Always attach a short detail so values like recall `40.0% (4/10)`
583
+ // show up on PASS lines / playground, not only on failure.
584
+ const rendered =
585
+ typeof value === "string" ? value : JSON.stringify(value);
475
586
  return {
476
587
  passed,
477
- ...(passed ? {} : { detail: `got ${JSON.stringify(value)}` }),
588
+ detail: passed ? rendered : `got ${rendered}`,
478
589
  };
479
590
  }
480
591
  case "satisfies": {
@@ -17,9 +17,11 @@ const DEFAULT_POLL_MS = 1_000;
17
17
  export type RemoteEvalErrorCode =
18
18
  | "no_matching_evals"
19
19
  | "eval_run_in_progress"
20
+ | "eval_run_not_running"
20
21
  | "unknown_eval_run"
21
22
  | "start_failed"
22
23
  | "poll_failed"
24
+ | "cancel_failed"
23
25
  | "aborted";
24
26
 
25
27
  export class RemoteEvalError extends Error {
@@ -269,6 +271,85 @@ export async function startRemoteEvalRun(args: {
269
271
  return json as EvalRunSnapshot;
270
272
  }
271
273
 
274
+ /** Cancel a running batch via `POST /v1/dev/evals/runs/:runId/cancel`. */
275
+ export async function cancelRemoteEvalRun(args: {
276
+ baseUrl: string;
277
+ runId: string;
278
+ auth?: RequestAuth;
279
+ fetchImpl?: typeof fetch;
280
+ signal?: AbortSignal;
281
+ }): Promise<EvalRunSnapshot> {
282
+ const baseUrl = args.baseUrl.replace(/\/$/, "");
283
+ const fetchImpl = args.fetchImpl ?? fetch;
284
+ let resp: Response;
285
+ try {
286
+ resp = await fetchImpl(
287
+ `${baseUrl}/v1/dev/evals/runs/${encodeURIComponent(args.runId)}/cancel`,
288
+ {
289
+ method: "POST",
290
+ headers: mergeRequestHeaders(args.auth),
291
+ signal: args.signal,
292
+ }
293
+ );
294
+ } catch (error) {
295
+ throw new RemoteEvalError(
296
+ error instanceof Error ? error.message : "Could not cancel eval run.",
297
+ undefined,
298
+ "cancel_failed"
299
+ );
300
+ }
301
+
302
+ const json = (await resp.json().catch(() => ({}))) as {
303
+ ok?: boolean;
304
+ error?: string;
305
+ message?: string;
306
+ status?: string;
307
+ runId?: string;
308
+ };
309
+
310
+ if (resp.status === 404 || json.error === "unknown_eval_run") {
311
+ throw new RemoteEvalError(
312
+ `Unknown eval run ${args.runId}.`,
313
+ 404,
314
+ "unknown_eval_run"
315
+ );
316
+ }
317
+ if (resp.status === 409 || json.error === "eval_run_not_running") {
318
+ const status =
319
+ typeof json.status === "string" && json.status !== ""
320
+ ? json.status
321
+ : "unknown";
322
+ throw new RemoteEvalError(
323
+ typeof json.message === "string" && json.message !== ""
324
+ ? json.message
325
+ : `Eval run ${args.runId} is not running (status: ${status}).`,
326
+ 409,
327
+ "eval_run_not_running"
328
+ );
329
+ }
330
+ if (!resp.ok) {
331
+ const detail =
332
+ typeof json.message === "string" && json.message !== ""
333
+ ? json.message
334
+ : typeof json.error === "string" && json.error !== ""
335
+ ? json.error
336
+ : `HTTP ${resp.status}`;
337
+ throw new RemoteEvalError(
338
+ `Could not cancel eval run: ${detail}`,
339
+ resp.status,
340
+ "cancel_failed"
341
+ );
342
+ }
343
+ if (typeof json.runId !== "string" || json.runId === "") {
344
+ throw new RemoteEvalError(
345
+ "Eval cancel response missing runId.",
346
+ undefined,
347
+ "cancel_failed"
348
+ );
349
+ }
350
+ return json as EvalRunSnapshot;
351
+ }
352
+
272
353
  /** One-shot `GET /v1/dev/evals/runs/:runId` (no polling). */
273
354
  export async function getRemoteEvalRun(args: {
274
355
  baseUrl: string;
@@ -408,6 +489,7 @@ export function snapshotToEvalRunResults(
408
489
  ...(c.toolCalls === undefined ? {} : { toolCalls: c.toolCalls }),
409
490
  ...(c.error === undefined ? {} : { error: c.error }),
410
491
  logs: c.logs ?? [],
492
+ metrics: c.metrics ?? {},
411
493
  durationMs: c.durationMs ?? 0,
412
494
  }));
413
495
  }
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import type {
15
+ AgentCloudOptions,
15
16
  AuthContext,
16
17
  AuthPolicy,
17
18
  HttpChannelDefinition,
@@ -100,12 +101,16 @@ export function buildHttpChannelRoutes(args: {
100
101
  }
101
102
  const message = await transformMessage(body.message, ctx.auth);
102
103
  const workspaceFiles = readWorkspaceFiles(body.workspaceFiles);
104
+ // Per-session cloud attach (create only). Eval harness uses this to pin
105
+ // fixture repos without putting them on the agent's default cloud.repos.
106
+ const cloud = readCloudOptions(body.cloud);
103
107
  const session = await engine.send(HTTP_CHANNEL_ID, message, {
104
108
  auth: ctx.auth,
105
109
  ...(body.state === undefined ? {} : { state: body.state as JsonValue }),
106
110
  ...(typeof body.title === "string" ? { title: body.title } : {}),
107
111
  ...(body.purpose === "eval" ? { purpose: "eval" as const } : {}),
108
112
  ...(workspaceFiles === undefined ? {} : { workspaceFiles }),
113
+ ...(cloud === undefined ? {} : { cloud }),
109
114
  });
110
115
  return jsonResponse(
111
116
  {
@@ -129,7 +134,7 @@ export function buildHttpChannelRoutes(args: {
129
134
  auth,
130
135
  handler: async (request, ctx) => {
131
136
  const sessionId = ctx.params.sessionId ?? "";
132
- const record = await engine.sessions.get(sessionId);
137
+ const record = await engine.getOrRestoreSession(sessionId);
133
138
  if (record === undefined) {
134
139
  return errorResponse(
135
140
  404,
@@ -230,7 +235,9 @@ export function buildHttpChannelRoutes(args: {
230
235
  // custom channels or schedules are observable here too when the caller
231
236
  // matches the session's stored principal (or `--dev` relax). Follow-ups
232
237
  // use the same ownership gate plus the session's continuation token.
233
- const record = await engine.sessions.get(sessionId);
238
+ // Lazy-restore by id so playground Open-trace works after redeploy
239
+ // when the session is durable but not in the local state root yet.
240
+ const record = await engine.getOrRestoreSession(sessionId);
234
241
  if (record === undefined) {
235
242
  return errorResponse(
236
243
  404,
@@ -276,7 +283,7 @@ export function buildHttpChannelRoutes(args: {
276
283
  auth,
277
284
  handler: async (request, ctx) => {
278
285
  const sessionId = ctx.params.sessionId ?? "";
279
- const record = await engine.sessions.get(sessionId);
286
+ const record = await engine.getOrRestoreSession(sessionId);
280
287
  if (record === undefined) {
281
288
  return errorResponse(
282
289
  404,
@@ -323,7 +330,7 @@ export function buildHttpChannelRoutes(args: {
323
330
  auth,
324
331
  handler: async (_request, ctx) => {
325
332
  const sessionId = ctx.params.sessionId ?? "";
326
- const record = await engine.sessions.get(sessionId);
333
+ const record = await engine.getOrRestoreSession(sessionId);
327
334
  if (record === undefined) {
328
335
  return errorResponse(
329
336
  404,
@@ -351,7 +358,7 @@ export function buildHttpChannelRoutes(args: {
351
358
  handler: async (request, ctx) => {
352
359
  const sessionId = ctx.params.sessionId ?? "";
353
360
  const callId = ctx.params.callId ?? "";
354
- const record = await engine.sessions.get(sessionId);
361
+ const record = await engine.getOrRestoreSession(sessionId);
355
362
  if (record === undefined) {
356
363
  return errorResponse(
357
364
  404,
@@ -420,7 +427,7 @@ export function buildHttpChannelRoutes(args: {
420
427
  auth,
421
428
  handler: async (_request, ctx) => {
422
429
  const sessionId = ctx.params.sessionId ?? "";
423
- const record = await engine.sessions.get(sessionId);
430
+ const record = await engine.getOrRestoreSession(sessionId);
424
431
  if (record === undefined) {
425
432
  return errorResponse(
426
433
  404,
@@ -527,6 +534,80 @@ function readWorkspaceFiles(
527
534
  return Object.keys(files).length === 0 ? undefined : files;
528
535
  }
529
536
 
537
+ function isRecord(value: unknown): value is Record<string, unknown> {
538
+ return typeof value === "object" && value !== null && !Array.isArray(value);
539
+ }
540
+
541
+ /** Best-effort parse of {@link AgentCloudOptions} from a JSON body field. */
542
+ function readCloudOptions(value: unknown): AgentCloudOptions | undefined {
543
+ if (!isRecord(value)) {
544
+ return undefined;
545
+ }
546
+ const cloud: AgentCloudOptions = {};
547
+
548
+ if (isRecord(value.env) && typeof value.env.type === "string") {
549
+ const name =
550
+ typeof value.env.name === "string" ? value.env.name : undefined;
551
+ if (value.env.type === "cloud") {
552
+ cloud.env =
553
+ name === undefined ? { type: "cloud" } : { type: "cloud", name };
554
+ } else if (value.env.type === "pool") {
555
+ cloud.env =
556
+ name === undefined ? { type: "pool" } : { type: "pool", name };
557
+ } else if (value.env.type === "machine") {
558
+ cloud.env =
559
+ name === undefined ? { type: "machine" } : { type: "machine", name };
560
+ }
561
+ }
562
+
563
+ if (Array.isArray(value.repos)) {
564
+ const repos: NonNullable<AgentCloudOptions["repos"]> = [];
565
+ for (const entry of value.repos) {
566
+ if (
567
+ !isRecord(entry) ||
568
+ typeof entry.url !== "string" ||
569
+ entry.url === ""
570
+ ) {
571
+ continue;
572
+ }
573
+ repos.push({
574
+ url: entry.url,
575
+ ...(typeof entry.startingRef === "string"
576
+ ? { startingRef: entry.startingRef }
577
+ : {}),
578
+ ...(typeof entry.prUrl === "string" ? { prUrl: entry.prUrl } : {}),
579
+ });
580
+ }
581
+ if (repos.length > 0) {
582
+ cloud.repos = repos;
583
+ }
584
+ }
585
+
586
+ if (typeof value.workOnCurrentBranch === "boolean") {
587
+ cloud.workOnCurrentBranch = value.workOnCurrentBranch;
588
+ }
589
+ if (typeof value.autoCreatePR === "boolean") {
590
+ cloud.autoCreatePR = value.autoCreatePR;
591
+ }
592
+ if (typeof value.skipReviewerRequest === "boolean") {
593
+ cloud.skipReviewerRequest = value.skipReviewerRequest;
594
+ }
595
+
596
+ if (isRecord(value.envVars)) {
597
+ const envVars: Record<string, string> = {};
598
+ for (const [key, envValue] of Object.entries(value.envVars)) {
599
+ if (typeof key === "string" && typeof envValue === "string") {
600
+ envVars[key] = envValue;
601
+ }
602
+ }
603
+ if (Object.keys(envVars).length > 0) {
604
+ cloud.envVars = envVars;
605
+ }
606
+ }
607
+
608
+ return Object.keys(cloud).length === 0 ? undefined : cloud;
609
+ }
610
+
530
611
  export async function readJsonObject(
531
612
  request: Request
532
613
  ): Promise<Record<string, unknown> | undefined> {
@@ -1,16 +1,23 @@
1
1
  /**
2
2
  * Run `fn` over `items` with at most `concurrency` in flight; preserve order
3
3
  * of results (and of side-effect completion relative to index assignment).
4
+ *
5
+ * When `signal` aborts, workers stop claiming new items; in-flight `fn` calls
6
+ * are responsible for honoring the same signal.
4
7
  */
5
8
  export async function mapPool<T, R>(
6
9
  items: readonly T[],
7
10
  concurrency: number,
8
- fn: (item: T, index: number) => Promise<R>
11
+ fn: (item: T, index: number) => Promise<R>,
12
+ options?: { signal?: AbortSignal }
9
13
  ): Promise<R[]> {
10
14
  const results = new Array<R>(items.length);
11
15
  let next = 0;
12
16
  async function worker(): Promise<void> {
13
17
  for (;;) {
18
+ if (options?.signal?.aborted) {
19
+ return;
20
+ }
14
21
  const index = next;
15
22
  next += 1;
16
23
  if (index >= items.length) {
@@ -52,17 +52,18 @@ export class McpHost implements HostMcpRegistry {
52
52
  }
53
53
 
54
54
  async listTools(connectionName: string): Promise<HostMcpToolInfo[]> {
55
- const { client } = await this.getClient(connectionName);
56
- const listed = await client.listTools();
57
- return listed.tools.map((tool) => ({
58
- name: tool.name,
59
- ...(tool.description === undefined
60
- ? {}
61
- : { description: tool.description }),
62
- ...(tool.inputSchema === undefined
63
- ? {}
64
- : { inputSchema: tool.inputSchema as JsonObject }),
65
- }));
55
+ return this.withClient(connectionName, async (client) => {
56
+ const listed = await client.listTools();
57
+ return listed.tools.map((tool) => ({
58
+ name: tool.name,
59
+ ...(tool.description === undefined
60
+ ? {}
61
+ : { description: tool.description }),
62
+ ...(tool.inputSchema === undefined
63
+ ? {}
64
+ : { inputSchema: tool.inputSchema as JsonObject }),
65
+ }));
66
+ });
66
67
  }
67
68
 
68
69
  async callTool(
@@ -70,12 +71,13 @@ export class McpHost implements HostMcpRegistry {
70
71
  toolName: string,
71
72
  args: Record<string, unknown> = {}
72
73
  ): Promise<HostMcpCallResult> {
73
- const { client } = await this.getClient(connectionName);
74
- const result = await client.callTool({
75
- name: toolName,
76
- arguments: args,
74
+ return this.withClient(connectionName, async (client) => {
75
+ const result = await client.callTool({
76
+ name: toolName,
77
+ arguments: args,
78
+ });
79
+ return normalizeCallResult(result);
77
80
  });
78
- return normalizeCallResult(result);
79
81
  }
80
82
 
81
83
  async close(): Promise<void> {
@@ -90,6 +92,32 @@ export class McpHost implements HostMcpRegistry {
90
92
  );
91
93
  }
92
94
 
95
+ /**
96
+ * Run against a cached host MCP client, reconnecting once when the remote
97
+ * Streamable HTTP session is gone (`session_terminated` / HTTP 404).
98
+ *
99
+ * Anytool (and the MCP spec) return 404 for unknown/terminated
100
+ * `Mcp-Session-Id`s so clients can re-initialize. The SDK transport keeps
101
+ * the dead session id, so without this invalidation every later host.mcp
102
+ * call fails until the serve process restarts.
103
+ */
104
+ private async withClient<T>(
105
+ name: string,
106
+ run: (client: Client) => Promise<T>
107
+ ): Promise<T> {
108
+ const opened = await this.getClient(name);
109
+ try {
110
+ return await run(opened.client);
111
+ } catch (error) {
112
+ if (!isStaleMcpSessionError(error)) {
113
+ throw error;
114
+ }
115
+ await this.invalidateClient(name, opened);
116
+ const reopened = await this.getClient(name);
117
+ return await run(reopened.client);
118
+ }
119
+ }
120
+
93
121
  private getClient(name: string): Promise<OpenedConnection> {
94
122
  let pending = this.clients.get(name);
95
123
  if (pending === undefined) {
@@ -102,6 +130,23 @@ export class McpHost implements HostMcpRegistry {
102
130
  return pending;
103
131
  }
104
132
 
133
+ private async invalidateClient(
134
+ name: string,
135
+ opened: OpenedConnection
136
+ ): Promise<void> {
137
+ const pending = this.clients.get(name);
138
+ if (pending !== undefined) {
139
+ try {
140
+ if ((await pending) === opened) {
141
+ this.clients.delete(name);
142
+ }
143
+ } catch {
144
+ this.clients.delete(name);
145
+ }
146
+ }
147
+ await opened.close();
148
+ }
149
+
105
150
  private async connect(name: string): Promise<OpenedConnection> {
106
151
  const connection = this.byName.get(name);
107
152
  if (connection === undefined) {
@@ -114,6 +159,19 @@ export class McpHost implements HostMcpRegistry {
114
159
  }
115
160
  }
116
161
 
162
+ /** Exported for unit tests. */
163
+ export function isStaleMcpSessionError(error: unknown): boolean {
164
+ if (!(error instanceof Error)) {
165
+ return false;
166
+ }
167
+ if (error.message.includes("session_terminated")) {
168
+ return true;
169
+ }
170
+ // StreamableHTTPError from the MCP SDK carries the HTTP status as `.code`.
171
+ const code = (error as { code?: unknown }).code;
172
+ return code === 404;
173
+ }
174
+
117
175
  async function openConnection(
118
176
  connection: DiscoveredConnection
119
177
  ): Promise<OpenedConnection> {