@cursor/july 0.1.16 → 0.1.17

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 (236) hide show
  1. package/README.md +9 -4
  2. package/dist/bin/agent-serve.js +60 -2
  3. package/dist/channels/slack/api.d.ts.map +1 -1
  4. package/dist/channels/slack/api.js +13 -3
  5. package/dist/channels/slack/auth.d.ts +7 -1
  6. package/dist/channels/slack/auth.d.ts.map +1 -1
  7. package/dist/channels/slack/auth.js +14 -0
  8. package/dist/channels/slack/blocks.d.ts +61 -0
  9. package/dist/channels/slack/blocks.d.ts.map +1 -0
  10. package/dist/channels/slack/blocks.js +90 -0
  11. package/dist/channels/slack/defaults.d.ts +2 -0
  12. package/dist/channels/slack/defaults.d.ts.map +1 -1
  13. package/dist/channels/slack/defaults.js +29 -3
  14. package/dist/channels/slack/dispatch.d.ts +3 -17
  15. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  16. package/dist/channels/slack/index.d.ts +1 -0
  17. package/dist/channels/slack/index.d.ts.map +1 -1
  18. package/dist/channels/slack/index.js +1 -0
  19. package/dist/channels/slack/interactive.d.ts +39 -3
  20. package/dist/channels/slack/interactive.d.ts.map +1 -1
  21. package/dist/channels/slack/interactive.js +152 -7
  22. package/dist/channels/slack/manifest.d.ts +3 -0
  23. package/dist/channels/slack/manifest.d.ts.map +1 -1
  24. package/dist/channels/slack/manifest.js +3 -0
  25. package/dist/channels/slack/setup.d.ts +4 -1
  26. package/dist/channels/slack/setup.d.ts.map +1 -1
  27. package/dist/channels/slack/setup.js +16 -2
  28. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  29. package/dist/channels/slack/slack-channel.js +24 -3
  30. package/dist/channels/slack/types.d.ts +77 -0
  31. package/dist/channels/slack/types.d.ts.map +1 -1
  32. package/dist/connections.d.ts +4 -1
  33. package/dist/connections.d.ts.map +1 -1
  34. package/dist/connections.js +4 -1
  35. package/dist/docs/404.html +2 -2
  36. package/dist/docs/ab.html +3 -3
  37. package/dist/docs/assets/{app.DjHC-Ggf.js → app.BbEXr2KO.js} +1 -1
  38. package/dist/docs/assets/chunks/@localSearchIndexroot.DCBsrfwP.js +1 -0
  39. package/dist/docs/assets/chunks/{VPLocalSearchBox.DwviwmIq.js → VPLocalSearchBox.DBEi4HDp.js} +1 -1
  40. package/dist/docs/assets/chunks/{theme.BIpeYSon.js → theme.DGfptcHw.js} +2 -2
  41. package/dist/docs/assets/{guides_mcp-oauth.md.DqmEOQY1.js → guides_mcp-oauth.md.Dd8EgSem.js} +3 -2
  42. package/dist/docs/assets/{guides_mcp-oauth.md.DqmEOQY1.lean.js → guides_mcp-oauth.md.Dd8EgSem.lean.js} +1 -1
  43. package/dist/docs/assets/{guides_slack.md.FWale6Ip.js → guides_slack.md.D_xFKlnF.js} +1 -1
  44. package/dist/docs/assets/{reference_connections.md.CsSr3rDz.js → reference_connections.md.B9Q3TOve.js} +12 -5
  45. package/dist/docs/assets/{reference_connections.md.CsSr3rDz.lean.js → reference_connections.md.B9Q3TOve.lean.js} +1 -1
  46. package/dist/docs/building-with-agents.html +3 -3
  47. package/dist/docs/concepts.html +3 -3
  48. package/dist/docs/deployment.html +3 -3
  49. package/dist/docs/evals.html +3 -3
  50. package/dist/docs/example-agents/approval-buddy.html +3 -3
  51. package/dist/docs/example-agents/benny.html +3 -3
  52. package/dist/docs/example-agents/bugbot.html +3 -3
  53. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  54. package/dist/docs/example-agents/codeowners-review.html +3 -3
  55. package/dist/docs/example-agents/concierge.html +3 -3
  56. package/dist/docs/example-agents/fsd.html +3 -3
  57. package/dist/docs/example-agents/index.html +3 -3
  58. package/dist/docs/example-agents/knowledge-base.html +3 -3
  59. package/dist/docs/example-agents/oncall.html +3 -3
  60. package/dist/docs/example-agents/security-reviewer.html +3 -3
  61. package/dist/docs/example-agents/slack-agent.html +3 -3
  62. package/dist/docs/example-agents/weather-agent.html +3 -3
  63. package/dist/docs/guides/agent-to-agent.html +3 -3
  64. package/dist/docs/guides/cloud-runtime.html +3 -3
  65. package/dist/docs/guides/github.html +3 -3
  66. package/dist/docs/guides/human-in-the-loop.html +3 -3
  67. package/dist/docs/guides/mcp-oauth.html +6 -5
  68. package/dist/docs/guides/slack.html +5 -5
  69. package/dist/docs/guides/webhooks.html +3 -3
  70. package/dist/docs/hashmap.json +1 -1
  71. package/dist/docs/hillclimbing.html +3 -3
  72. package/dist/docs/index.html +3 -3
  73. package/dist/docs/quickstart.html +3 -3
  74. package/dist/docs/reference/agent-config.html +3 -3
  75. package/dist/docs/reference/channels.html +3 -3
  76. package/dist/docs/reference/cli.html +3 -3
  77. package/dist/docs/reference/connections.html +14 -7
  78. package/dist/docs/reference/hooks.html +3 -3
  79. package/dist/docs/reference/http-api.html +3 -3
  80. package/dist/docs/reference/instructions.html +3 -3
  81. package/dist/docs/reference/playground.html +3 -3
  82. package/dist/docs/reference/project-layout.html +3 -3
  83. package/dist/docs/reference/prompt.html +3 -3
  84. package/dist/docs/reference/schedules.html +3 -3
  85. package/dist/docs/reference/sessions.html +3 -3
  86. package/dist/docs/reference/skills.html +3 -3
  87. package/dist/docs/reference/subagents.html +3 -3
  88. package/dist/docs/reference/tools.html +3 -3
  89. package/dist/docs/scaffolding-agents.html +3 -3
  90. package/dist/docs/storage.html +3 -3
  91. package/dist/docs/troubleshooting.html +3 -3
  92. package/dist/evals/assertions.d.ts +190 -0
  93. package/dist/evals/assertions.d.ts.map +1 -0
  94. package/dist/evals/assertions.js +461 -0
  95. package/dist/evals/expect.d.ts +95 -0
  96. package/dist/evals/expect.d.ts.map +1 -0
  97. package/dist/evals/expect.js +194 -0
  98. package/dist/evals/judge.d.ts +133 -0
  99. package/dist/evals/judge.d.ts.map +1 -0
  100. package/dist/evals/judge.js +228 -0
  101. package/dist/evals/loaders.d.ts +51 -0
  102. package/dist/evals/loaders.d.ts.map +1 -0
  103. package/dist/evals/loaders.js +124 -0
  104. package/dist/evals/matchers.d.ts +95 -0
  105. package/dist/evals/matchers.d.ts.map +1 -0
  106. package/dist/evals/matchers.js +145 -0
  107. package/dist/evals/reporters.d.ts +70 -0
  108. package/dist/evals/reporters.d.ts.map +1 -0
  109. package/dist/evals/reporters.js +221 -0
  110. package/dist/evals/results.d.ts +103 -0
  111. package/dist/evals/results.d.ts.map +1 -0
  112. package/dist/evals/results.js +28 -0
  113. package/dist/evals/run-facts.d.ts +77 -0
  114. package/dist/evals/run-facts.d.ts.map +1 -0
  115. package/dist/evals/run-facts.js +187 -0
  116. package/dist/evals.d.ts +118 -31
  117. package/dist/evals.d.ts.map +1 -1
  118. package/dist/evals.js +46 -12
  119. package/dist/index.d.ts +1 -1
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +1 -1
  122. package/dist/internal/cli-ax.d.ts +27 -0
  123. package/dist/internal/cli-ax.d.ts.map +1 -1
  124. package/dist/internal/cli-ax.js +90 -12
  125. package/dist/internal/cli-mcp.d.ts +8 -0
  126. package/dist/internal/cli-mcp.d.ts.map +1 -1
  127. package/dist/internal/cli-mcp.js +98 -0
  128. package/dist/internal/cli-slack.d.ts +29 -1
  129. package/dist/internal/cli-slack.d.ts.map +1 -1
  130. package/dist/internal/cli-slack.js +777 -12
  131. package/dist/internal/deploy-client.d.ts +7 -0
  132. package/dist/internal/deploy-client.d.ts.map +1 -1
  133. package/dist/internal/deploy-client.js +9 -0
  134. package/dist/internal/discovery.d.ts.map +1 -1
  135. package/dist/internal/discovery.js +29 -9
  136. package/dist/internal/distribution.d.ts.map +1 -1
  137. package/dist/internal/distribution.js +2 -0
  138. package/dist/internal/eval-judge-model.d.ts +59 -0
  139. package/dist/internal/eval-judge-model.d.ts.map +1 -0
  140. package/dist/internal/eval-judge-model.js +131 -0
  141. package/dist/internal/eval-run-store.d.ts.map +1 -1
  142. package/dist/internal/eval-run-store.js +12 -20
  143. package/dist/internal/eval-runner.d.ts +31 -25
  144. package/dist/internal/eval-runner.d.ts.map +1 -1
  145. package/dist/internal/eval-runner.js +329 -126
  146. package/dist/internal/evals-client.d.ts.map +1 -1
  147. package/dist/internal/evals-client.js +4 -1
  148. package/dist/internal/local-env.d.ts +8 -0
  149. package/dist/internal/local-env.d.ts.map +1 -0
  150. package/dist/internal/local-env.js +47 -0
  151. package/dist/internal/playground/static.d.ts +3 -0
  152. package/dist/internal/playground/static.d.ts.map +1 -1
  153. package/dist/internal/playground/static.js +3 -0
  154. package/dist/internal/run-client.d.ts +15 -0
  155. package/dist/internal/run-client.d.ts.map +1 -1
  156. package/dist/internal/run-client.js +3 -2
  157. package/dist/internal/sdk-runner.d.ts +36 -1
  158. package/dist/internal/sdk-runner.d.ts.map +1 -1
  159. package/dist/internal/sdk-runner.js +65 -25
  160. package/dist/internal/server.d.ts.map +1 -1
  161. package/dist/internal/server.js +10 -1
  162. package/dist/internal/session-engine.d.ts +18 -5
  163. package/dist/internal/session-engine.d.ts.map +1 -1
  164. package/dist/internal/session-engine.js +72 -19
  165. package/dist/internal/slack-provision-client.d.ts +130 -0
  166. package/dist/internal/slack-provision-client.d.ts.map +1 -0
  167. package/dist/internal/slack-provision-client.js +284 -0
  168. package/dist/playground/assets/cursor-icons-16-f_W_ogc-.woff2 +0 -0
  169. package/dist/playground/assets/index-CidizGZv.css +1 -0
  170. package/dist/playground/assets/index-DNqirsrK.js +85 -0
  171. package/dist/playground/index.html +2 -2
  172. package/dist/types.d.ts +70 -4
  173. package/dist/types.d.ts.map +1 -1
  174. package/dist/types.js +26 -0
  175. package/docs/guides/mcp-oauth.md +10 -5
  176. package/docs/guides/slack.md +3 -2
  177. package/docs/reference/connections.md +28 -18
  178. package/package.json +20 -2
  179. package/skills/create-agent/SKILL.md +15 -7
  180. package/skills/debug/SKILL.md +3 -1
  181. package/skills/mcp-auth/SKILL.md +1 -1
  182. package/skills/setup-slack/SKILL.md +173 -11
  183. package/src/bin/agent-serve.ts +63 -2
  184. package/src/channels/slack/api.ts +16 -3
  185. package/src/channels/slack/auth.ts +16 -1
  186. package/src/channels/slack/blocks.ts +139 -0
  187. package/src/channels/slack/defaults.ts +32 -4
  188. package/src/channels/slack/dispatch.ts +3 -18
  189. package/src/channels/slack/index.ts +1 -0
  190. package/src/channels/slack/interactive.ts +183 -19
  191. package/src/channels/slack/manifest.ts +3 -0
  192. package/src/channels/slack/setup.ts +16 -2
  193. package/src/channels/slack/slack-channel.ts +37 -6
  194. package/src/channels/slack/types.ts +89 -0
  195. package/src/connections.ts +4 -1
  196. package/src/evals/assertions.ts +704 -0
  197. package/src/evals/expect.ts +272 -0
  198. package/src/evals/judge.ts +346 -0
  199. package/src/evals/loaders.ts +113 -0
  200. package/src/evals/matchers.ts +246 -0
  201. package/src/evals/reporters.ts +274 -0
  202. package/src/evals/results.ts +132 -0
  203. package/src/evals/run-facts.ts +274 -0
  204. package/src/evals.ts +205 -42
  205. package/src/index.ts +2 -0
  206. package/src/internal/cli-ax.ts +134 -15
  207. package/src/internal/cli-mcp.ts +118 -5
  208. package/src/internal/cli-slack.ts +993 -15
  209. package/src/internal/deploy-client.ts +10 -0
  210. package/src/internal/discovery.ts +31 -5
  211. package/src/internal/distribution.ts +2 -0
  212. package/src/internal/eval-judge-model.ts +148 -0
  213. package/src/internal/eval-run-store.ts +10 -14
  214. package/src/internal/eval-runner.ts +460 -214
  215. package/src/internal/evals-client.ts +5 -0
  216. package/src/internal/local-env.ts +53 -0
  217. package/src/internal/playground/static.ts +3 -0
  218. package/src/internal/run-client.ts +17 -1
  219. package/src/internal/sdk-runner.ts +103 -22
  220. package/src/internal/server.ts +10 -0
  221. package/src/internal/session-engine.ts +89 -21
  222. package/src/internal/slack-provision-client.ts +438 -0
  223. package/src/types.ts +92 -4
  224. package/dist/channels/slack/post-update-delivery.d.ts +0 -85
  225. package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
  226. package/dist/docs/assets/chunks/@localSearchIndexroot.DZtu0ZIc.js +0 -1
  227. package/dist/internal/json-dir-store.d.ts +0 -32
  228. package/dist/internal/json-dir-store.d.ts.map +0 -1
  229. package/dist/internal/persistence-coordinator.d.ts +0 -127
  230. package/dist/internal/persistence-coordinator.d.ts.map +0 -1
  231. package/dist/persistence.d.ts +0 -184
  232. package/dist/persistence.d.ts.map +0 -1
  233. package/dist/playground/assets/cursor-icons-16-CQ50JpfO.woff2 +0 -0
  234. package/dist/playground/assets/index-Bwl4Onx4.js +0 -85
  235. package/dist/playground/assets/index-ByV4nfch.css +0 -1
  236. /package/dist/docs/assets/{guides_slack.md.FWale6Ip.lean.js → guides_slack.md.D_xFKlnF.lean.js} +0 -0
