@cursor/july 0.1.12 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/agent-serve.js +32 -9
- package/dist/channels/github/api.d.ts.map +1 -1
- package/dist/channels/github/api.js +11 -4
- package/dist/channels/github/cursor-account.d.ts +21 -1
- package/dist/channels/github/cursor-account.d.ts.map +1 -1
- package/dist/channels/github/cursor-account.js +29 -1
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +1 -1
- package/dist/channels/github/instrument.d.ts +20 -0
- package/dist/channels/github/instrument.d.ts.map +1 -0
- package/dist/channels/slack/api.d.ts +9 -0
- package/dist/channels/slack/api.d.ts.map +1 -1
- package/dist/channels/slack/api.js +40 -7
- package/dist/channels/slack/eval-directive.d.ts +3 -0
- package/dist/channels/slack/eval-directive.d.ts.map +1 -1
- package/dist/channels/slack/eval-directive.js +20 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.Dh_JkJjI.js → app.CKFFLL4Z.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DGWkvXJc.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.CptQNLkR.js → VPLocalSearchBox.BXk19JBN.js} +1 -1
- package/dist/docs/assets/chunks/{theme.DWflH-LQ.js → theme.BXSPBMyn.js} +2 -2
- package/dist/docs/assets/{evals.md.DAgEc_hL.js → evals.md.DYOjkRCX.js} +10 -4
- package/dist/docs/assets/{evals.md.DAgEc_hL.lean.js → evals.md.DYOjkRCX.lean.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.B74NrVzb.js → reference_cli.md.DnYfr5V2.js} +4 -1
- package/dist/docs/assets/{reference_http-api.md.Dx_nmDG6.js → reference_http-api.md.BncLd3PZ.js} +2 -2
- package/dist/docs/assets/{reference_http-api.md.Dx_nmDG6.lean.js → reference_http-api.md.BncLd3PZ.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +13 -7
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +8 -5
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +5 -5
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/evals.d.ts +41 -2
- package/dist/evals.d.ts.map +1 -1
- package/dist/evals.js +53 -16
- package/dist/internal/cli-ax.d.ts +4 -0
- package/dist/internal/cli-ax.d.ts.map +1 -1
- package/dist/internal/cli-ax.js +163 -33
- package/dist/internal/cli-deploy.d.ts +8 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +92 -12
- package/dist/internal/cli-mcp-oauth.d.ts +14 -2
- package/dist/internal/cli-mcp-oauth.d.ts.map +1 -1
- package/dist/internal/cli-mcp-oauth.js +95 -2
- package/dist/internal/cursor/backend-client.d.ts +34 -0
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +73 -9
- package/dist/internal/cursor/credentials.d.ts +7 -1
- package/dist/internal/cursor/credentials.d.ts.map +1 -1
- package/dist/internal/cursor/credentials.js +9 -3
- package/dist/internal/cursor-account-mcp-auth.d.ts +55 -0
- package/dist/internal/cursor-account-mcp-auth.d.ts.map +1 -0
- package/dist/internal/cursor-account-mcp-auth.js +175 -0
- package/dist/internal/deploy-client.d.ts +6 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +13 -0
- package/dist/internal/eval-run-store.d.ts +15 -0
- package/dist/internal/eval-run-store.d.ts.map +1 -1
- package/dist/internal/eval-run-store.js +87 -8
- package/dist/internal/eval-runner.d.ts +12 -0
- package/dist/internal/eval-runner.d.ts.map +1 -1
- package/dist/internal/eval-runner.js +97 -22
- package/dist/internal/evals-client.d.ts +9 -1
- package/dist/internal/evals-client.d.ts.map +1 -1
- package/dist/internal/evals-client.js +43 -0
- package/dist/internal/http-channel.d.ts.map +1 -1
- package/dist/internal/http-channel.js +74 -7
- package/dist/internal/map-pool.d.ts +6 -1
- package/dist/internal/map-pool.d.ts.map +1 -1
- package/dist/internal/map-pool.js +8 -1
- package/dist/internal/mcp-host.d.ts +13 -0
- package/dist/internal/mcp-host.d.ts.map +1 -1
- package/dist/internal/mcp-host.js +68 -13
- package/dist/internal/run-client.d.ts +5 -1
- package/dist/internal/run-client.d.ts.map +1 -1
- package/dist/internal/run-client.js +26 -5
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +57 -4
- package/dist/internal/session-engine.d.ts +13 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +49 -0
- package/dist/internal/storage-coordinator.d.ts +7 -0
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +29 -4
- package/dist/playground/assets/index-CuXb4j4V.css +1 -0
- package/dist/playground/assets/{index-1dzluIcW.js → index-DeyYpOit.js} +46 -46
- package/dist/playground/index.html +2 -2
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/evals.md +46 -11
- package/docs/reference/cli.md +3 -0
- package/docs/reference/http-api.md +2 -1
- package/package.json +1 -1
- package/src/bin/agent-serve.ts +38 -8
- package/src/channels/github/api.ts +12 -1
- package/src/channels/github/cursor-account.ts +51 -1
- package/src/channels/github/github-channel.ts +4 -1
- package/src/channels/slack/api.ts +57 -8
- package/src/channels/slack/eval-directive.ts +24 -1
- package/src/evals.ts +104 -25
- package/src/internal/cli-ax.ts +192 -16
- package/src/internal/cli-deploy.ts +140 -13
- package/src/internal/cli-mcp-oauth.ts +122 -3
- package/src/internal/cursor/backend-client.ts +104 -1
- package/src/internal/cursor/credentials.ts +10 -2
- package/src/internal/cursor-account-mcp-auth.ts +241 -0
- package/src/internal/deploy-client.ts +16 -0
- package/src/internal/eval-run-store.ts +89 -6
- package/src/internal/eval-runner.ts +113 -18
- package/src/internal/evals-client.ts +81 -0
- package/src/internal/http-channel.ts +87 -6
- package/src/internal/map-pool.ts +8 -1
- package/src/internal/mcp-host.ts +74 -16
- package/src/internal/run-client.ts +32 -1
- package/src/internal/server.ts +69 -2
- package/src/internal/session-engine.ts +58 -0
- package/src/internal/storage-coordinator.ts +32 -6
- package/src/types.ts +10 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.Er_R50vY.js +0 -1
- package/dist/playground/assets/index-kZjeJs1v.css +0 -1
- /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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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> {
|
package/src/internal/map-pool.ts
CHANGED
|
@@ -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) {
|
package/src/internal/mcp-host.ts
CHANGED
|
@@ -52,17 +52,18 @@ export class McpHost implements HostMcpRegistry {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
async listTools(connectionName: string): Promise<HostMcpToolInfo[]> {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 {
|
|
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
|
}
|