@cursor/july 0.1.5 → 0.1.6

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 (169) hide show
  1. package/dist/ab.d.ts +8 -95
  2. package/dist/ab.d.ts.map +1 -1
  3. package/dist/ab.js +9 -150
  4. package/dist/bin/agent-serve.js +14 -6
  5. package/dist/docs/404.html +2 -2
  6. package/dist/docs/ab.html +4 -4
  7. package/dist/docs/assets/{app.DabPG-io.js → app.DMboGKUD.js} +1 -1
  8. package/dist/docs/assets/chunks/@localSearchIndexroot.O6eP7QZr.js +1 -0
  9. package/dist/docs/assets/chunks/{VPLocalSearchBox.jmyr0bU0.js → VPLocalSearchBox.DkEqEnJU.js} +1 -1
  10. package/dist/docs/assets/chunks/{theme.DysN9-VN.js → theme.ASugdMNm.js} +2 -2
  11. package/dist/docs/assets/deployment.md.BtfEsc9S.js +55 -0
  12. package/dist/docs/assets/deployment.md.BtfEsc9S.lean.js +1 -0
  13. package/dist/docs/assets/example-agents_approval-buddy.md.8R5phXb5.js +10 -0
  14. package/dist/docs/assets/example-agents_approval-buddy.md.8R5phXb5.lean.js +1 -0
  15. package/dist/docs/assets/example-agents_benny.md.B0gjhI-p.js +7 -0
  16. package/dist/docs/assets/example-agents_benny.md.B0gjhI-p.lean.js +1 -0
  17. package/dist/docs/assets/example-agents_bugbot.md.DelIdhxB.js +11 -0
  18. package/dist/docs/assets/example-agents_bugbot.md.DelIdhxB.lean.js +1 -0
  19. package/dist/docs/assets/example-agents_codebase-wiki.md.DC6sgwn0.js +8 -0
  20. package/dist/docs/assets/example-agents_codebase-wiki.md.DC6sgwn0.lean.js +1 -0
  21. package/dist/docs/assets/example-agents_codeowners-review.md.Ku_tG2RY.js +8 -0
  22. package/dist/docs/assets/example-agents_codeowners-review.md.Ku_tG2RY.lean.js +1 -0
  23. package/dist/docs/assets/example-agents_concierge.md.4rQTSMXt.js +23 -0
  24. package/dist/docs/assets/example-agents_concierge.md.4rQTSMXt.lean.js +1 -0
  25. package/dist/docs/assets/example-agents_fsd.md.CzgUrDfi.js +15 -0
  26. package/dist/docs/assets/example-agents_fsd.md.CzgUrDfi.lean.js +1 -0
  27. package/dist/docs/assets/example-agents_index.md.CRqJlnIf.js +2 -0
  28. package/dist/docs/assets/example-agents_index.md.CRqJlnIf.lean.js +1 -0
  29. package/dist/docs/assets/example-agents_knowledge-base.md.BPJiVueF.js +11 -0
  30. package/dist/docs/assets/example-agents_knowledge-base.md.BPJiVueF.lean.js +1 -0
  31. package/dist/docs/assets/example-agents_security-reviewer.md.D2rtwDTO.js +19 -0
  32. package/dist/docs/assets/example-agents_security-reviewer.md.D2rtwDTO.lean.js +1 -0
  33. package/dist/docs/assets/example-agents_slack-agent.md.buLbgvBf.js +5 -0
  34. package/dist/docs/assets/example-agents_slack-agent.md.buLbgvBf.lean.js +1 -0
  35. package/dist/docs/assets/example-agents_weather-agent.md.C9Qv-W0o.js +24 -0
  36. package/dist/docs/assets/example-agents_weather-agent.md.C9Qv-W0o.lean.js +1 -0
  37. package/dist/docs/assets/index.md.COiu-1jL.js +20 -0
  38. package/dist/docs/assets/{index.md.Cylk70gg.lean.js → index.md.COiu-1jL.lean.js} +1 -1
  39. package/dist/docs/assets/reference_cli.md.D189RBCH.js +60 -0
  40. package/dist/docs/assets/reference_cli.md.D189RBCH.lean.js +1 -0
  41. package/dist/docs/building-with-agents.html +4 -4
  42. package/dist/docs/concepts.html +4 -4
  43. package/dist/docs/deployment.html +58 -17
  44. package/dist/docs/evals.html +4 -4
  45. package/dist/docs/example-agents/approval-buddy.html +34 -0
  46. package/dist/docs/example-agents/benny.html +31 -0
  47. package/dist/docs/example-agents/bugbot.html +35 -0
  48. package/dist/docs/example-agents/codebase-wiki.html +32 -0
  49. package/dist/docs/example-agents/codeowners-review.html +32 -0
  50. package/dist/docs/example-agents/concierge.html +47 -0
  51. package/dist/docs/example-agents/fsd.html +39 -0
  52. package/dist/docs/example-agents/index.html +26 -0
  53. package/dist/docs/example-agents/knowledge-base.html +35 -0
  54. package/dist/docs/example-agents/security-reviewer.html +43 -0
  55. package/dist/docs/example-agents/slack-agent.html +29 -0
  56. package/dist/docs/example-agents/weather-agent.html +48 -0
  57. package/dist/docs/guides/agent-to-agent.html +4 -4
  58. package/dist/docs/guides/cloud-runtime.html +5 -5
  59. package/dist/docs/guides/github.html +4 -4
  60. package/dist/docs/guides/human-in-the-loop.html +4 -4
  61. package/dist/docs/guides/slack.html +4 -4
  62. package/dist/docs/guides/webhooks.html +4 -4
  63. package/dist/docs/hashmap.json +1 -1
  64. package/dist/docs/hillclimbing.html +4 -4
  65. package/dist/docs/index.html +7 -7
  66. package/dist/docs/quickstart.html +4 -4
  67. package/dist/docs/reference/agent-config.html +4 -4
  68. package/dist/docs/reference/channels.html +4 -4
  69. package/dist/docs/reference/cli.html +52 -30
  70. package/dist/docs/reference/connections.html +4 -4
  71. package/dist/docs/reference/hooks.html +4 -4
  72. package/dist/docs/reference/http-api.html +4 -4
  73. package/dist/docs/reference/instructions.html +4 -4
  74. package/dist/docs/reference/playground.html +4 -4
  75. package/dist/docs/reference/project-layout.html +4 -4
  76. package/dist/docs/reference/schedules.html +4 -4
  77. package/dist/docs/reference/sessions.html +4 -4
  78. package/dist/docs/reference/skills.html +4 -4
  79. package/dist/docs/reference/subagents.html +4 -4
  80. package/dist/docs/reference/tools.html +4 -4
  81. package/dist/docs/scaffolding-agents.html +4 -4
  82. package/dist/docs/storage.html +4 -4
  83. package/dist/docs/troubleshooting.html +4 -4
  84. package/dist/evals.d.ts +4 -61
  85. package/dist/evals.d.ts.map +1 -1
  86. package/dist/evals.js +3 -66
  87. package/dist/index.d.ts +1 -1
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/internal/ab-collector.d.ts +7 -5
  90. package/dist/internal/ab-collector.d.ts.map +1 -1
  91. package/dist/internal/ab-collector.js +3 -14
  92. package/dist/internal/ab-snapshot.d.ts +2 -4
  93. package/dist/internal/ab-snapshot.d.ts.map +1 -1
  94. package/dist/internal/cli-ax.d.ts +18 -3
  95. package/dist/internal/cli-ax.d.ts.map +1 -1
  96. package/dist/internal/cli-ax.js +80 -42
  97. package/dist/internal/cli-deploy.js +1 -1
  98. package/dist/internal/discovery.js +3 -3
  99. package/dist/internal/eval-run-store.d.ts +33 -30
  100. package/dist/internal/eval-run-store.d.ts.map +1 -1
  101. package/dist/internal/eval-run-store.js +84 -96
  102. package/dist/internal/init-project.d.ts.map +1 -1
  103. package/dist/internal/init-project.js +3 -2
  104. package/dist/internal/playground-proxy.d.ts +5 -5
  105. package/dist/internal/playground-proxy.js +3 -3
  106. package/dist/internal/resolve-prod-target.d.ts +30 -0
  107. package/dist/internal/resolve-prod-target.d.ts.map +1 -1
  108. package/dist/internal/resolve-prod-target.js +74 -2
  109. package/dist/internal/server.d.ts.map +1 -1
  110. package/dist/internal/server.js +5 -3
  111. package/dist/internal/session-engine.d.ts +1 -2
  112. package/dist/internal/session-engine.d.ts.map +1 -1
  113. package/dist/internal/session-engine.js +14 -31
  114. package/dist/internal/storage-coordinator.d.ts +16 -15
  115. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  116. package/dist/internal/storage-coordinator.js +73 -80
  117. package/dist/playground/assets/cursor-icons-16-CQ50JpfO.woff2 +0 -0
  118. package/dist/playground/assets/index-7GJIUFQj.css +1 -0
  119. package/dist/playground/assets/index-mux9aj5P.js +319 -0
  120. package/dist/playground/index.html +2 -2
  121. package/dist/storage.d.ts +51 -10
  122. package/dist/storage.d.ts.map +1 -1
  123. package/dist/storage.js +27 -10
  124. package/docs/README.md +34 -5
  125. package/docs/deployment.md +352 -149
  126. package/docs/example-agents/approval-buddy.md +270 -0
  127. package/docs/example-agents/benny.md +186 -0
  128. package/docs/example-agents/bugbot.md +231 -0
  129. package/docs/example-agents/codebase-wiki.md +174 -0
  130. package/docs/example-agents/codeowners-review.md +195 -0
  131. package/docs/example-agents/concierge.md +205 -0
  132. package/docs/example-agents/fsd.md +330 -0
  133. package/docs/example-agents/index.md +102 -0
  134. package/docs/example-agents/knowledge-base.md +171 -0
  135. package/docs/example-agents/security-reviewer.md +296 -0
  136. package/docs/example-agents/slack-agent.md +146 -0
  137. package/docs/example-agents/weather-agent.md +302 -0
  138. package/docs/reference/cli.md +546 -147
  139. package/package.json +1 -1
  140. package/src/ab.ts +9 -261
  141. package/src/bin/agent-serve.ts +14 -6
  142. package/src/evals.ts +4 -118
  143. package/src/index.ts +2 -0
  144. package/src/internal/ab-collector.ts +12 -22
  145. package/src/internal/ab-snapshot.ts +2 -4
  146. package/src/internal/cli-ax.ts +101 -45
  147. package/src/internal/cli-deploy.ts +1 -1
  148. package/src/internal/discovery.ts +2 -2
  149. package/src/internal/eval-run-store.ts +86 -100
  150. package/src/internal/init-project.ts +3 -2
  151. package/src/internal/playground-proxy.ts +5 -5
  152. package/src/internal/resolve-prod-target.ts +101 -3
  153. package/src/internal/server.ts +5 -3
  154. package/src/internal/session-engine.ts +9 -29
  155. package/src/internal/storage-coordinator.ts +109 -101
  156. package/src/storage.ts +79 -14
  157. package/dist/docs/assets/chunks/@localSearchIndexroot.QwK5BtEH.js +0 -1
  158. package/dist/docs/assets/deployment.md.DTKwE15Z.js +0 -14
  159. package/dist/docs/assets/deployment.md.DTKwE15Z.lean.js +0 -1
  160. package/dist/docs/assets/index.md.Cylk70gg.js +0 -20
  161. package/dist/docs/assets/reference_cli.md.Bv6pOxcF.js +0 -38
  162. package/dist/docs/assets/reference_cli.md.Bv6pOxcF.lean.js +0 -1
  163. package/dist/internal/json-dir-store.d.ts +0 -32
  164. package/dist/internal/json-dir-store.d.ts.map +0 -1
  165. package/dist/internal/json-dir-store.js +0 -100
  166. package/dist/playground/assets/cursor-icons-outline-BxTT_FVJ.woff2 +0 -0
  167. package/dist/playground/assets/index-BEauYlII.css +0 -1
  168. package/dist/playground/assets/index-BtM0wEGg.js +0 -319
  169. package/src/internal/json-dir-store.ts +0 -109