@@ -488,6 +488,11 @@ export function snapshotToEvalRunResults(
488
488
  path: c.fileId,
489
489
  ...(c.description === undefined ? {} : { description: c.description }),
490
490
  ok: c.ok === true,
491
+ // Older serve processes send no verdict; infer it from `ok` so a remote
492
+ // batch still prints and reports consistently.
493
+ verdict: c.verdict ?? (c.ok === true ? "passed" : "failed"),
494
+ ...(c.skipReason === undefined ? {} : { skipReason: c.skipReason }),
495
+ ...(c.metadata === undefined ? {} : { metadata: c.metadata }),
491
496
  assertions: c.assertions ?? [],
492
497
  ...(c.sessionId === undefined ? {} : { sessionId: c.sessionId }),
493
498
  inputs: c.inputs ?? [],
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Loads an agent directory's `.env.local` (and each immediate child's,
3
+ * for multi-agent hosts) into process.env. Real env always wins —
4
+ * `process.loadEnvFile` never overrides. Values a previous call loaded are
5
+ * refreshed, so a reload picks up edits made since.
6
+ */
7
+
8
+ import { existsSync, readdirSync } from "node:fs";
9
+ import { join } from "node:path";
10
+
11
+ // Not real env, so a reload has to clear them first: loadEnvFile skips
12
+ // anything already in process.env, which would pin the first-boot values.
13
+ let fileLoadedKeys = new Set<string>();
14
+
15
+ export function loadAgentLocalEnv(dir: string): string[] {
16
+ for (const key of fileLoadedKeys) {
17
+ delete process.env[key];
18
+ }
19
+ const realKeys = new Set(Object.keys(process.env));
20
+ const loaded: string[] = [];
21
+ const tryLoad = (path: string): void => {
22
+ if (!existsSync(path)) {
23
+ return;
24
+ }
25
+ process.loadEnvFile(path);
26
+ loaded.push(path);
27
+ };
28
+ const trackLoadedKeys = (): void => {
29
+ fileLoadedKeys = new Set(
30
+ Object.keys(process.env).filter((key) => !realKeys.has(key))
31
+ );
32
+ };
33
+
34
+ tryLoad(join(dir, ".env.local"));
35
+
36
+ let children: string[];
37
+ try {
38
+ children = readdirSync(dir, { withFileTypes: true })
39
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
40
+ .map((entry) => entry.name);
41
+ } catch {
42
+ trackLoadedKeys();
43
+ return loaded;
44
+ }
45
+ for (const child of children) {
46
+ if (child === "node_modules") {
47
+ continue;
48
+ }
49
+ tryLoad(join(dir, child, ".env.local"));
50
+ }
51
+ trackLoadedKeys();
52
+ return loaded;
53
+ }
@@ -52,6 +52,9 @@ export function playgroundBundleExists(): boolean {
52
52
  * The SPA `index.html` with a `<base href>` injected so its relative asset
53
53
  * URLs resolve under the mount point (`/playground/` or `/<slug>/playground/`).
54
54
  * Falls back to a helpful placeholder page when the bundle is missing.
55
+ * The hosted alias performs the same injection for its central copy
56
+ * (`backend/server/src/agentServe/aliasPlaygroundShell.ts`) — keep them
57
+ * behaviorally aligned.
55
58
  */
56
59
  export function renderPlaygroundIndexHtml(baseHref: string): string {
57
60
  if (!playgroundBundleExists()) {
@@ -46,6 +46,11 @@ export interface RunSessionResult {
46
46
  events: SessionEvent[];
47
47
  trajectory: Trajectory;
48
48
  ok: boolean;
49
+ /**
50
+ * Index to pass as `startIndex` on the next call for this session. Undefined
51
+ * when the turn streamed nothing, in which case reuse the previous value.
52
+ */
53
+ nextEventIndex?: number;
49
54
  }
50
55
 
51
56
  const TERMINAL_TYPES = new Set([
@@ -63,6 +68,16 @@ export async function runSession(
63
68
  messages: string[];
64
69
  sessionId?: string;
65
70
  continuationToken?: string;
71
+ /**
72
+ * First event index to read when resuming an existing session.
73
+ *
74
+ * The stream replays from `startIndex` and this client stops at the first
75
+ * terminal event, so a caller that sends several turns across separate
76
+ * `runSession` calls **must** pass the index after its last consumed event
77
+ * (see {@link RunSessionResult.nextEventIndex}). Omitting it replays the
78
+ * earlier turn and returns those stale events instead of the new turn's.
79
+ */
80
+ startIndex?: number;
66
81
  /** Applied only when creating a new session (first message). */
67
82
  workspaceDir?: string;
68
83
  /** Applied only when creating a new session (first message). */
@@ -78,7 +93,7 @@ export async function runSession(
78
93
  let sessionId = options.sessionId;
79
94
  let continuationToken = options.continuationToken;
80
95
  const allEvents: SessionEvent[] = [];
81
- let nextEventIndex = 0;
96
+ let nextEventIndex = options.startIndex ?? 0;
82
97
 
83
98
  const auth: RequestAuth = {
84
99
  bearerToken: options.bearerToken,
@@ -126,6 +141,7 @@ export async function runSession(
126
141
  events: allEvents,
127
142
  trajectory,
128
143
  ok: trajectory.ok,
144
+ nextEventIndex: allEvents.length === 0 ? undefined : nextEventIndex,
129
145
  };
130
146
  }
131
147
 
@@ -12,6 +12,7 @@ import {
12
12
  type AgentDefinition,
13
13
  type AgentOptions,
14
14
  type CloudAgentOptions,
15
+ createAgentPlatform,
15
16
  JsonlLocalAgentStore,
16
17
  type LocalAgentStore,
17
18
  type ModelSelection,
@@ -30,9 +31,48 @@ import {
30
31
  /** Internal seam between the session engine and the Cursor SDK harness. */
31
32
  export interface AgentRunner {
32
33
  runTurn(request: RunnerTurnRequest): Promise<TurnOutcome>;
34
+ /**
35
+ * Resolve a local workspace before any session needs it, so the first turn
36
+ * is not the one that pays for it. Best effort, hence the reported outcome:
37
+ * a runner that cannot warm must say so rather than let a caller log
38
+ * success over a workspace that was never touched.
39
+ */
40
+ prewarm?(request: RunnerPrewarmRequest): Promise<PrewarmOutcome>;
33
41
  dispose?(): Promise<void>;
34
42
  }
35
43
 
44
+ /**
45
+ * The parts of a turn that decide which local executor it runs on. Must stay
46
+ * a subset of {@link RunnerTurnRequest} — anything that disagrees warms an
47
+ * executor no turn will ask for.
48
+ */
49
+ export type RunnerPrewarmRequest = Pick<
50
+ RunnerTurnRequest,
51
+ | "workspaceDir"
52
+ | "runnerStateDir"
53
+ | "sandbox"
54
+ | "mcpServers"
55
+ | "subagents"
56
+ | "model"
57
+ >;
58
+
59
+ /** Whether a workspace was actually resolved, and why not when it wasn't. */
60
+ export type PrewarmOutcome =
61
+ | { readonly warmed: true }
62
+ | { readonly warmed: false; readonly reason: string };
63
+
64
+ /**
65
+ * `CursorAgentPlatform.prewarmLocalWorkspace` as of `@cursor/sdk` 1.0.26.
66
+ * Declared structurally so this keeps building — and reports that it skipped
67
+ * warming — against the pinned older SDK. Delete once the pin passes that
68
+ * version.
69
+ */
70
+ type MaybePrewarmingPlatform = {
71
+ prewarmLocalWorkspace?: (
72
+ options: AgentOptions
73
+ ) => Promise<() => Promise<void>>;
74
+ };
75
+
36
76
  export interface CursorSdkRunnerOptions {
37
77
  /** Cursor API key. Defaults to the `CURSOR_API_KEY` environment variable. */
38
78
  apiKey?: string;
@@ -49,6 +89,7 @@ export interface CursorSdkRunnerOptions {
49
89
  export class CursorSdkRunner implements AgentRunner {
50
90
  private readonly agents = new Map<string, Promise<SDKAgent>>();
51
91
  private readonly stores = new Map<string, Promise<LocalAgentStore>>();
92
+ private readonly prewarmReleases: Array<() => Promise<void>> = [];
52
93
 
53
94
  constructor(private readonly options: CursorSdkRunnerOptions = {}) {}
54
95
 
@@ -112,7 +153,31 @@ export class CursorSdkRunner implements AgentRunner {
112
153
  }
113
154
  }
114
155
 
156
+ /**
157
+ * Build the local executor for a workspace before any session asks for it.
158
+ * The warmed executor is reference counted, so the lease is held until
159
+ * {@link dispose}; dropping it early would tear the workspace back down.
160
+ */
161
+ async prewarm(request: RunnerPrewarmRequest): Promise<PrewarmOutcome> {
162
+ const platform = (await createAgentPlatform()) as MaybePrewarmingPlatform;
163
+ if (typeof platform.prewarmLocalWorkspace !== "function") {
164
+ // Older @cursor/sdk. Turns still work; they just pay on the first one.
165
+ return {
166
+ warmed: false,
167
+ reason:
168
+ "the installed @cursor/sdk has no prewarmLocalWorkspace; upgrade it to warm the workspace before the first turn",
169
+ };
170
+ }
171
+ const release = await platform.prewarmLocalWorkspace(
172
+ await this.buildLocalOptions(request)
173
+ );
174
+ this.prewarmReleases.push(release);
175
+ return { warmed: true };
176
+ }
177
+
115
178
  async dispose(): Promise<void> {
179
+ const releases = this.prewarmReleases.splice(0);
180
+ await Promise.allSettled(releases.map((release) => release()));
116
181
  const agents = await Promise.allSettled([...this.agents.values()]);
117
182
  this.agents.clear();
118
183
  for (const settled of agents) {
@@ -134,47 +199,61 @@ export class CursorSdkRunner implements AgentRunner {
134
199
  return agentPromise;
135
200
  }
136
201
 
137
- private async openAgent(request: RunnerTurnRequest): Promise<SDKAgent> {
202
+ private requireApiKey(): string {
138
203
  const apiKey = this.options.apiKey ?? process.env.CURSOR_API_KEY;
139
204
  if (apiKey === undefined || apiKey.trim() === "") {
140
205
  throw new Error(
141
206
  "The Cursor SDK runner requires a Cursor credential: run `agent-serve login`, set CURSOR_API_KEY, or pass serve({ apiKey })."
142
207
  );
143
208
  }
144
- const model = request.model ?? DEFAULT_MODEL;
209
+ return apiKey;
210
+ }
211
+
212
+ private buildSharedOptions(
213
+ request: RunnerPrewarmRequest
214
+ ): Pick<AgentOptions, "model" | "apiKey" | "mcpServers" | "agents"> {
145
215
  const subagents = toAgentDefinitions(request.subagents);
146
- const shared: Pick<
147
- AgentOptions,
148
- "model" | "apiKey" | "mcpServers" | "agents"
149
- > = {
150
- model: toModelSelection(model),
151
- apiKey,
216
+ return {
217
+ model: toModelSelection(request.model ?? DEFAULT_MODEL),
218
+ apiKey: this.requireApiKey(),
152
219
  ...(Object.keys(request.mcpServers).length > 0
153
220
  ? { mcpServers: request.mcpServers as AgentOptions["mcpServers"] }
154
221
  : {}),
155
222
  ...(subagents === undefined ? {} : { agents: subagents }),
156
223
  };
157
-
158
- if (request.runtime === "cloud") {
159
- return this.openCloudAgent(request, shared);
160
- }
161
- return this.openLocalAgent(request, shared);
162
224
  }
163
225
 
164
- private async openLocalAgent(
165
- request: RunnerTurnRequest,
166
- shared: Pick<AgentOptions, "model" | "apiKey" | "mcpServers" | "agents">
167
- ): Promise<SDKAgent> {
226
+ /**
227
+ * The options that decide which local executor a turn runs on. Prewarming
228
+ * goes through here too, so a warmed workspace is the one turns land on.
229
+ */
230
+ private async buildLocalOptions(
231
+ request: RunnerPrewarmRequest
232
+ ): Promise<AgentOptions> {
168
233
  const store =
169
234
  this.options.store ?? (await this.getStore(request.runnerStateDir));
170
- const options: AgentOptions = {
171
- ...shared,
235
+ return {
236
+ ...this.buildSharedOptions(request),
172
237
  local: {
173
238
  cwd: request.workspaceDir,
174
239
  settingSources: ["project"],
175
240
  store,
241
+ ...(request.sandbox === true
242
+ ? { sandboxOptions: { enabled: true } }
243
+ : {}),
176
244
  },
177
245
  };
246
+ }
247
+
248
+ private async openAgent(request: RunnerTurnRequest): Promise<SDKAgent> {
249
+ if (request.runtime === "cloud") {
250
+ return this.openCloudAgent(request, this.buildSharedOptions(request));
251
+ }
252
+ return this.openLocalAgent(request);
253
+ }
254
+
255
+ private async openLocalAgent(request: RunnerTurnRequest): Promise<SDKAgent> {
256
+ const options = await this.buildLocalOptions(request);
178
257
 
179
258
  if (request.isFirstTurn || request.sdkAgentId === undefined) {
180
259
  return Agent.create({
@@ -341,11 +420,13 @@ function toTurnOutcome(result: RunResult): TurnOutcome {
341
420
  return {
342
421
  status: result.status,
343
422
  ...(result.result === undefined ? {} : { result: result.result }),
344
- // The published RunResult carries no structured error details; failures
345
- // thrown by send()/wait() surface through the engine's catch instead.
346
423
  ...(result.status === "error"
347
- ? { errorMessage: "The Cursor agent run failed" }
424
+ ? { errorMessage: runErrorMessage(result) }
348
425
  : {}),
349
426
  ...(usage === undefined ? {} : { usage }),
350
427
  };
351
428
  }
429
+
430
+ export function runErrorMessage(result: Pick<RunResult, "error">): string {
431
+ return result.error?.message?.trim() || "The Cursor agent run failed";
432
+ }
@@ -385,6 +385,12 @@ export async function startServer(
385
385
  // channel or route can address them (continuation tokens must resolve).
386
386
  await engine.restoreStoredSessions();
387
387
 
388
+ // Resolve a shared harness workspace in the background: on a large repo
389
+ // that takes seconds, and a deployment coming online should absorb it
390
+ // rather than whoever sends the first message. No-op unless the agent
391
+ // pins `local.workspaceDir`.
392
+ void engine.prewarmSharedWorkspace();
393
+
388
394
  if (!githubAuthWarmed) {
389
395
  // Soft ambient GitHub auth check before channel onStart (which may call
390
396
  // host.github). Missing auth is a warning only — serve still starts.
@@ -911,6 +917,10 @@ function mountAgentRoutes(
911
917
  name: project.name,
912
918
  dev,
913
919
  basePath: base,
920
+ // Framework version, for SPA/engine skew observability now that the
921
+ // hosted playground shell is served centrally (backend deploys)
922
+ // while engines stay on their install-time version.
923
+ version: packageVersion(),
914
924
  ...(multi ? { indexPath: "/" } : {}),
915
925
  }),
916
926
  });
@@ -39,6 +39,7 @@ import {
39
39
  type PendingApproval,
40
40
  type ReceiveInput,
41
41
  type ResolvedAgent,
42
+ type RunnerSubagent,
42
43
  type RunnerTurnRequest,
43
44
  type SendMessageFn,
44
45
  type SendMessageOptions,
@@ -957,13 +958,18 @@ export class SessionEngine {
957
958
  }
958
959
 
959
960
  /**
960
- * Local harness cwd for a session: per-send override, else
961
- * `<agent.local.cwd>/<sessionId>`, else the durable session workspace.
961
+ * Local harness cwd for a session: per-send override, else the shared
962
+ * `agent.local.workspaceDir`, else `<agent.local.cwd>/<sessionId>`, else
963
+ * the durable session workspace.
962
964
  */
963
965
  harnessCwd(record: SessionRecord): string {
964
966
  if (record.workspaceOverride !== undefined) {
965
967
  return record.workspaceOverride;
966
968
  }
969
+ const shared = this.project.agent.local?.workspaceDir;
970
+ if (shared !== undefined) {
971
+ return shared;
972
+ }
967
973
  const agentCwd = this.project.agent.local?.cwd;
968
974
  if (agentCwd !== undefined) {
969
975
  return join(agentCwd, record.sessionId);
@@ -972,12 +978,64 @@ export class SessionEngine {
972
978
  }
973
979
 
974
980
  /**
975
- * Directory that receives AGENTS.md / skills / agent tools. When the
976
- * harness cwd is a one-off override (e.g. PR worktree), scaffold stays in
977
- * the durable session workspace; otherwise scaffold lands in the harness.
981
+ * True when the harness cwd is a directory the session does not own — a
982
+ * per-send override (e.g. a PR worktree) or a workspace shared by every
983
+ * session. Agent scaffold has to stay out of those, so it lands in the
984
+ * durable session workspace and the instructions travel inline instead.
985
+ */
986
+ private hasBorrowedWorkspace(record: SessionRecord): boolean {
987
+ return (
988
+ record.workspaceOverride !== undefined ||
989
+ this.project.agent.local?.workspaceDir !== undefined
990
+ );
991
+ }
992
+
993
+ /**
994
+ * Resolve the shared harness workspace during startup, so the first session
995
+ * is not the one that waits for it. Only possible with
996
+ * `local.workspaceDir`: every other layout gives a session its own cwd,
997
+ * which is unknowable until that session exists.
998
+ *
999
+ * Best effort — warming is an optimization, and a turn rebuilds on its own.
978
1000
  */
1001
+ async prewarmSharedWorkspace(): Promise<void> {
1002
+ const agent = this.project.agent;
1003
+ const workspaceDir = agent.local?.workspaceDir;
1004
+ if (
1005
+ workspaceDir === undefined ||
1006
+ agent.runtime !== "local" ||
1007
+ this.runner.prewarm === undefined
1008
+ ) {
1009
+ return;
1010
+ }
1011
+ const startedAt = Date.now();
1012
+ try {
1013
+ const outcome = await this.runner.prewarm({
1014
+ workspaceDir,
1015
+ runnerStateDir: join(this.stateRoot, "runner"),
1016
+ sandbox: agent.local?.sandbox === true,
1017
+ mcpServers: this.buildMcpServers("local"),
1018
+ subagents: buildRunnerSubagents(agent),
1019
+ ...(agent.model === undefined ? {} : { model: agent.model }),
1020
+ });
1021
+ // Report what happened rather than that the call returned: a runner
1022
+ // that cannot warm resolves cleanly, and logging success there would
1023
+ // tell an operator the workspace is hot when nothing touched it.
1024
+ this.logger(
1025
+ outcome.warmed
1026
+ ? `[agentkit] warmed ${workspaceDir} in ${Date.now() - startedAt}ms`
1027
+ : `[agentkit] did not warm ${workspaceDir}: ${outcome.reason}`
1028
+ );
1029
+ } catch (error) {
1030
+ this.logger(
1031
+ `[agentkit] could not warm ${workspaceDir}: ${describeError(error)}`
1032
+ );
1033
+ }
1034
+ }
1035
+
1036
+ /** Directory that receives AGENTS.md / skills / agent tools. */
979
1037
  private scaffoldDir(record: SessionRecord): string {
980
- if (record.workspaceOverride !== undefined) {
1038
+ if (this.hasBorrowedWorkspace(record)) {
981
1039
  return this.sessions.workspaceDir(record.sessionId);
982
1040
  }
983
1041
  return this.harnessCwd(record);
@@ -1053,13 +1111,13 @@ export class SessionEngine {
1053
1111
  });
1054
1112
  }
1055
1113
  // The harness only loads the scaffolded AGENTS.md when the session
1056
- // workspace is its own project root. With a per-session cwd override
1057
- // (PR worktree) the scaffold lands elsewhere, and a workspace nested
1058
- // inside a git repo re-roots harness rules at that repo — in both
1059
- // cases deliver instructions inline on the first turn instead.
1114
+ // workspace is its own project root. When the harness cwd is borrowed
1115
+ // (PR worktree, shared checkout) the scaffold lands elsewhere, and a
1116
+ // workspace nested inside a git repo re-roots harness rules at that
1117
+ // repo — in both cases deliver instructions inline on the first turn.
1060
1118
  const instructionsInvisibleToHarness =
1061
1119
  isFirstTurn &&
1062
- (record.workspaceOverride !== undefined ||
1120
+ (this.hasBorrowedWorkspace(record) ||
1063
1121
  (await isNestedInGitRepo(harnessCwd)));
1064
1122
  const request = this.buildTurnRequest(
1065
1123
  record,
@@ -1201,10 +1259,10 @@ export class SessionEngine {
1201
1259
  turnPrompt = `${preamble}\n\n${turnPrompt}`;
1202
1260
  } else if (instructionsInvisibleToHarness) {
1203
1261
  // Local workspace whose AGENTS.md the harness will not load. With a
1204
- // per-session harness cwd override, agent tool scripts live only in
1205
- // the durable scaffold dir — embed bodies like cloud. Otherwise the
1206
- // catalog can reference on-disk paths under the harness cwd.
1207
- const includeScripts = record.workspaceOverride !== undefined;
1262
+ // borrowed harness cwd, agent tool scripts live only in the durable
1263
+ // scaffold dir — embed bodies like cloud. Otherwise the catalog can
1264
+ // reference on-disk paths under the harness cwd.
1265
+ const includeScripts = this.hasBorrowedWorkspace(record);
1208
1266
  const preamble = buildAgentsMdContent(agent, { includeScripts });
1209
1267
  turnPrompt = `${preamble}\n\n${turnPrompt}`;
1210
1268
  }
@@ -1215,6 +1273,7 @@ export class SessionEngine {
1215
1273
  ? {}
1216
1274
  : { sdkAgentId: record.sdkAgentId }),
1217
1275
  workspaceDir: this.harnessCwd(record),
1276
+ sandbox: this.project.agent.local?.sandbox === true,
1218
1277
  runnerStateDir: join(this.stateRoot, "runner"),
1219
1278
  prompt: turnPrompt,
1220
1279
  isFirstTurn,
@@ -1224,12 +1283,7 @@ export class SessionEngine {
1224
1283
  customTools:
1225
1284
  runtime === "local" ? this.buildCustomTools(record, turnId, emit) : {},
1226
1285
  mcpServers: this.buildMcpServers(runtime),
1227
- subagents: agent.subagents.map((subagent) => ({
1228
- name: subagent.name,
1229
- description: subagent.description ?? subagent.name,
1230
- prompt: subagentPrompt(subagent),
1231
- ...(subagent.model === undefined ? {} : { model: subagent.model }),
1232
- })),
1286
+ subagents: buildRunnerSubagents(agent),
1233
1287
  onUpdate,
1234
1288
  signal,
1235
1289
  // Local follow-ups expire any lingering active run so a preempted turn
@@ -2509,6 +2563,20 @@ function subagentPrompt(subagent: ResolvedAgent): string {
2509
2563
  return subagent.description ?? subagent.name;
2510
2564
  }
2511
2565
 
2566
+ /**
2567
+ * Subagents as the runner takes them. Shared by turns and by workspace
2568
+ * warming, which has to describe the same agent a turn would to reach the
2569
+ * executor that turn will run on.
2570
+ */
2571
+ function buildRunnerSubagents(agent: ResolvedAgent): RunnerSubagent[] {
2572
+ return agent.subagents.map((subagent) => ({
2573
+ name: subagent.name,
2574
+ description: subagent.description ?? subagent.name,
2575
+ prompt: subagentPrompt(subagent),
2576
+ ...(subagent.model === undefined ? {} : { model: subagent.model }),
2577
+ }));
2578
+ }
2579
+
2512
2580
  function describeError(error: unknown): string {
2513
2581
  return error instanceof Error ? error.message : String(error);
2514
2582
  }