@cursor/july 0.1.11 → 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 (174) 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.BcXUGca7.js → app.CKFFLL4Z.js} +1 -1
  20. package/dist/docs/assets/chunks/@localSearchIndexroot.DGWkvXJc.js +1 -0
  21. package/dist/docs/assets/chunks/{VPLocalSearchBox.CWur6XGZ.js → VPLocalSearchBox.BXk19JBN.js} +1 -1
  22. package/dist/docs/assets/chunks/{theme.BnvulOX2.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/cursor-event-relay.d.ts +2 -0
  96. package/dist/internal/cursor-event-relay.d.ts.map +1 -1
  97. package/dist/internal/cursor-event-relay.js +3 -0
  98. package/dist/internal/deploy-client.d.ts +6 -0
  99. package/dist/internal/deploy-client.d.ts.map +1 -1
  100. package/dist/internal/deploy-client.js +13 -0
  101. package/dist/internal/eval-run-store.d.ts +15 -0
  102. package/dist/internal/eval-run-store.d.ts.map +1 -1
  103. package/dist/internal/eval-run-store.js +87 -8
  104. package/dist/internal/eval-runner.d.ts +12 -0
  105. package/dist/internal/eval-runner.d.ts.map +1 -1
  106. package/dist/internal/eval-runner.js +97 -22
  107. package/dist/internal/evals-client.d.ts +9 -1
  108. package/dist/internal/evals-client.d.ts.map +1 -1
  109. package/dist/internal/evals-client.js +43 -0
  110. package/dist/internal/http-channel.d.ts.map +1 -1
  111. package/dist/internal/http-channel.js +74 -7
  112. package/dist/internal/map-pool.d.ts +6 -1
  113. package/dist/internal/map-pool.d.ts.map +1 -1
  114. package/dist/internal/map-pool.js +8 -1
  115. package/dist/internal/mcp-host.d.ts +13 -0
  116. package/dist/internal/mcp-host.d.ts.map +1 -1
  117. package/dist/internal/mcp-host.js +68 -13
  118. package/dist/internal/run-client.d.ts +5 -1
  119. package/dist/internal/run-client.d.ts.map +1 -1
  120. package/dist/internal/run-client.js +26 -5
  121. package/dist/internal/server.d.ts.map +1 -1
  122. package/dist/internal/server.js +57 -4
  123. package/dist/internal/session-engine.d.ts +13 -0
  124. package/dist/internal/session-engine.d.ts.map +1 -1
  125. package/dist/internal/session-engine.js +49 -0
  126. package/dist/internal/storage-coordinator.d.ts +7 -0
  127. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  128. package/dist/internal/storage-coordinator.js +29 -4
  129. package/dist/playground/assets/index-CuXb4j4V.css +1 -0
  130. package/dist/playground/assets/index-DeyYpOit.js +86 -0
  131. package/dist/playground/index.html +2 -2
  132. package/dist/types.d.ts +10 -0
  133. package/dist/types.d.ts.map +1 -1
  134. package/docs/evals.md +46 -11
  135. package/docs/reference/cli.md +3 -0
  136. package/docs/reference/http-api.md +2 -1
  137. package/package.json +1 -1
  138. package/src/bin/agent-serve.ts +38 -8
  139. package/src/channels/github/api.ts +12 -1
  140. package/src/channels/github/cursor-account.ts +51 -1
  141. package/src/channels/github/github-channel.ts +4 -1
  142. package/src/channels/slack/api.ts +57 -8
  143. package/src/channels/slack/eval-directive.ts +24 -1
  144. package/src/evals.ts +104 -25
  145. package/src/internal/cli-ax.ts +192 -16
  146. package/src/internal/cli-deploy.ts +140 -13
  147. package/src/internal/cli-mcp-oauth.ts +122 -3
  148. package/src/internal/cursor/backend-client.ts +104 -1
  149. package/src/internal/cursor/credentials.ts +10 -2
  150. package/src/internal/cursor-account-mcp-auth.ts +241 -0
  151. package/src/internal/cursor-event-relay.ts +5 -0
  152. package/src/internal/deploy-client.ts +16 -0
  153. package/src/internal/eval-run-store.ts +89 -6
  154. package/src/internal/eval-runner.ts +113 -18
  155. package/src/internal/evals-client.ts +81 -0
  156. package/src/internal/http-channel.ts +87 -6
  157. package/src/internal/map-pool.ts +8 -1
  158. package/src/internal/mcp-host.ts +74 -16
  159. package/src/internal/run-client.ts +32 -1
  160. package/src/internal/server.ts +69 -2
  161. package/src/internal/session-engine.ts +58 -0
  162. package/src/internal/storage-coordinator.ts +32 -6
  163. package/src/types.ts +10 -0
  164. package/dist/channels/slack/post-update-delivery.d.ts +0 -85
  165. package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
  166. package/dist/channels/slack/post-update-delivery.js +0 -318
  167. package/dist/docs/assets/chunks/@localSearchIndexroot.BIbvK8M1.js +0 -1
  168. package/dist/internal/corp-ca.d.ts +0 -9
  169. package/dist/internal/corp-ca.d.ts.map +0 -1
  170. package/dist/internal/corp-ca.js +0 -44
  171. package/dist/playground/assets/index-BIGLubwK.js +0 -86
  172. package/dist/playground/assets/index-C17ZQKDG.css +0 -1
  173. package/docs/.vitepress/cache/deps/package.json +0 -3
  174. /package/dist/docs/assets/{reference_cli.md.B74NrVzb.lean.js → reference_cli.md.DnYfr5V2.lean.js} +0 -0
@@ -95,6 +95,8 @@ export interface RunEvalsOptions extends EvalSelectionOptions {
95
95
  evals: readonly DiscoveredEval[];
96
96
  config: Partial<EvalConfigFile>;
97
97
  };
98
+ /** When aborted, stop starting new cases and cancel in-flight turns. */
99
+ signal?: AbortSignal;
98
100
  }