@@ -35,7 +35,12 @@ import { offerInstallCursorSkills } from "./install-cursor-skills.js";
35
35
  import { runLogsTail, sanitizeCustomerError } from "./logs-client.js";
36
36
  import { openBrowser } from "./open-browser.js";
37
37
  import { startPlaygroundProxy } from "./playground-proxy.js";
38
- import { resolveProdSlug, resolveProdTarget } from "./resolve-prod-target.js";
38
+ import {
39
+ ProdTargetError,
40
+ resolveProdAliasPlaygroundUrl,
41
+ resolveProdSlug,
42
+ resolveProdTarget,
43
+ } from "./resolve-prod-target.js";
39
44
  import { runSession } from "./run-client.js";
40
45
  import {
41
46
  fetchSessionEvents,
@@ -92,8 +97,15 @@ export interface AxCliOptions {
92
97
  * Route chat/resume/run/call/eval/logs/sessions at the team's hosted
93
98
  * deployment
94
99
  * instead of a local ephemeral server. Mutually exclusive with `--url`.
100
+ * `eval` targets the deployment by default, so there `--prod` is just
101
+ * being explicit.
95
102
  */
96
103
  prod?: boolean;
104
+ /**
105
+ * `eval`: run against an ephemeral local server instead of the hosted
106
+ * deployment (the eval default). Mutually exclusive with `--url`/`--prod`.
107
+ */
108
+ local?: boolean;
97
109
  /** Cursor team id for `--prod` (defaults to the account's team). */
98
110
  team?: string;
99
111
  /** Cursor API key override for `--prod` management calls. */
@@ -654,6 +666,79 @@ export async function cmdTrajectory(options: AxCliOptions): Promise<number> {
654
666
  return trajectory.ok ? 0 : 1;
655
667
  }
656
668
 
669
+ /**
670
+ * Which surface `eval` should target. Deployment is the default; `--local`
671
+ * opts into an ephemeral local server, `--url` into a running one.
672
+ * Exported for tests.
673
+ */
674
+ export function resolveEvalTargetMode(
675
+ options: Pick<AxCliOptions, "local" | "prod" | "url">
676
+ ): "local" | "url" | "deployment" | { error: string } {
677
+ const flags = [
678
+ options.local === true ? "--local" : undefined,
679
+ options.prod === true ? "--prod" : undefined,
680
+ options.url !== undefined ? "--url" : undefined,
681
+ ].filter((flag) => flag !== undefined);
682
+ if (flags.length > 1) {
683
+ return { error: `${flags.join(" and ")} are mutually exclusive` };
684
+ }
685
+ if (options.local === true) {
686
+ return "local";
687
+ }
688
+ if (options.url !== undefined) {
689
+ return "url";
690
+ }
691
+ return "deployment";
692
+ }
693
+
694
+ /** Resolve the eval target; prints a CLI-ready error and returns an exit code on failure. */
695
+ async function resolveEvalTarget(
696
+ options: AxCliOptions
697
+ ): Promise<ResolvedAgentTarget | number> {
698
+ const mode = resolveEvalTargetMode(options);
699
+ if (typeof mode === "object") {
700
+ process.stderr.write(`${mode.error}\n`);
701
+ return 2;
702
+ }
703
+ if (mode === "url") {
704
+ return { agentUrl: options.url!.replace(/\/$/, "") };
705
+ }
706
+ if (mode === "local") {
707
+ return startEphemeral(options);
708
+ }
709
+ try {
710
+ const prod = await resolveProdTarget({
711
+ dir: options.dir,
712
+ cwd: options.cwd,
713
+ slug: options.slug,
714
+ team: options.team,
715
+ apiKey: options.apiKey,
716
+ fetchImpl: options.fetchImpl,
717
+ });
718
+ return { agentUrl: prod.agentUrl, headers: prod.headers };
719
+ } catch (error) {
720
+ if (error instanceof ProdTargetError && error.notFound) {
721
+ // Keep the API error text: a 404 can also mean a wrong API host or a
722
+ // disabled feature gate, and the message distinguishes those from a
723
+ // genuinely missing deployment.
724
+ process.stderr.write(
725
+ `${sanitizeCustomerError(error.message)}\n` +
726
+ `Evals run on the hosted deployment by default.\n` +
727
+ ` deploy it first: ${CLI_COMMAND_NAME} deploy --slug ${error.slug}\n` +
728
+ ` or run locally: ${CLI_COMMAND_NAME} eval --local --dir ${options.dir}\n`
729
+ );
730
+ return 1;
731
+ }
732
+ process.stderr.write(
733
+ `${sanitizeCustomerError(
734
+ error instanceof Error ? error.message : String(error)
735
+ )}\n` +
736
+ `Evals run on the hosted deployment by default; pass --local to run on an ephemeral local server.\n`
737
+ );
738
+ return 1;
739
+ }
740
+ }
741
+
657
742
  export async function cmdEval(options: AxCliOptions): Promise<number> {
658
743
  const projectRoot = resolve(options.cwd ?? process.cwd(), options.dir);
659
744
 
@@ -695,7 +780,10 @@ export async function cmdEval(options: AxCliOptions): Promise<number> {
695
780
  return 0;
696
781
  }
697
782
 
698
- const target = await resolveTarget(options);
783
+ const target = await resolveEvalTarget(options);
784
+ if (typeof target === "number") {
785
+ return target;
786
+ }
699
787
  const stream = shouldStreamProgress(options);
700
788
  const progress = stream ? createStreamProgress() : undefined;
701
789
  try {
@@ -991,9 +1079,9 @@ export async function cmdSession(
991
1079
  /**
992
1080
  * `playground` — open the web UI against local / `--url` / `--prod`.
993
1081
  *
994
- * Hosted engines need short-lived access headers a browser cannot set, so
995
- * `--prod` (and `--bearer-token`) run a loopback reverse proxy that injects
996
- * them. Ctrl-C stops the proxy.
1082
+ * `--prod` opens the stable hosted alias playground URL; the SPA collects
1083
+ * the alias token. `--bearer-token` (and other injected headers) still run a
1084
+ * loopback reverse proxy — Ctrl-C stops that proxy.
997
1085
  */
998
1086
  export async function cmdPlayground(options: AxCliOptions): Promise<number> {
999
1087
  if (options.prod === true && options.url !== undefined) {
@@ -1006,17 +1094,18 @@ export async function cmdPlayground(options: AxCliOptions): Promise<number> {
1006
1094
  ? options.sessionId.trim()
1007
1095
  : undefined;
1008
1096
 
1009
- // Hosted: mint engineAccess and refresh it for the life of the proxy.
1097
+ // Hosted: open the alias playground; SPA owns alias-token sign-in.
1010
1098
  if (options.prod === true) {
1011
- let initial: Awaited<ReturnType<typeof resolveProdTarget>>;
1099
+ let resolved: Awaited<ReturnType<typeof resolveProdAliasPlaygroundUrl>>;
1012
1100
  try {
1013
- initial = await resolveProdTarget({
1101
+ resolved = await resolveProdAliasPlaygroundUrl({
1014
1102
  dir: options.dir,
1015
1103
  cwd: options.cwd,
1016
1104
  slug: options.slug,
1017
1105
  team: options.team,
1018
1106
  apiKey: options.apiKey,
1019
1107
  fetchImpl: options.fetchImpl,
1108
+ sessionId,
1020
1109
  });
1021
1110
  } catch (error) {
1022
1111
  process.stderr.write(
@@ -1027,44 +1116,11 @@ export async function cmdPlayground(options: AxCliOptions): Promise<number> {
1027
1116
  return 1;
1028
1117
  }
1029
1118
 
1030
- let proxy: Awaited<ReturnType<typeof startPlaygroundProxy>>;
1031
- try {
1032
- proxy = await startPlaygroundProxy({
1033
- upstreamUrl: initial.agentUrl,
1034
- headers: initial.headers,
1035
- expiresAt: initial.expiresAt,
1036
- refreshUpstream: async () => {
1037
- const next = await resolveProdTarget({
1038
- dir: options.dir,
1039
- cwd: options.cwd,
1040
- slug: options.slug ?? initial.slug,
1041
- team: options.team,
1042
- apiKey: options.apiKey,
1043
- fetchImpl: options.fetchImpl,
1044
- err: () => {},
1045
- });
1046
- return {
1047
- upstreamUrl: next.agentUrl,
1048
- headers: next.headers,
1049
- expiresAt: next.expiresAt,
1050
- };
1051
- },
1052
- });
1053
- } catch (error) {
1054
- process.stderr.write(
1055
- `${sanitizeCustomerError(
1056
- error instanceof Error ? error.message : String(error)
1057
- )}\n`
1058
- );
1059
- return 1;
1119
+ process.stdout.write(`${resolved.playgroundUrl}\n`);
1120
+ if (options.print !== true) {
1121
+ openBrowser(resolved.playgroundUrl);
1060
1122
  }
1061
-
1062
- return await runPlaygroundProxySession({
1063
- proxy,
1064
- sessionId,
1065
- print: options.print === true,
1066
- signal: options.signal,
1067
- });
1123
+ return 0;
1068
1124
  }
1069
1125
 
1070
1126
  const target = await resolveSessionTarget(options);
@@ -675,7 +675,7 @@ async function deployOne(
675
675
  ` kind: ${detail.deploymentKind}\n` +
676
676
  ` alias: ${aliasUrl}\n` +
677
677
  ` playground: ${aliasUrl}/playground (sign in with the alias token)\n` +
678
- ` or: ${CLI} playground --prod --slug ${slug} --team ${ctx.teamId}\n`
678
+ ` open: ${CLI} playground --prod --slug ${slug} --team ${ctx.teamId}\n`
679
679
  );
680
680
  return 0;
681
681
  }
@@ -1611,8 +1611,8 @@ export function projectInfo(project: AgentProject): AgentProjectInfo {
1611
1611
  maxPlaygroundSessions: resolveABMaxPlaygroundSessions(
1612
1612
  project.abConfig?.maxPlaygroundSessions
1613
1613
  ),
1614
- durableSamples: project.abConfig?.persistSamples !== undefined,
1615
- durableSnapshots: project.abConfig?.persistSnapshots !== undefined,
1614
+ durableSamples: project.storage?.abs !== undefined,
1615
+ durableSnapshots: project.storage?.abs?.putSnapshot !== undefined,
1616
1616
  },
1617
1617
  }),
1618
1618
  storage:
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Store for playground-triggered eval batches.
3
3
  *
4
- * Default: process memory only, capped by `maxPlaygroundRuns` (see `defineEvalConfig`).
5
- * Optional durable `persistRuns` from `evals/evals.config.ts` survives restart.
4
+ * Default: process memory only, capped by `maxPlaygroundRuns` (see
5
+ * `defineEvalConfig`). When `agent/storage.ts` (`defineStorage`) declares an
6
+ * `evals` table, runs are mirrored there and survive restart.
6
7
  * Runs against the live mount's loopback URL so case sessions appear in
7
8
  * the playground session list.
8
9
  */
@@ -11,7 +12,6 @@ import { randomUUID } from "node:crypto";
11
12
  import {
12
13
  type EvalCaseSnapshot,
13
14
  type EvalConfigFile,
14
- type EvalRunPersistence,
15
15
  type EvalRunSnapshot,
16
16
  resolveEvalMaxPlaygroundRuns,
17
17
  } from "../evals.js";
@@ -30,6 +30,18 @@ export type {
30
30
  EvalRunSnapshot,
31
31
  } from "../evals.js";
32
32
 
33
+ /**
34
+ * Durable mirror for playground eval batches — the `StorageCoordinator`
35
+ * adapter over the `defineStorage` `evals` table. Writes are
36
+ * fire-and-forget (the coordinator serializes and isolates failures);
37
+ * `list` is the serve-start hydrate read.
38
+ */
39
+ export interface EvalRunStorage {
40
+ save(run: EvalRunSnapshot): void;
41
+ delete(runId: string): void;
42
+ list(): Promise<EvalRunSnapshot[]>;
43
+ }
44
+
33
45
  export interface StartEvalRunInput {
34
46
  filterIds?: string[];
35
47
  tags?: string[];
@@ -41,7 +53,7 @@ export type EvalListConfig = {
41
53
  maxConcurrency?: number;
42
54
  timeoutMs?: number;
43
55
  maxPlaygroundRuns: number;
44
- /** True when `evals.config.ts` set `persistRuns`. */
56
+ /** True when `agent/storage.ts` declares an `evals` table. */
45
57
  durableRuns: boolean;
46
58
  };
47
59
 
@@ -55,28 +67,16 @@ export class EvalRunStore {
55
67
  private activeRunId: string | undefined;
56
68
  private hydratePromise: Promise<void> | undefined;
57
69
  private hydrated = false;
58
- /** Per-run persist chain so intermediate saves cannot overwrite a later final write. */
59
- private readonly persistChains = new Map<string, Promise<void>>();
60
70
  /** Filled after listen — e.g. `http://127.0.0.1:3000/weather-agent`. */
61
71
  loopbackUrl: string | undefined;
62
72
 
63
73
  constructor(
64
74
  private readonly projectRoot: string,
65
75
  private readonly logger: (line: string) => void,
66
- /**
67
- * Project-wide sink from `agent/storage.ts` (`defineStorage`),
68
- * used when `evals.config.ts` sets no `persistRuns` of its own.
69
- */
70
- private readonly fallbackPersistence?: EvalRunPersistence
76
+ /** `defineStorage` evals-table adapter; undefined ⇒ memory only. */
77
+ private readonly storage?: EvalRunStorage | undefined
71
78
  ) {}
72
79
 
73
- /** `evals.config.ts` `persistRuns` wins; `defineStorage` is fallback. */
74
- private resolvePersist(
75
- config: Partial<EvalConfigFile>
76
- ): EvalRunPersistence | undefined {
77
- return config.persistRuns ?? this.fallbackPersistence;
78
- }
79
-
80
80
  async list(): Promise<{
81
81
  evals: Array<{
82
82
  id: string;
@@ -97,10 +97,7 @@ export class EvalRunStore {
97
97
  : { description: e.definition.description }),
98
98
  ...(e.definition.tags === undefined ? {} : { tags: e.definition.tags }),
99
99
  })),
100
- config: snapshotListConfig(
101
- config,
102
- this.resolvePersist(config) !== undefined
103
- ),
100
+ config: snapshotListConfig(config, this.storage !== undefined),
104
101
  };
105
102
  }
106
103
 
@@ -175,15 +172,12 @@ export class EvalRunStore {
175
172
  done: 0,
176
173
  },
177
174
  cases,
178
- config: snapshotRunConfig(
179
- config,
180
- this.resolvePersist(config) !== undefined
181
- ),
175
+ config: snapshotRunConfig(config, this.storage !== undefined),
182
176
  };
183
177
  this.runs.set(runId, snapshot);
184
178
  this.activeRunId = runId;
185
- await this.pruneOldRuns(config);
186
- await this.enqueuePersist(snapshot, this.resolvePersist(config));
179
+ this.pruneOldRuns(config);
180
+ this.persist(snapshot);
187
181
  this.logger(
188
182
  `[agentkit] eval run ${runId}: starting ${cases.length} case(s)`
189
183
  );
@@ -208,39 +202,28 @@ export class EvalRunStore {
208
202
  }
209
203
 
210
204
  /**
211
- * Hydrate playground eval history from durable persistence
212
- * (`evals.config.ts` `persistRuns`, else the `defineStorage`
213
- * fallback). Called eagerly from the serve startup restore step
214
- * alongside session restore and lazily as a safety net by every read.
215
- * Idempotent; concurrent callers share one pass.
205
+ * Hydrate playground eval history from the `defineStorage` evals table.
206
+ * Called eagerly from the serve startup restore step — alongside session
207
+ * restore and lazily as a safety net by every read. Idempotent;
208
+ * concurrent callers share one pass.
216
209
  */
217
210
  async hydrate(): Promise<void> {
218
211
  if (this.hydrated) {
219
212
  return;
220
213
  }
221
214
  if (this.hydratePromise === undefined) {
222
- this.hydratePromise = this.hydrateFromPersistence();
215
+ this.hydratePromise = this.hydrateFromStorage();
223
216
  }
224
217
  await this.hydratePromise;
225
218
  }
226
219
 
227
- private async hydrateFromPersistence(): Promise<void> {
220
+ private async hydrateFromStorage(): Promise<void> {
228
221
  try {
229
- const { config } = await discoverEvals(this.projectRoot);
230
- const persist = this.resolvePersist(config);
231
- if (persist === undefined) {
232
- this.hydrated = true;
233
- return;
234
- }
235
- const loaded = await persist.load({ projectRoot: this.projectRoot });
236
- if (!Array.isArray(loaded)) {
237
- this.logger(
238
- "[agentkit] eval persistRuns.load() did not return an array; ignoring"
239
- );
222
+ if (this.storage === undefined) {
240
223
  this.hydrated = true;
241
224
  return;
242
225
  }
243
- const repaired: EvalRunSnapshot[] = [];
226
+ const loaded = await this.storage.list();
244
227
  for (const run of loaded) {
245
228
  if (
246
229
  run === null ||
@@ -253,16 +236,26 @@ export class EvalRunStore {
253
236
  // Incomplete batches from a killed process should not block a new run.
254
237
  if (run.status === "running") {
255
238
  markInterruptedRun(run);
256
- repaired.push(run);
239
+ this.persist(run);
257
240
  }
258
241
  this.runs.set(run.runId, run);
259
242
  }
260
- for (const run of repaired) {
261
- await this.enqueuePersist(run, persist);
243
+ // Prune with the defaults even when eval discovery fails — loaded
244
+ // history must not stay unpruned for the process lifetime just
245
+ // because an eval file has a syntax error at boot.
246
+ let config: Partial<EvalConfigFile> = {};
247
+ try {
248
+ config = (await discoverEvals(this.projectRoot)).config;
249
+ } catch (error) {
250
+ this.logger(
251
+ `[agentkit] eval discovery failed during hydrate (pruning with defaults): ${
252
+ error instanceof Error ? error.message : String(error)
253
+ }`
254
+ );
262
255
  }
263
- await this.pruneOldRuns(config);
256
+ this.pruneOldRuns(config);
264
257
  this.logger(
265
- `[agentkit] hydrated ${this.runs.size} playground eval run(s) from persistRuns`
258
+ `[agentkit] hydrated ${this.runs.size} playground eval run(s) from storage`
266
259
  );
267
260
  } catch (error) {
268
261
  const message = error instanceof Error ? error.message : String(error);
@@ -274,7 +267,7 @@ export class EvalRunStore {
274
267
  }
275
268
  }
276
269
 
277
- private async pruneOldRuns(config: Partial<EvalConfigFile>): Promise<void> {
270
+ private pruneOldRuns(config: Partial<EvalConfigFile>): void {
278
271
  const limit = resolveEvalMaxPlaygroundRuns(config.maxPlaygroundRuns);
279
272
  const ordered = this.sortedRuns();
280
273
  if (ordered.length <= limit) {
@@ -285,58 +278,34 @@ export class EvalRunStore {
285
278
  continue;
286
279
  }
287
280
  this.runs.delete(stale.runId);
288
- await this.persistDelete(stale.runId, this.resolvePersist(config));
289
- }
290
- }
291
-
292
- /**
293
- * Queue a durable save for `run`. Snapshots are cloned at enqueue time and
294
- * writes for the same runId run strictly in order so an intermediate
295
- * fire-and-forget save cannot finish after (and overwrite) a later final save.
296
- */
297
- private enqueuePersist(
298
- run: EvalRunSnapshot,
299
- persist: EvalRunPersistence | undefined
300
- ): Promise<void> {
301
- if (persist === undefined) {
302
- return Promise.resolve();
281
+ this.storage?.delete(stale.runId);
303
282
  }
304
- const snapshot = structuredClone(run);
305
- const prev = this.persistChains.get(run.runId) ?? Promise.resolve();
306
- const next = prev
307
- .catch(() => undefined)
308
- .then(() => this.persistSave(snapshot, persist));
309
- this.persistChains.set(run.runId, next);
310
- return next;
311
283
  }
312
284
 
313
- private async persistSave(
314
- run: EvalRunSnapshot,
315
- persist: EvalRunPersistence
316
- ): Promise<void> {
317
- try {
318
- await persist.save(run, { projectRoot: this.projectRoot });
319
- } catch (error) {
320
- const message = error instanceof Error ? error.message : String(error);
321
- this.logger(
322
- `[agentkit] persistRuns.save(${run.runId}) failed: ${message}`
323
- );
324
- }
285
+ /** Mirror the run onto durable storage (coordinator clones + serializes). */
286
+ private persist(run: EvalRunSnapshot): void {
287
+ this.storage?.save(run);
325
288
  }
326
289
 
327
- private async persistDelete(
328
- runId: string,
329
- persist: EvalRunPersistence | undefined
330
- ): Promise<void> {
331
- if (persist === undefined) {
290
+ /**
291
+ * Serve is draining: durably mark any in-flight batch as interrupted
292
+ * while the storage queue can still deliver. Late `execute` writes are
293
+ * dropped by the closed coordinator, so without this a batch that was
294
+ * running at shutdown would look "running" (or silently lose its final
295
+ * completion write) until the next hydrate repairs it.
296
+ */
297
+ close(): void {
298
+ const activeRunId = this.activeRunId;
299
+ if (activeRunId === undefined) {
332
300
  return;
333
301
  }
334
- try {
335
- await persist.delete(runId, { projectRoot: this.projectRoot });
336
- } catch (error) {
337
- const message = error instanceof Error ? error.message : String(error);
338
- this.logger(`[agentkit] persistRuns.delete(${runId}) failed: ${message}`);
302
+ const run = this.runs.get(activeRunId);
303
+ if (run === undefined || run.status !== "running") {
304
+ return;
339
305
  }
306
+ markInterruptedRun(run);
307
+ this.persist(run);
308
+ this.activeRunId = undefined;
340
309
  }
341
310
 
342
311
  private async execute(
@@ -408,7 +377,7 @@ export class EvalRunStore {
408
377
  this.applyCaseResult(runId, result);
409
378
  const run = this.runs.get(runId);
410
379
  if (run !== undefined) {
411
- void this.enqueuePersist(run, this.resolvePersist(opts.config));
380
+ this.persist(run);
412
381
  }
413
382
  },
414
383
  });
@@ -418,7 +387,7 @@ export class EvalRunStore {
418
387
  }
419
388
  run.status = "completed";
420
389
  run.finishedAt = new Date().toISOString();
421
- await this.enqueuePersist(run, this.resolvePersist(opts.config));
390
+ this.persist(run);
422
391
  this.logger(
423
392
  `[agentkit] eval run ${runId}: done — ${run.summary.passed} passed, ${run.summary.failed} failed`
424
393
  );
@@ -430,7 +399,7 @@ export class EvalRunStore {
430
399
  run.status = "failed";
431
400
  run.finishedAt = new Date().toISOString();
432
401
  run.error = error instanceof Error ? error.message : String(error);
433
- await this.enqueuePersist(run, this.resolvePersist(opts.config));
402
+ this.persist(run);
434
403
  this.logger(`[agentkit] eval run ${runId}: failed — ${run.error}`);
435
404
  } finally {
436
405
  if (this.activeRunId === runId) {
@@ -489,6 +458,23 @@ function markInterruptedRun(run: EvalRunSnapshot): void {
489
458
  c.error = c.error ?? INTERRUPTED_CASE_ERROR;
490
459
  }
491
460
  }
461
+ // Reconcile the summary with the repaired case rows so a persisted
462
+ // interrupted batch never claims pre-interruption pass counts.
463
+ let passed = 0;
464
+ let failed = 0;
465
+ for (const c of run.cases) {
466
+ if (c.ok === true) {
467
+ passed += 1;
468
+ } else {
469
+ failed += 1;
470
+ }
471
+ }
472
+ run.summary = {
473
+ passed,
474
+ failed,
475
+ total: run.cases.length,
476
+ done: run.cases.length,
477
+ };
492
478
  }
493
479
 
494
480
  function snapshotListConfig(
@@ -433,14 +433,15 @@ Recommended loop once the project exists:
433
433
  1. Edit agent/ + tools under agent/tools/
434
434
  2. ${CLI_COMMAND_NAME} info --dir . --json
435
435
  3. ${CLI_COMMAND_NAME} run --dir . --message "..."
436
- 4. ${CLI_COMMAND_NAME} eval --dir .
436
+ 4. ${CLI_COMMAND_NAME} eval --local --dir .
437
437
 
438
438
  Evals:
439
439
  Create evals/**/*.eval.ts using defineEval from ${PACKAGE_NAME}/evals
440
440
  Case id is the path: evals/weather/nyc.eval.ts → weather/nyc
441
441
  Required: evals/evals.config.ts with defineEvalConfig({ maxConcurrency })
442
442
  (hard-capped at 200 due to model provider request limits)
443
- Run: ${CLI_COMMAND_NAME} eval [--json] [evalId...]
443
+ Run: ${CLI_COMMAND_NAME} eval --local [--json] [evalId...]
444
+ (omit --local to run against the hosted deployment)
444
445
 
445
446
  See AGENTS.md in the ${PACKAGE_NAME} package for the full loop.
446
447
  `);
@@ -14,9 +14,9 @@
14
14
  * request-targets are rejected so injected headers never leave the configured
15
15
  * upstream.
16
16
  *
17
- * When `refreshUpstream` is provided (hosted `--prod`), credentials are
18
- * re-minted shortly before `expiresAt` and once on upstream 401 so a long
19
- * playground session does not require restarting the CLI.
17
+ * When `refreshUpstream` is provided (e.g. short-lived upstream tokens),
18
+ * credentials are re-minted shortly before `expiresAt` and once on upstream
19
+ * 401 so a long playground session does not require restarting the CLI.
20
20
  */
21
21
 
22
22
  import { randomBytes } from "node:crypto";
@@ -84,8 +84,8 @@ export interface StartPlaygroundProxyArgs {
84
84
  /** Initial engineAccess expiry (ISO); enables proactive refresh. */
85
85
  expiresAt?: string;
86
86
  /**
87
- * Re-mint upstream URL + headers (hosted `--prod`). Single-flighted across
88
- * concurrent browser requests; called near expiry and once on 401.
87
+ * Re-mint upstream URL + headers. Single-flighted across concurrent browser
88
+ * requests; called near expiry and once on 401.
89
89
  */
90
90
  refreshUpstream?: () => Promise<PlaygroundUpstreamAccess>;
91
91
  /** Bind port; `0` = ephemeral. */