@cursor/july 0.1.94 → 0.1.95

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 (209) hide show
  1. package/dist/channels/github/github-channel.d.ts.map +1 -1
  2. package/dist/channels/github/github-channel.js +14 -2
  3. package/dist/channels/github/types.d.ts +18 -3
  4. package/dist/channels/github/types.d.ts.map +1 -1
  5. package/dist/channels/origin/origin-channel.d.ts.map +1 -1
  6. package/dist/channels/origin/origin-channel.js +11 -3
  7. package/dist/channels/origin/origin-webhook.d.ts +11 -1
  8. package/dist/channels/origin/origin-webhook.d.ts.map +1 -1
  9. package/dist/channels/origin/origin-webhook.js +23 -3
  10. package/dist/channels/origin/types.d.ts +8 -0
  11. package/dist/channels/origin/types.d.ts.map +1 -1
  12. package/dist/channels/slack/pr-ref.d.ts +7 -1
  13. package/dist/channels/slack/pr-ref.d.ts.map +1 -1
  14. package/dist/channels/slack/pr-ref.js +42 -23
  15. package/dist/docs/404.html +2 -2
  16. package/dist/docs/ab.html +4 -4
  17. package/dist/docs/assets/{app.CFDEas4I.js → app.BBj0klBO.js} +1 -1
  18. package/dist/docs/assets/chunks/@localSearchIndexroot.oqPawjiD.js +1 -0
  19. package/dist/docs/assets/chunks/{VPLocalSearchBox.B1IIYpYS.js → VPLocalSearchBox.CUEXpL78.js} +1 -1
  20. package/dist/docs/assets/chunks/{theme.Ct4NSiLm.js → theme.DabmQnia.js} +2 -2
  21. package/dist/docs/assets/index.md.BoHaMdeZ.js +5 -0
  22. package/dist/docs/assets/{index.md.DRakGHFe.lean.js → index.md.BoHaMdeZ.lean.js} +1 -1
  23. package/dist/docs/assets/{reference_cli.md.Cw6_ICYG.js → reference_cli.md.BsMOxDvh.js} +3 -2
  24. package/dist/docs/assets/{reference_cli.md.Cw6_ICYG.lean.js → reference_cli.md.BsMOxDvh.lean.js} +1 -1
  25. package/dist/docs/assets/{reference_skills.md.8son6Hjm.js → reference_skills.md.BFW9retM.js} +3 -3
  26. package/dist/docs/assets/{templates_agentic-owners.md.DqtPdm6f.js → templates_agentic-owners.md.BZSH4N9z.js} +1 -1
  27. package/dist/docs/assets/{templates_demo.md.DhFcWN6j.js → templates_demo.md.BeQX9V3H.js} +1 -1
  28. package/dist/docs/assets/{templates_pr-autofixer.md.DU7dQpor.js → templates_pr-autofixer.md.x5zl6-GT.js} +2 -2
  29. package/dist/docs/assets/{templates_pr-autofixer.md.DU7dQpor.lean.js → templates_pr-autofixer.md.x5zl6-GT.lean.js} +1 -1
  30. package/dist/docs/assets/templates_security-help.md.C3Ny_Qr2.js +4 -0
  31. package/dist/docs/assets/templates_security-help.md.C3Ny_Qr2.lean.js +1 -0
  32. package/dist/docs/assets/{templates_security-reviewer.md.CTa7u_l1.js → templates_security-reviewer.md.lshxbCLK.js} +2 -2
  33. package/dist/docs/assets/{templates_security-reviewer.md.CTa7u_l1.lean.js → templates_security-reviewer.md.lshxbCLK.lean.js} +1 -1
  34. package/dist/docs/assets/{templates_triage.md.CVlpctKS.js → templates_triage.md.Co4UNzkZ.js} +3 -3
  35. package/dist/docs/assets/{templates_triage.md.CVlpctKS.lean.js → templates_triage.md.Co4UNzkZ.lean.js} +1 -1
  36. package/dist/docs/building-with-agents.html +4 -4
  37. package/dist/docs/concepts.html +4 -4
  38. package/dist/docs/deployment.html +4 -4
  39. package/dist/docs/evals.html +4 -4
  40. package/dist/docs/guides/agent-to-agent.html +4 -4
  41. package/dist/docs/guides/cloud-runtime.html +4 -4
  42. package/dist/docs/guides/convert-automation.html +4 -4
  43. package/dist/docs/guides/github.html +4 -4
  44. package/dist/docs/guides/human-in-the-loop.html +4 -4
  45. package/dist/docs/guides/mcp-oauth.html +4 -4
  46. package/dist/docs/guides/opentelemetry.html +4 -4
  47. package/dist/docs/guides/slack.html +4 -4
  48. package/dist/docs/guides/webhooks.html +4 -4
  49. package/dist/docs/hashmap.json +1 -1
  50. package/dist/docs/hillclimbing.html +4 -4
  51. package/dist/docs/index.html +6 -6
  52. package/dist/docs/index.md +3 -1
  53. package/dist/docs/llms-full.txt +90 -26
  54. package/dist/docs/llms.txt +1 -0
  55. package/dist/docs/quickstart.html +4 -4
  56. package/dist/docs/reference/agent-config.html +4 -4
  57. package/dist/docs/reference/artifacts.html +4 -4
  58. package/dist/docs/reference/channels.html +4 -4
  59. package/dist/docs/reference/cli.html +7 -6
  60. package/dist/docs/reference/cli.md +3 -1
  61. package/dist/docs/reference/connections.html +4 -4
  62. package/dist/docs/reference/hooks.html +4 -4
  63. package/dist/docs/reference/http-api.html +4 -4
  64. package/dist/docs/reference/instructions.html +4 -4
  65. package/dist/docs/reference/playground.html +4 -4
  66. package/dist/docs/reference/project-layout.html +4 -4
  67. package/dist/docs/reference/prompt.html +4 -4
  68. package/dist/docs/reference/schedules.html +4 -4
  69. package/dist/docs/reference/sessions.html +4 -4
  70. package/dist/docs/reference/skills.html +7 -7
  71. package/dist/docs/reference/subagents.html +4 -4
  72. package/dist/docs/reference/tools.html +4 -4
  73. package/dist/docs/scaffolding-agents.html +4 -4
  74. package/dist/docs/storage.html +4 -4
  75. package/dist/docs/templates/agentic-owners.html +7 -7
  76. package/dist/docs/templates/agentic-owners.md +1 -1
  77. package/dist/docs/templates/demo.html +6 -6
  78. package/dist/docs/templates/demo.md +3 -2
  79. package/dist/docs/templates/pr-autofixer.html +6 -6
  80. package/dist/docs/templates/pr-autofixer.md +7 -13
  81. package/dist/docs/templates/security-help.html +30 -0
  82. package/dist/docs/templates/security-help.md +65 -0
  83. package/dist/docs/templates/security-reviewer.html +6 -6
  84. package/dist/docs/templates/security-reviewer.md +1 -2
  85. package/dist/docs/templates/triage.html +7 -7
  86. package/dist/docs/templates/triage.md +2 -6
  87. package/dist/docs/troubleshooting.html +4 -4
  88. package/dist/files-backends/cursor-hosted.d.ts +6 -2
  89. package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
  90. package/dist/files-backends/cursor-hosted.js +2 -2
  91. package/dist/files.d.ts +2 -0
  92. package/dist/files.d.ts.map +1 -1
  93. package/dist/files.js +5 -0
  94. package/dist/internal/cli-deploy.d.ts.map +1 -1
  95. package/dist/internal/cli-deploy.js +135 -6
  96. package/dist/internal/conversation-mirror.d.ts +82 -0
  97. package/dist/internal/conversation-mirror.d.ts.map +1 -0
  98. package/dist/internal/conversation-mirror.js +251 -0
  99. package/dist/internal/deferred-channel-session.d.ts +20 -0
  100. package/dist/internal/deferred-channel-session.d.ts.map +1 -0
  101. package/dist/internal/deferred-channel-session.js +62 -0
  102. package/dist/internal/deploy-client.d.ts +13 -1
  103. package/dist/internal/deploy-client.d.ts.map +1 -1
  104. package/dist/internal/deploy-client.js +11 -1
  105. package/dist/internal/deploy-manifest.d.ts.map +1 -1
  106. package/dist/internal/deploy-manifest.js +11 -5
  107. package/dist/internal/discovery.d.ts.map +1 -1
  108. package/dist/internal/discovery.js +22 -2
  109. package/dist/internal/framework-storage-selection.d.ts +32 -16
  110. package/dist/internal/framework-storage-selection.d.ts.map +1 -1
  111. package/dist/internal/framework-storage-selection.js +51 -17
  112. package/dist/internal/hosted-admission-context.d.ts +20 -0
  113. package/dist/internal/hosted-admission-context.d.ts.map +1 -0
  114. package/dist/internal/hosted-admission-context.js +31 -0
  115. package/dist/internal/hosted-delivery-protocol.d.ts +5 -0
  116. package/dist/internal/hosted-delivery-protocol.d.ts.map +1 -1
  117. package/dist/internal/hosted-delivery-protocol.js +33 -1
  118. package/dist/internal/hosted-delivery.d.ts +4 -2
  119. package/dist/internal/hosted-delivery.d.ts.map +1 -1
  120. package/dist/internal/hosted-delivery.js +75 -35
  121. package/dist/internal/hosted-managed-automation.d.ts +30 -0
  122. package/dist/internal/hosted-managed-automation.d.ts.map +1 -0
  123. package/dist/internal/hosted-managed-automation.js +58 -0
  124. package/dist/internal/review-comments.d.ts.map +1 -1
  125. package/dist/internal/review-comments.js +10 -98
  126. package/dist/internal/scm/pr-url.d.ts +14 -0
  127. package/dist/internal/scm/pr-url.d.ts.map +1 -0
  128. package/dist/internal/scm/pr-url.js +65 -0
  129. package/dist/internal/sdk-runner.d.ts +14 -0
  130. package/dist/internal/sdk-runner.d.ts.map +1 -1
  131. package/dist/internal/sdk-runner.js +41 -2
  132. package/dist/internal/server.d.ts.map +1 -1
  133. package/dist/internal/server.js +10 -2
  134. package/dist/internal/session-engine.d.ts.map +1 -1
  135. package/dist/internal/session-engine.js +9 -0
  136. package/dist/playground/assets/index-CF7hUDnQ.css +1 -0
  137. package/dist/playground/assets/{index-B3JCyigB.js → index-CZA1uEWA.js} +48 -48
  138. package/dist/playground/index.html +2 -2
  139. package/dist/storage-backends/cursor-hosted.d.ts +7 -2
  140. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
  141. package/dist/storage-backends/cursor-hosted.js +2 -2
  142. package/docs/README.md +3 -1
  143. package/docs/reference/cli.md +3 -1
  144. package/docs/templates/agentic-owners.md +1 -1
  145. package/docs/templates/demo.md +3 -2
  146. package/docs/templates/pr-autofixer.md +7 -13
  147. package/docs/templates/security-help.md +70 -0
  148. package/docs/templates/security-reviewer.md +1 -2
  149. package/docs/templates/triage.md +2 -6
  150. package/package.json +1 -1
  151. package/src/channels/github/github-channel.ts +23 -8
  152. package/src/channels/github/types.ts +19 -2
  153. package/src/channels/origin/origin-channel.ts +13 -1
  154. package/src/channels/origin/origin-webhook.ts +27 -3
  155. package/src/channels/origin/types.ts +8 -0
  156. package/src/channels/slack/pr-ref.ts +56 -25
  157. package/src/files-backends/cursor-hosted.ts +9 -3
  158. package/src/files.ts +11 -0
  159. package/src/internal/cli-deploy.ts +171 -7
  160. package/src/internal/conversation-mirror.ts +330 -0
  161. package/src/internal/deferred-channel-session.ts +61 -0
  162. package/src/internal/deploy-client.ts +24 -1
  163. package/src/internal/deploy-manifest.ts +10 -5
  164. package/src/internal/discovery.ts +25 -2
  165. package/src/internal/framework-storage-selection.ts +61 -19
  166. package/src/internal/hosted-admission-context.ts +37 -0
  167. package/src/internal/hosted-delivery-protocol.ts +44 -1
  168. package/src/internal/hosted-delivery.ts +95 -40
  169. package/src/internal/hosted-managed-automation.ts +72 -0
  170. package/src/internal/review-comments.ts +10 -113
  171. package/src/internal/scm/pr-url.ts +95 -0
  172. package/src/internal/sdk-runner.ts +57 -2
  173. package/src/internal/server.ts +10 -0
  174. package/src/internal/session-engine.ts +12 -0
  175. package/src/storage-backends/cursor-hosted.ts +10 -3
  176. package/templates/agentic-owners/README.md +1 -1
  177. package/templates/agentic-owners/agent/agent.ts +0 -10
  178. package/templates/agentic-owners/agent/channels/github.ts +5 -14
  179. package/templates/agentic-owners/agent/lib/config.ts +0 -8
  180. package/templates/agentic-owners/agent/lib/review.ts +2 -15
  181. package/templates/agentic-owners/agent/tools/record_review.ts +2 -4
  182. package/templates/demo/agent/agent.ts +0 -10
  183. package/templates/pr-autofixer/README.md +0 -2
  184. package/templates/pr-autofixer/agent/agent.ts +0 -11
  185. package/templates/pr-autofixer/agent/channels/slack.ts +0 -7
  186. package/templates/pr-autofixer/agent/lib/pr-state.ts +5 -17
  187. package/templates/pr-autofixer/agent/lib/repos.ts +0 -1
  188. package/templates/security-help/README.md +2 -2
  189. package/templates/security-help/agent/agent.ts +1 -2
  190. package/templates/security-help/agent/channels/slack.ts +0 -3
  191. package/templates/security-help/agent/instructions.md +9 -10
  192. package/templates/security-help/agent/skills/access-request.md +1 -1
  193. package/templates/security-help/agent/skills/faq.md +31 -0
  194. package/templates/security-help/agent/skills/security-playbooks.md +1 -1
  195. package/templates/security-help/package.json +1 -2
  196. package/templates/security-reviewer/agent/agent.ts +0 -10
  197. package/templates/triage/agent/agent.ts +0 -10
  198. package/templates/triage/agent/channels/queue.ts +1 -1
  199. package/templates/triage/agent/channels/webhook.ts +1 -3
  200. package/dist/docs/assets/chunks/@localSearchIndexroot.DU3U2Ij2.js +0 -1
  201. package/dist/docs/assets/index.md.DRakGHFe.js +0 -5
  202. package/dist/playground/assets/index-D9N7-q97.css +0 -1
  203. package/templates/security-help/agent/knowledge/faq/approvals.md +0 -5
  204. package/templates/security-help/agent/knowledge/faq/channels.md +0 -6
  205. package/templates/security-help/agent/knowledge/faq/phishing.md +0 -10
  206. package/templates/security-help/agent/skills/security-first-pass.md +0 -15
  207. /package/dist/docs/assets/{reference_skills.md.8son6Hjm.lean.js → reference_skills.md.BFW9retM.lean.js} +0 -0
  208. /package/dist/docs/assets/{templates_agentic-owners.md.DqtPdm6f.lean.js → templates_agentic-owners.md.BZSH4N9z.lean.js} +0 -0
  209. /package/dist/docs/assets/{templates_demo.md.DhFcWN6j.lean.js → templates_demo.md.BeQX9V3H.lean.js} +0 -0