99
101
 
100
102
  async function defaultImport(
@@ -233,24 +235,86 @@ export async function runDiscoveredEvals(
233
235
  const maxConcurrency = resolveEvalMaxConcurrency(config);
234
236
  const defaultTimeoutMs = options.timeoutMs ?? config.timeoutMs ?? 180_000;
235
237
 
236
- return mapPool(selected, maxConcurrency, async (discovered) => {
237
- if (options.verbose) {
238
- process.stderr.write(` eval ${discovered.id}…\n`);
238
+ // Iterations of the same authored datapoint share one concurrency slot and
239
+ // run sequentially. maxConcurrency is therefore the max number of distinct
240
+ // datapoints in flight, not datapoints × iterations.
241
+ const groups = groupEvalsForIterationSchedule(selected);
242
+ const nested = await mapPool(
243
+ groups,
244
+ maxConcurrency,
245
+ async (group) => {
246
+ const results: EvalRunResult[] = [];
247
+ for (const discovered of group) {
248
+ if (options.signal?.aborted) {
249
+ break;
250
+ }
251
+ if (options.verbose) {
252
+ process.stderr.write(` eval ${discovered.id}…\n`);
253
+ }
254
+ options.onCaseStart?.(discovered.id);
255
+ const result = await runOneEval(discovered, {
256
+ baseUrl: options.baseUrl,
257
+ defaultTimeoutMs,
258
+ bearerToken: options.bearerToken,
259
+ headers: options.headers,
260
+ verbose: Boolean(options.verbose),
261
+ signal: options.signal,
262
+ onEvent: (event) => {
263
+ options.onCaseEvent?.(discovered.id, event);
264
+ options.onEvent?.(event);
265
+ },
266
+ });
267
+ options.onCaseDone?.(result);
268
+ results.push(result);
269
+ }
270
+ return results;
271
+ },
272
+ { signal: options.signal }
273
+ );
274
+ return nested.flat();
275
+ }
276
+
277
+ /**
278
+ * Schedule key for {@link groupEvalsForIterationSchedule}: iteration siblings
279
+ * (`weather/nyc/1`…`/3`) share a key; single-iteration ids keep their full id.
280
+ */
281
+ export function iterationScheduleKey(discovered: DiscoveredEval): string {
282
+ const iterations = discovered.definition.iterations ?? 1;
283
+ const iteration = discovered.definition.iteration ?? 1;
284
+ if (iterations <= 1) {
285
+ return discovered.id;
286
+ }
287
+ const suffix = `/${iteration}`;
288
+ if (discovered.id.endsWith(suffix)) {
289
+ return discovered.id.slice(0, -suffix.length);
290
+ }
291
+ return discovered.id;
292
+ }
293
+
294
+ /**
295
+ * Group expanded iteration cases so each authored datapoint's iterations run
296
+ * back-to-back under one {@link mapPool} slot (sorted by iteration index).
297
+ */
298
+ export function groupEvalsForIterationSchedule(
299
+ evals: readonly DiscoveredEval[]
300
+ ): DiscoveredEval[][] {
301
+ const groups = new Map<string, DiscoveredEval[]>();
302
+ const order: string[] = [];
303
+ for (const discovered of evals) {
304
+ const key = iterationScheduleKey(discovered);
305
+ let group = groups.get(key);
306
+ if (group === undefined) {
307
+ group = [];
308
+ groups.set(key, group);
309
+ order.push(key);
239
310
  }
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;
311
+ group.push(discovered);
312
+ }
313
+ return order.map((key) => {
314
+ const group = groups.get(key)!;
315
+ return group.sort(
316
+ (a, b) => (a.definition.iteration ?? 1) - (b.definition.iteration ?? 1)
317
+ );
254
318
  });
255
319
  }
256
320
 
@@ -274,6 +338,7 @@ async function runOneEval(
274
338
  headers?: Record<string, string>;
275
339
  verbose: boolean;
276
340
  onEvent?: (event: SessionEvent) => void;
341
+ signal?: AbortSignal;
277
342
  }
278
343
  ): Promise<EvalRunResult> {
279
344
  const logs: string[] = [];
@@ -288,6 +353,24 @@ async function runOneEval(
288
353
  const inputs: string[] = [];
289
354
  let reply: string | null = null;
290
355
 
356
+ if (opts.signal?.aborted) {
357
+ return {
358
+ id: discovered.id,
359
+ path: discovered.path,
360
+ ...(def.description === undefined
361
+ ? {}
362
+ : { description: def.description }),
363
+ ok: false,
364
+ assertions,
365
+ error: "cancelled",
366
+ logs,
367
+ inputs,
368
+ toolCalls: [],
369
+ tools: [],
370
+ durationMs: Date.now() - startedAt,
371
+ };
372
+ }
373
+
291
374
  const record = (name: string, passed: boolean, detail?: string): void => {
292
375
  assertions.push({
293
376
  name,
@@ -321,6 +404,12 @@ async function runOneEval(
321
404
  get events() {
322
405
  return allEvents;
323
406
  },
407
+ get iteration() {
408
+ return def.iteration ?? 1;
409
+ },
410
+ get iterations() {
411
+ return def.iterations ?? 1;
412
+ },
324
413
  async send(
325
414
  message: string,
326
415
  options: EvalSendOptions = {}
@@ -337,7 +426,9 @@ async function runOneEval(
337
426
  headers: opts.headers,
338
427
  workspaceDir: options.workspaceDir,
339
428
  workspaceFiles: options.workspaceFiles,
429
+ cloud: options.cloud,
340
430
  timeoutMs,
431
+ signal: opts.signal,
341
432
  onEvent: opts.onEvent,
342
433
  });
343
434
  sessionId = result.sessionId;
@@ -472,9 +563,13 @@ function matchExpectation(
472
563
  }
473
564
  case "equals": {
474
565
  const passed = deepEqual(value, assertion.value);
566
+ // Always attach a short detail so values like recall `40.0% (4/10)`
567
+ // show up on PASS lines / playground, not only on failure.
568
+ const rendered =
569
+ typeof value === "string" ? value : JSON.stringify(value);
475
570
  return {
476
571
  passed,
477
- ...(passed ? {} : { detail: `got ${JSON.stringify(value)}` }),
572
+ detail: passed ? rendered : `got ${rendered}`,
478
573
  };
479
574
  }
480
575
  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;
@@ -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> {
@@ -6,7 +6,11 @@
6
6
  * compact {@link Trajectory} plus the continuation token for follow-ups.
7
7
  */
8
8
 
9
- import type { SessionEvent, SessionPurpose } from "../types.js";
9
+ import type {
10
+ AgentCloudOptions,
11
+ SessionEvent,
12
+ SessionPurpose,
13
+ } from "../types.js";
10
14
  import { mergeRequestHeaders, type RequestAuth } from "./request-headers.js";
11
15
  import { buildTrajectory, type Trajectory } from "./trajectory.js";
12
16
 
@@ -24,6 +28,8 @@ export interface RunClientOptions extends RequestAuth {
24
28
  baseUrl: string;
25
29
  /** Abort a hung turn. */
26
30
  timeoutMs?: number;
31
+ /** External cancel (combined with {@link timeoutMs} when both are set). */
32
+ signal?: AbortSignal;
27
33
  /** Called for each stream event as it arrives (for CLI progress). */
28
34
  onEvent?: (event: SessionEvent) => void;
29
35
  /**
@@ -61,6 +67,8 @@ export async function runSession(
61
67
  workspaceDir?: string;
62
68
  /** Applied only when creating a new session (first message). */
63
69
  workspaceFiles?: Record<string, string>;
70
+ /** Applied only when creating a new session (first message). */
71
+ cloud?: AgentCloudOptions;
64
72
  }
65
73
  ): Promise<RunSessionResult> {
66
74
  if (options.messages.length === 0) {
@@ -86,11 +94,13 @@ export async function runSession(
86
94
  startIndex: sessionId === undefined ? 0 : nextEventIndex,
87
95
  auth,
88
96
  timeoutMs: options.timeoutMs,
97
+ signal: options.signal,
89
98
  onEvent: options.onEvent,
90
99
  purpose: sessionId === undefined ? options.purpose : undefined,
91
100
  workspaceDir: sessionId === undefined ? options.workspaceDir : undefined,
92
101
  workspaceFiles:
93
102
  sessionId === undefined ? options.workspaceFiles : undefined,
103
+ cloud: sessionId === undefined ? options.cloud : undefined,
94
104
  });
95
105
  sessionId = turn.sessionId;
96
106
  continuationToken = turn.continuationToken;
@@ -127,9 +137,11 @@ interface OneTurnArgs {
127
137
  purpose?: SessionPurpose;
128
138
  workspaceDir?: string;
129
139
  workspaceFiles?: Record<string, string>;
140
+ cloud?: AgentCloudOptions;
130
141
  startIndex: number;
131
142
  auth?: RequestAuth;
132
143
  timeoutMs?: number;
144
+ signal?: AbortSignal;
133
145
  onEvent?: (event: SessionEvent) => void;
134
146
  }
135
147
 
@@ -155,6 +167,9 @@ async function runOneTurn(args: OneTurnArgs): Promise<OneTurnResult> {
155
167
  if (args.workspaceFiles !== undefined) {
156
168
  body.workspaceFiles = args.workspaceFiles;
157
169
  }
170
+ if (args.cloud !== undefined) {
171
+ body.cloud = args.cloud;
172
+ }
158
173
  const created = await postJson(
159
174
  args.baseUrl,
160
175
  "/v1/session",
@@ -193,6 +208,7 @@ async function runOneTurn(args: OneTurnArgs): Promise<OneTurnResult> {
193
208
  startIndex: args.startIndex,
194
209
  auth: args.auth,
195
210
  timeoutMs: args.timeoutMs,
211
+ signal: args.signal,
196
212
  onEvent: args.onEvent,
197
213
  });
198
214
 
@@ -212,9 +228,20 @@ async function collectStream(args: {
212
228
  startIndex: number;
213
229
  auth?: RequestAuth;
214
230
  timeoutMs?: number;
231
+ signal?: AbortSignal;
215
232
  onEvent?: (event: SessionEvent) => void;
216
233
  }): Promise<SessionEvent[]> {
217
234
  const controller = new AbortController();
235
+ const onExternalAbort = (): void => {
236
+ controller.abort();
237
+ };
238
+ if (args.signal !== undefined) {
239
+ if (args.signal.aborted) {
240
+ controller.abort();
241
+ } else {
242
+ args.signal.addEventListener("abort", onExternalAbort, { once: true });
243
+ }
244
+ }
218
245
  const timer =
219
246
  args.timeoutMs === undefined
220
247
  ? undefined
@@ -271,6 +298,9 @@ async function collectStream(args: {
271
298
  return events;
272
299
  } catch (error) {
273
300
  if (error instanceof Error && error.name === "AbortError") {
301
+ if (args.signal?.aborted) {
302
+ throw new Error("cancelled");
303
+ }
274
304
  throw new RunSessionTimeoutError(
275
305
  `turn timed out after ${args.timeoutMs ?? "?"}ms (session ${args.sessionId})`,
276
306
  args.sessionId
@@ -278,6 +308,7 @@ async function collectStream(args: {
278
308
  }
279
309
  throw error;
280
310
  } finally {
311
+ args.signal?.removeEventListener("abort", onExternalAbort);
281
312
  if (timer !== undefined) {
282
313
  clearTimeout(timer);
283
314
  }