@@ -27,6 +27,7 @@ import { authorizeCursorAccountConnectors } from "./cursor-account-mcp-auth.js";
27
27
  import { readDeployTemplate } from "./cursor-agent-template.js";
28
28
  import {
29
29
  type AgentkitDeployAccepted,
30
+ type AgentkitDeployComponent,
30
31
  type AgentkitDeployStatus,
31
32
  type AgentkitStopAccepted,
32
33
  AgentServeDeployClient,
@@ -294,10 +295,35 @@ export async function cmdDeploy(options: DeployCliOptions): Promise<number> {
294
295
  return targets;
295
296
  }
296
297
 
298
+ if (targets.length === 1) {
299
+ return deployOne(ctx, options, targets[0]!);
300
+ }
301
+
302
+ // Multi-agent parent: when every child declares `architecture: "v2"`,
303
+ // the v2 control plane models sibling agents as release components of
304
+ // ONE application, so deploy them together (one app, one component per
305
+ // child) instead of minting a separate application per child. Mixed and
306
+ // v1 parents keep the per-child sequential deploys.
307
+ const manifests: DeployManifest[] = [];
308
+ for (const target of targets) {
309
+ manifests.push(await readDeployManifest(target.dir));
310
+ }
311
+ if (manifests.every((manifest) => manifest.architecture === "v2")) {
312
+ return deployAgentsdkApplication(ctx, options, targets);
313
+ }
314
+ if (
315
+ !ctx.json &&
316
+ manifests.some((manifest) => manifest.architecture === "v2")
317
+ ) {
318
+ ctx.err(
319
+ `warning: agents under this parent mix architectures; deploying each separately (v2 children become standalone single-component applications).\n`
320
+ );
321
+ }
322
+
297
323
  // --path is per-agent (relative to the repo root). When deploying several
298
324
  // agents, ignore a shared --path so each target can infer its own path.
299
325
  let deployOptions = options;
300
- if (targets.length > 1 && options.path !== undefined) {
326
+ if (options.path !== undefined) {
301
327
  if (!ctx.json) {
302
328
  ctx.err(
303
329
  `warning: ignoring --path ${JSON.stringify(options.path)} for a multi-agent deploy; each agent infers its own path from git.\n`
@@ -306,10 +332,6 @@ export async function cmdDeploy(options: DeployCliOptions): Promise<number> {
306
332
  deployOptions = { ...options, path: undefined };
307
333
  }
308
334
 
309
- if (targets.length === 1) {
310
- return deployOne(ctx, deployOptions, targets[0]!);
311
- }
312
-
313
335
  // Multi-agent batch: deploy sequentially; --json emits an array.
314
336
  const jsonResults: unknown[] = [];
315
337
  let failures = 0;
@@ -477,6 +499,19 @@ async function resolveDeployTargets(
477
499
  return [match];
478
500
  }
479
501
 
502
+ // An all-v2 parent deploys as ONE application (siblings are components
503
+ // of a single app on the v2 control plane), so "deploy the parent" is
504
+ // unambiguous and needs no picker or --all. A child that is v1 — or
505
+ // fails to load — keeps the interactive picker below.
506
+ if (await allChildrenAreV2(children)) {
507
+ if (!ctx.json) {
508
+ ctx.err(
509
+ `Deploying all ${children.length} v2 agents under ${basename(dir)} as one application (pass --slug <name> to deploy a single agent standalone).\n`
510
+ );
511
+ }
512
+ return [...children];
513
+ }
514
+
480
515
  const chooser =
481
516
  options.chooseTargets ??
482
517
  ((agents) =>
@@ -508,6 +543,27 @@ function formatAgentList(agents: readonly DeployTarget[]): string {
508
543
  return agents.map((agent, i) => ` ${i + 1}) ${agent.slug}\n`).join("");
509
544
  }
510
545
 
546
+ /**
547
+ * True when every child agent project loads and declares
548
+ * `architecture: "v2"`. Broken projects count as not-v2 so target
549
+ * resolution falls back to the picker instead of failing the whole deploy
550
+ * on a child the user might not even select.
551
+ */
552
+ async function allChildrenAreV2(
553
+ children: readonly DeployTarget[]
554
+ ): Promise<boolean> {
555
+ for (const child of children) {
556
+ try {
557
+ if ((await readDeployManifest(child.dir)).architecture !== "v2") {
558
+ return false;
559
+ }
560
+ } catch {
561
+ return false;
562
+ }
563
+ }
564
+ return true;
565
+ }
566
+
511
567
  /**
512
568
  * TTY picker for multi-agent parents. Non-interactive callers get `[]`
513
569
  * so {@link resolveDeployTargets} can print the --slug/--all hint.
@@ -777,6 +833,100 @@ async function deployOne(
777
833
  return 0;
778
834
  }
779
835
 
836
+ /**
837
+ * Multi-agent parent whose children all declare `architecture: "v2"`:
838
+ * deploy ONE application whose release carries a component per child. The
839
+ * application slug derives from the parent directory name; each component
840
+ * key is the child's slug and its path is the child directory relative to
841
+ * the repo root (a shared --path names the parent's directory in the repo).
842
+ */
843
+ async function deployAgentsdkApplication(
844
+ ctx: DeployCliContext,
845
+ options: DeployCliOptions,
846
+ targets: readonly DeployTarget[]
847
+ ): Promise<number> {
848
+ const parentDir = resolve(options.dir ?? ".");
849
+ const slug = slugFromDirectoryName(basename(parentDir));
850
+ if (slug === undefined) {
851
+ ctx.err(
852
+ `Could not derive an application slug from the parent directory name ${JSON.stringify(basename(parentDir))} — rename it (deploy slugs use lowercase letters, digits, '-' or '_').\n`
853
+ );
854
+ return 1;
855
+ }
856
+
857
+ const needsGitInference =
858
+ options.repo === undefined ||
859
+ options.ref === undefined ||
860
+ options.path === undefined;
861
+ const inferred = needsGitInference
862
+ ? await inferDeployGitSource(parentDir, {
863
+ ...(options.git === undefined ? {} : { git: options.git }),
864
+ })
865
+ : {};
866
+ const gitRepoUrl = options.repo ?? inferred.gitRepoUrl;
867
+ const gitRef = options.ref ?? inferred.gitRef;
868
+ const parentPath = options.path ?? inferred.agentPath;
869
+
870
+ if (gitRepoUrl !== undefined && !gitRepoUrl.startsWith("https://")) {
871
+ ctx.err(
872
+ `Invalid --repo: ${gitRepoUrl} (expected an https:// git repository URL)\n`
873
+ );
874
+ return 1;
875
+ }
876
+ if (gitRepoUrl === undefined) {
877
+ ctx.err(
878
+ `Could not resolve an https:// git repository URL for this deploy.\n` +
879
+ ` Pass --repo https://github.com/<owner>/<repo> (inferred from origin when possible).\n`
880
+ );
881
+ return 1;
882
+ }
883
+ for (const repo of options.cursorEventsRepos ?? []) {
884
+ if (!isValidCursorEventRepo(repo)) {
885
+ ctx.err(`Invalid --cursor-events-repo: ${repo} (expected owner/name)\n`);
886
+ return 1;
887
+ }
888
+ }
889
+
890
+ const components: AgentkitDeployComponent[] = targets.map((target) => ({
891
+ componentKey: target.slug,
892
+ agentPath: [parentPath, basename(target.dir)]
893
+ .filter((part): part is string => part !== undefined && part !== "")
894
+ .join("/"),
895
+ }));
896
+
897
+ if (!ctx.json) {
898
+ ctx.err(
899
+ `Deploying "${slug}" as one v2 application with ${targets.length} components (slug from the parent directory name).\n` +
900
+ ` architecture: v2\n` +
901
+ ` source: ${gitRepoUrl}${gitRef === undefined ? "" : ` @ ${gitRef}`}\n` +
902
+ components
903
+ .map(
904
+ (component) =>
905
+ ` component: ${component.componentKey} (${component.agentPath ?? "."})\n`
906
+ )
907
+ .join("")
908
+ );
909
+ }
910
+
911
+ // Components bake sequentially on the control plane, so the default wait
912
+ // budget is per component; otherwise a 2+ component deploy routinely hits
913
+ // the client-side timeout while the server-side deploy is still fine. An
914
+ // explicitly passed timeout always wins.
915
+ const waitCtx =
916
+ options.timeoutMs === undefined
917
+ ? { ...ctx, timeoutMs: ctx.timeoutMs * targets.length }
918
+ : ctx;
919
+
920
+ return deployOneAgentsdk(waitCtx, undefined, {
921
+ slug,
922
+ gitRepoUrl,
923
+ gitRef,
924
+ agentPath: undefined,
925
+ components,
926
+ repositories: options.cursorEventsRepos,
927
+ });
928
+ }
929
+
780
930
  /**
781
931
  * `architecture: "v2"` deploy: POST `/internal/agentkit/deployments` and
782
932
  * poll the matching GET. The v2 accept path starts freeze bake; it does
@@ -790,10 +940,17 @@ async function deployOneAgentsdk(
790
940
  gitRepoUrl: string;
791
941
  gitRef: string | undefined;
792
942
  agentPath: string | undefined;
943
+ components?: AgentkitDeployComponent[];
793
944
  repositories: string[] | undefined;
794
945
  }
795
946
  ): Promise<number> {
796
947
  const { slug } = args;
948
+ const componentsLine =
949
+ args.components === undefined
950
+ ? ""
951
+ : ` components: ${args.components
952
+ .map((component) => component.componentKey)
953
+ .join(", ")}\n`;
797
954
  let accepted: AgentkitDeployAccepted;
798
955
  try {
799
956
  accepted = await ctx.client.deployAgentkit({
@@ -802,6 +959,7 @@ async function deployOneAgentsdk(
802
959
  gitRepoUrl: args.gitRepoUrl,
803
960
  gitRef: args.gitRef,
804
961
  agentPath: args.agentPath,
962
+ components: args.components,
805
963
  repositories: args.repositories,
806
964
  });
807
965
  } catch (error) {
@@ -817,7 +975,8 @@ async function deployOneAgentsdk(
817
975
  ctx.out(
818
976
  `Deploy accepted: ${slug} (team ${ctx.teamId}, v2)\n` +
819
977
  ` application: ${accepted.applicationId}\n` +
820
- ` release: ${accepted.releaseId}\n`
978
+ ` release: ${accepted.releaseId}\n` +
979
+ componentsLine
821
980
  );
822
981
  }
823
982
  return 0;
@@ -851,7 +1010,12 @@ async function deployOneAgentsdk(
851
1010
  ` status: running\n` +
852
1011
  ` architecture: v2\n` +
853
1012
  ` application: ${detail.applicationId}\n` +
854
- ` release: ${detail.releaseId}\n`
1013
+ ` release: ${detail.releaseId}\n` +
1014
+ (args.components === undefined
1015
+ ? ""
1016
+ : ` components: ${args.components
1017
+ .map((component) => component.componentKey)
1018
+ .join(", ")}\n`)
855
1019
  );
856
1020
  return 0;
857
1021
  }
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Durable mirror for the SDK runner's conversation store.
3
+ *
4
+ * The Cursor SDK keeps each local conversation in SQLite under the engine's
5
+ * state root (`runner/index.db` + `runner/agents/agent-<hash>/store.db`).
6
+ * That disk dies with the pod: on architecture v2, session **records** and
7
+ * event chunks survive replacement through the control-plane storage mirror
8
+ * (framework-storage-selection.ts), but the conversation itself was never
9
+ * hydrated anywhere — a replacement pod resumed every thread as a fresh
10
+ * conversation.
11
+ *
12
+ * This module mirrors the conversation through the SDK's own store APIs —
13
+ * the agent document (which carries `sdkMetadata.blobEncryptionKey`, without
14
+ * which the checkpoint blobs cannot be decrypted) plus its content-addressed
15
+ * checkpoint blobs — into the deployment's shared file storage. On
16
+ * Cursor-managed hosting that is the Agent Store reached over
17
+ * `/v0/agent-serve-files` presigned URLs: the store is **not mounted** as a
18
+ * filesystem in the engine VM (the FUSE mount at
19
+ * {@link DEPLOYMENT_STORE_MOUNT_PATH} exists only on cloud-runtime VMs), so
20
+ * the mirror moves bytes through the {@link FileSink} contract instead.
21
+ *
22
+ * Going through the store APIs rather than copying `store.db` files sidesteps
23
+ * WAL consistency and the shared `index.db` (per-session raw copies would
24
+ * clobber other sessions' rows); checkpoint blobs are content-addressed, so
25
+ * repeat persists upload only what is new.
26
+ *
27
+ * Timing: the runner hydrates before `Agent.resume` (so a follow-up landing
28
+ * on a fresh pod — including a v2 hosted-execution delivery — finds the
29
+ * conversation before the turn runs) and persists after each local turn
30
+ * settles, **awaited**, because a hosted pod can be parked immediately after
31
+ * the execution reply.
32
+ *
33
+ * Run rows and run events are deliberately not mirrored: they are per-pod
34
+ * bookkeeping (turn numbers, stream offsets), and the durable session event
35
+ * stream already lives in the control-plane storage mirror.
36
+ */
37
+
38
+ import type { LocalAgentDocument, LocalAgentStore } from "@cursor/sdk";
39
+ import { agentStoreKeys, type FileSink } from "../files.js";
40
+ import {
41
+ cursorHostedFiles,
42
+ isCursorHostedFilesAvailable,
43
+ } from "../files-backends/cursor-hosted.js";
44
+ import type { AgentProject } from "../types.js";
45
+ import { resolveApiKeySync } from "./cursor/credentials.js";
46
+ import { AGENT_SERVE_GENERATION_ENV_VAR } from "./cursor-relay-core.js";
47
+ import { describeError } from "./describe-error.js";
48
+
49
+ /**
50
+ * Persist/restore one SDK conversation between a {@link LocalAgentStore} and
51
+ * durable file storage. Implemented by {@link createConversationMirror};
52
+ * consumed by the SDK runner.
53
+ */
54
+ export interface ConversationMirror {
55
+ /**
56
+ * Restore `sdkAgentId`'s conversation into `store` when the store does not
57
+ * already hold it. Resolves `true` when a conversation was restored,
58
+ * `false` on a definitive miss (never persisted, or already local — the
59
+ * caller falls through to its fresh-conversation path). A sink **failure**
60
+ * propagates: an outage must fail the turn (retryable) rather than
61
+ * silently fork the thread onto a fresh conversation.
62
+ */
63
+ hydrate(store: LocalAgentStore, sdkAgentId: string): Promise<boolean>;
64
+ /**
65
+ * Mirror `sdkAgentId`'s conversation from `store` to the sink
66
+ * (last write wins). No-op until the conversation has a checkpoint.
67
+ */
68
+ persist(store: LocalAgentStore, sdkAgentId: string): Promise<void>;
69
+ }
70
+
71
+ /** Wire shape at {@link agentStoreKeys.conversationManifest}. */
72
+ interface ConversationManifestV1 {
73
+ v: 1;
74
+ /** The SDK agent document, verbatim apart from `activeRunId` (nulled). */
75
+ agent: LocalAgentDocument;
76
+ /** Checkpoint blob ids present under the conversation's blob prefix. */
77
+ blobIds: string[];
78
+ persistedAtMs: number;
79
+ }
80
+
81
+ export interface CreateConversationMirrorOptions {
82
+ sink: FileSink;
83
+ /** Agent name baked into every minted key. */
84
+ agentName: string;
85
+ logger?: (line: string) => void;
86
+ }
87
+
88
+ /** Build a {@link ConversationMirror} over a {@link FileSink}. */
89
+ export function createConversationMirror(
90
+ options: CreateConversationMirrorOptions
91
+ ): ConversationMirror {
92
+ const { sink, agentName } = options;
93
+ const logger = options.logger ?? (() => {});
94
+ /** Blob ids known to be in the sink, per SDK agent id (upload dedupe). */
95
+ const mirrored = new Map<string, Set<string>>();
96
+ /**
97
+ * Per-conversation serialization so a persist cannot interleave with a
98
+ * hydrate (or another persist) for the same conversation.
99
+ */
100
+ const chains = new Map<string, Promise<unknown>>();
101
+
102
+ function serialize<T>(sdkAgentId: string, run: () => Promise<T>): Promise<T> {
103
+ const previous = chains.get(sdkAgentId) ?? Promise.resolve();
104
+ const next = previous.then(run, run);
105
+ chains.set(
106
+ sdkAgentId,
107
+ next.catch(() => {})
108
+ );
109
+ return next;
110
+ }
111
+
112
+ async function mirroredBlobIds(sdkAgentId: string): Promise<Set<string>> {
113
+ const cached = mirrored.get(sdkAgentId);
114
+ if (cached !== undefined) {
115
+ return cached;
116
+ }
117
+ // Seed from the previous manifest so a fresh pod does not re-upload the
118
+ // whole history on its first persist. A miss (or parse failure) seeds
119
+ // empty — re-uploading is wasteful, never wrong.
120
+ const seeded = new Set<string>();
121
+ try {
122
+ const manifest = await readManifest(sink, agentName, sdkAgentId);
123
+ for (const blobId of manifest?.blobIds ?? []) {
124
+ seeded.add(blobId);
125
+ }
126
+ } catch (error) {
127
+ logger(
128
+ `[agent-sdk] conversation mirror: could not read the prior manifest for ${sdkAgentId} (${describeError(error)}); re-uploading blobs`
129
+ );
130
+ }
131
+ mirrored.set(sdkAgentId, seeded);
132
+ return seeded;
133
+ }
134
+
135
+ return {
136
+ persist(store, sdkAgentId): Promise<void> {
137
+ return serialize(sdkAgentId, async () => {
138
+ const document = await store.agents.get({ agentId: sdkAgentId });
139
+ if (document === null || document.latestCheckpoint == null) {
140
+ // Nothing restorable yet (no conversation, or no checkpoint).
141
+ return;
142
+ }
143
+ const uploaded = await mirroredBlobIds(sdkAgentId);
144
+ const blobIds = await listCheckpointBlobIds(store, sdkAgentId);
145
+ for (const blobId of blobIds) {
146
+ if (uploaded.has(blobId)) {
147
+ continue;
148
+ }
149
+ const bytes = await store.checkpoints.get({
150
+ agentId: sdkAgentId,
151
+ blobId,
152
+ });
153
+ if (bytes === null) {
154
+ continue;
155
+ }
156
+ await sink.put(
157
+ agentStoreKeys.conversationBlob(agentName, sdkAgentId, blobId),
158
+ bytes
159
+ );
160
+ // Content-addressed: once in the sink, never re-uploaded.
161
+ uploaded.add(blobId);
162
+ }
163
+ const manifest: ConversationManifestV1 = {
164
+ v: 1,
165
+ // The referenced run is pod-local bookkeeping and is not mirrored;
166
+ // a hydrated store must not point at a run row it does not hold.
167
+ agent: { ...document, activeRunId: null },
168
+ blobIds: blobIds.filter((blobId) => uploaded.has(blobId)),
169
+ persistedAtMs: Date.now(),
170
+ };
171
+ // Manifest last: it only ever references blobs already uploaded.
172
+ await sink.put(
173
+ agentStoreKeys.conversationManifest(agentName, sdkAgentId),
174
+ new TextEncoder().encode(JSON.stringify(manifest))
175
+ );
176
+ });
177
+ },
178
+
179
+ hydrate(store, sdkAgentId): Promise<boolean> {
180
+ return serialize(sdkAgentId, async () => {
181
+ const existing = await store.agents.get({ agentId: sdkAgentId });
182
+ if (existing !== null) {
183
+ return false;
184
+ }
185
+ const manifest = await readManifest(sink, agentName, sdkAgentId);
186
+ if (manifest === undefined) {
187
+ return false;
188
+ }
189
+ const rootBlobId = manifest.agent.latestCheckpoint?.rootBlobId;
190
+ const restored = new Set<string>();
191
+ for (const blobId of manifest.blobIds) {
192
+ const body = await sink.get(
193
+ agentStoreKeys.conversationBlob(agentName, sdkAgentId, blobId)
194
+ );
195
+ const bytes =
196
+ body === undefined
197
+ ? undefined
198
+ : body instanceof Uint8Array
199
+ ? body
200
+ : body.body;
201
+ if (bytes === undefined) {
202
+ // Definitive absence (never uploaded, or deleted). A missing
203
+ // root blob makes the conversation unrestorable — fall through
204
+ // to the caller's fresh-conversation path rather than creating
205
+ // an agent row whose checkpoint cannot load.
206
+ if (blobId === rootBlobId) {
207
+ logger(
208
+ `[agent-sdk] conversation mirror: the root checkpoint blob for ${sdkAgentId} is missing from the sink; treating the conversation as unrestorable`
209
+ );
210
+ return false;
211
+ }
212
+ continue;
213
+ }
214
+ if (
215
+ (await store.checkpoints.get({ agentId: sdkAgentId, blobId })) ===
216
+ null
217
+ ) {
218
+ await store.checkpoints.create({
219
+ agentId: sdkAgentId,
220
+ blobId,
221
+ data: bytes,
222
+ });
223
+ }
224
+ restored.add(blobId);
225
+ }
226
+ if (rootBlobId !== undefined && !restored.has(rootBlobId)) {
227
+ logger(
228
+ `[agent-sdk] conversation mirror: the manifest for ${sdkAgentId} does not list its root checkpoint blob; treating the conversation as unrestorable`
229
+ );
230
+ return false;
231
+ }
232
+ await store.agents.create({
233
+ agent: { ...manifest.agent, activeRunId: null },
234
+ });
235
+ mirrored.set(sdkAgentId, restored);
236
+ logger(
237
+ `[agent-sdk] conversation mirror: restored the conversation for ${sdkAgentId} (${restored.size} checkpoint blob(s))`
238
+ );
239
+ return true;
240
+ });
241
+ },
242
+ };
243
+ }
244
+
245
+ async function readManifest(
246
+ sink: FileSink,
247
+ agentName: string,
248
+ sdkAgentId: string
249
+ ): Promise<ConversationManifestV1 | undefined> {
250
+ const body = await sink.get(
251
+ agentStoreKeys.conversationManifest(agentName, sdkAgentId)
252
+ );
253
+ if (body === undefined) {
254
+ return undefined;
255
+ }
256
+ const bytes = body instanceof Uint8Array ? body : body.body;
257
+ const parsed: unknown = JSON.parse(new TextDecoder().decode(bytes));
258
+ if (
259
+ parsed === null ||
260
+ typeof parsed !== "object" ||
261
+ (parsed as { v?: unknown }).v !== 1
262
+ ) {
263
+ throw new Error(
264
+ `conversation mirror: unsupported manifest for ${sdkAgentId}`
265
+ );
266
+ }
267
+ const manifest = parsed as ConversationManifestV1;
268
+ if (
269
+ manifest.agent === null ||
270
+ typeof manifest.agent !== "object" ||
271
+ manifest.agent.agentId !== sdkAgentId ||
272
+ !Array.isArray(manifest.blobIds)
273
+ ) {
274
+ throw new Error(
275
+ `conversation mirror: malformed manifest for ${sdkAgentId}`
276
+ );
277
+ }
278
+ return manifest;
279
+ }
280
+
281
+ async function listCheckpointBlobIds(
282
+ store: LocalAgentStore,
283
+ sdkAgentId: string
284
+ ): Promise<string[]> {
285
+ const blobIds: string[] = [];
286
+ let cursor: string | undefined;
287
+ do {
288
+ const page = await store.checkpoints.list({
289
+ filter: { agentIds: [sdkAgentId], cursor },
290
+ });
291
+ blobIds.push(...page.items);
292
+ cursor = page.nextCursor;
293
+ } while (cursor !== undefined);
294
+ return blobIds;
295
+ }
296
+
297
+ /**
298
+ * Whether (and over which sink) this serve process mirrors conversations.
299
+ *
300
+ * Selected for architecture-v2 agents on Cursor-managed hosting — the same
301
+ * generation signal as framework-storage-selection.ts, plus a provisioned
302
+ * deployment file store. Self-hosted v2 keeps everything on the state root
303
+ * (the operator owns the disk), and v1 keeps today's behavior while it runs.
304
+ */
305
+ export function selectConversationMirror(input: {
306
+ project: AgentProject;
307
+ logger?: (line: string) => void;
308
+ env?: Record<string, string | undefined>;
309
+ }): ConversationMirror | undefined {
310
+ if (input.project.agent.architecture !== "v2") {
311
+ return undefined;
312
+ }
313
+ const env = input.env ?? process.env;
314
+ if (
315
+ env[AGENT_SERVE_GENERATION_ENV_VAR] === undefined ||
316
+ !isCursorHostedFilesAvailable(env as NodeJS.ProcessEnv)
317
+ ) {
318
+ return undefined;
319
+ }
320
+ return createConversationMirror({
321
+ sink: cursorHostedFiles({
322
+ // Full chain (env → key file → stored login): frozen engines listen
323
+ // unsigned and bind CURSOR_API_KEY_FILE at task time, after this sink
324
+ // is constructed.
325
+ apiKey: () => resolveApiKeySync()?.apiKey,
326
+ }),
327
+ agentName: input.project.name,
328
+ logger: input.logger,
329
+ });
330
+ }
@@ -0,0 +1,61 @@
1
+ import type { ChannelSession } from "../types.js";
2
+
3
+ const deferredSessions = new WeakSet<ChannelSession>();
4
+
5
+ /**
6
+ * Authored code touched a member of the deferred handle that needs the live
7
+ * session. This is deterministic (the session never exists during admission),
8
+ * so the admission route surfaces it as a non-retryable 422 rather than a
9
+ * retryable failure.
10
+ */
11
+ export class DeferredChannelSessionError extends Error {
12
+ constructor(member: string) {
13
+ super(
14
+ `ChannelSession.${member} is not available on a send() captured ` +
15
+ "during hosted admission: the control plane allocates the session " +
16
+ "(and runs the turn) only after this delivery is acknowledged, so " +
17
+ "the handle cannot observe it yet. Move work that needs the live " +
18
+ "session into the session turn instead."
19
+ );
20
+ }
21
+ }
22
+
23
+ function unavailable(member: string): never {
24
+ throw new DeferredChannelSessionError(member);
25
+ }
26
+
27
+ /**
28
+ * Inert `ChannelSession` returned by the hosted-admission `send` stub: every
29
+ * member fails loudly via {@link DeferredChannelSessionError}. Caveat: new
30
+ * OPTIONAL members added to `ChannelSession` slip through this factory
31
+ * silently (reads return `undefined`) until a throwing getter is added here.
32
+ */
33
+ export function createDeferredChannelSession(): ChannelSession {
34
+ const session: ChannelSession = {
35
+ get id(): string {
36
+ return unavailable("id");
37
+ },
38
+ get continuationToken(): string | null {
39
+ return unavailable("continuationToken");
40
+ },
41
+ get isNew(): boolean {
42
+ return unavailable("isNew");
43
+ },
44
+ get coalesced(): boolean | undefined {
45
+ return unavailable("coalesced");
46
+ },
47
+ get info(): ChannelSession["info"] {
48
+ return unavailable("info");
49
+ },
50
+ getEventStream: async () => unavailable("getEventStream()"),
51
+ waitForCompletion: async () => unavailable("waitForCompletion()"),
52
+ setContinuationToken: async () => unavailable("setContinuationToken()"),
53
+ };
54
+ deferredSessions.add(session);
55
+ return session;
56
+ }
57
+
58
+ /** True when `send()` returned a hosted-admission capture handle. */
59
+ export function isDeferredChannelSession(session: ChannelSession): boolean {
60
+ return deferredSessions.has(session);
61
+ }
@@ -224,6 +224,16 @@ export interface AgentkitDeployAccepted {
224
224
  status: string;
225
225
  }
226
226
 
227
+ /**
228
+ * One release component of a v2 multi-component deploy: a stable key (the
229
+ * child agent's slug) plus that agent's directory relative to the repo
230
+ * root.
231
+ */
232
+ export interface AgentkitDeployComponent {
233
+ componentKey: string;
234
+ agentPath?: string;
235
+ }
236
+
227
237
  /** Customer-visible v2 deploy status (`GET /internal/agentkit/deployments/:slug`). */
228
238
  export type AgentkitCustomerStatus =
229
239
  | "pending"
@@ -576,7 +586,9 @@ export class AgentServeDeployClient {
576
586
  /**
577
587
  * Create or redeploy onto the v2 control plane
578
588
  * (`POST /internal/agentkit/deployments`). 202 means accepted; poll
579
- * {@link getAgentkitDeploy} until a terminal status.
589
+ * {@link getAgentkitDeploy} until a terminal status. `components` deploys
590
+ * one application with several release components (one per agent
591
+ * directory); it is mutually exclusive with `agentPath`.
580
592
  */
581
593
  async deployAgentkit(args: {
582
594
  teamId: number;
@@ -584,12 +596,22 @@ export class AgentServeDeployClient {
584
596
  gitRepoUrl: string;
585
597
  gitRef?: string;
586
598
  agentPath?: string;
599
+ components?: AgentkitDeployComponent[];
587
600
  repositories?: string[];
588
601
  }): Promise<AgentkitDeployAccepted> {
589
602
  const slugError = validateDeploymentSlug(args.slug);
590
603
  if (slugError !== undefined) {
591
604
  throw new DeployApiError(slugError, 400);
592
605
  }
606
+ for (const component of args.components ?? []) {
607
+ const keyError = validateDeploymentSlug(component.componentKey);
608
+ if (keyError !== undefined) {
609
+ throw new DeployApiError(
610
+ `Invalid component key ${JSON.stringify(component.componentKey)}: use lowercase letters, digits, '-' or '_' (max 64 chars)`,
611
+ 400
612
+ );
613
+ }
614
+ }
593
615
  const raw = await this.request({
594
616
  method: "POST",
595
617
  path: "/internal/agentkit/deployments",
@@ -599,6 +621,7 @@ export class AgentServeDeployClient {
599
621
  gitRepoUrl: args.gitRepoUrl,
600
622
  gitRef: args.gitRef,
601
623
  agentPath: args.agentPath,
624
+ components: args.components,
602
625
  repositories:
603
626
  args.repositories !== undefined && args.repositories.length > 0
604
627
  ? args.repositories