@cursor/july 0.1.10 → 0.1.11

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 (194) hide show
  1. package/AGENTS.md +1 -0
  2. package/dist/bin/agent-serve.js +17 -3
  3. package/dist/channels/github/github-channel.d.ts.map +1 -1
  4. package/dist/channels/github/github-channel.js +2 -1
  5. package/dist/channels/github/types.d.ts +4 -0
  6. package/dist/channels/github/types.d.ts.map +1 -1
  7. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  8. package/dist/channels/slack/cursor-account.js +1 -1
  9. package/dist/channels/slack/dispatch.d.ts +13 -0
  10. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  11. package/dist/channels/slack/dispatch.js +36 -11
  12. package/dist/channels/slack/eval-directive.d.ts +63 -0
  13. package/dist/channels/slack/eval-directive.d.ts.map +1 -0
  14. package/dist/channels/slack/eval-directive.js +283 -0
  15. package/dist/channels/slack/index.d.ts +1 -0
  16. package/dist/channels/slack/index.d.ts.map +1 -1
  17. package/dist/channels/slack/index.js +1 -0
  18. package/dist/channels/slack/post-update-delivery.js +318 -0
  19. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  20. package/dist/channels/slack/slack-channel.js +4 -2
  21. package/dist/channels/slack/types.d.ts +7 -0
  22. package/dist/channels/slack/types.d.ts.map +1 -1
  23. package/dist/connections.d.ts +10 -5
  24. package/dist/connections.d.ts.map +1 -1
  25. package/dist/connections.js +10 -5
  26. package/dist/docs/404.html +2 -2
  27. package/dist/docs/ab.html +4 -4
  28. package/dist/docs/assets/{app.CY2buaEE.js → app.BcXUGca7.js} +1 -1
  29. package/dist/docs/assets/chunks/@localSearchIndexroot.BIbvK8M1.js +1 -0
  30. package/dist/docs/assets/chunks/{VPLocalSearchBox.Ix954SFJ.js → VPLocalSearchBox.CWur6XGZ.js} +1 -1
  31. package/dist/docs/assets/chunks/{theme.PDwuJbyi.js → theme.BnvulOX2.js} +2 -2
  32. package/dist/docs/assets/{deployment.md.DC5-ImcM.js → deployment.md.TecHo0_2.js} +3 -3
  33. package/dist/docs/assets/{deployment.md.DC5-ImcM.lean.js → deployment.md.TecHo0_2.lean.js} +1 -1
  34. package/dist/docs/assets/guides_mcp-oauth.md.DqmEOQY1.js +29 -0
  35. package/dist/docs/assets/guides_mcp-oauth.md.DqmEOQY1.lean.js +1 -0
  36. package/dist/docs/assets/{index.md.COiu-1jL.js → index.md.t0TM2Qzz.js} +3 -3
  37. package/dist/docs/assets/{index.md.COiu-1jL.lean.js → index.md.t0TM2Qzz.lean.js} +1 -1
  38. package/dist/docs/assets/{reference_cli.md.CjfiV2Q-.js → reference_cli.md.B74NrVzb.js} +3 -3
  39. package/dist/docs/assets/{reference_cli.md.CjfiV2Q-.lean.js → reference_cli.md.B74NrVzb.lean.js} +1 -1
  40. package/dist/docs/assets/{reference_connections.md.FbWupyzW.js → reference_connections.md.CsSr3rDz.js} +8 -3
  41. package/dist/docs/assets/{reference_connections.md.FbWupyzW.lean.js → reference_connections.md.CsSr3rDz.lean.js} +1 -1
  42. package/dist/docs/assets/reference_prompt.md.BaiweQxE.js +1 -0
  43. package/dist/docs/assets/reference_prompt.md.BaiweQxE.lean.js +1 -0
  44. package/dist/docs/assets/{reference_tools.md.CqgJroI0.js → reference_tools.md.BRSDnTbN.js} +16 -3
  45. package/dist/docs/assets/{reference_tools.md.CqgJroI0.lean.js → reference_tools.md.BRSDnTbN.lean.js} +1 -1
  46. package/dist/docs/assets/{troubleshooting.md.DkovjS7h.js → troubleshooting.md.CmQkmnzC.js} +1 -1
  47. package/dist/docs/assets/{troubleshooting.md.DkovjS7h.lean.js → troubleshooting.md.CmQkmnzC.lean.js} +1 -1
  48. package/dist/docs/building-with-agents.html +4 -4
  49. package/dist/docs/concepts.html +4 -4
  50. package/dist/docs/deployment.html +7 -7
  51. package/dist/docs/evals.html +4 -4
  52. package/dist/docs/example-agents/approval-buddy.html +4 -4
  53. package/dist/docs/example-agents/benny.html +4 -4
  54. package/dist/docs/example-agents/bugbot.html +4 -4
  55. package/dist/docs/example-agents/codebase-wiki.html +4 -4
  56. package/dist/docs/example-agents/codeowners-review.html +4 -4
  57. package/dist/docs/example-agents/concierge.html +4 -4
  58. package/dist/docs/example-agents/fsd.html +4 -4
  59. package/dist/docs/example-agents/index.html +4 -4
  60. package/dist/docs/example-agents/knowledge-base.html +4 -4
  61. package/dist/docs/example-agents/oncall.html +4 -4
  62. package/dist/docs/example-agents/security-reviewer.html +4 -4
  63. package/dist/docs/example-agents/slack-agent.html +4 -4
  64. package/dist/docs/example-agents/weather-agent.html +4 -4
  65. package/dist/docs/guides/agent-to-agent.html +4 -4
  66. package/dist/docs/guides/cloud-runtime.html +4 -4
  67. package/dist/docs/guides/github.html +4 -4
  68. package/dist/docs/guides/human-in-the-loop.html +4 -4
  69. package/dist/docs/guides/mcp-oauth.html +53 -0
  70. package/dist/docs/guides/slack.html +4 -4
  71. package/dist/docs/guides/webhooks.html +4 -4
  72. package/dist/docs/hashmap.json +1 -1
  73. package/dist/docs/hillclimbing.html +4 -4
  74. package/dist/docs/index.html +6 -6
  75. package/dist/docs/quickstart.html +4 -4
  76. package/dist/docs/reference/agent-config.html +4 -4
  77. package/dist/docs/reference/channels.html +4 -4
  78. package/dist/docs/reference/cli.html +6 -6
  79. package/dist/docs/reference/connections.html +12 -7
  80. package/dist/docs/reference/hooks.html +4 -4
  81. package/dist/docs/reference/http-api.html +4 -4
  82. package/dist/docs/reference/instructions.html +4 -4
  83. package/dist/docs/reference/playground.html +4 -4
  84. package/dist/docs/reference/project-layout.html +4 -4
  85. package/dist/docs/reference/prompt.html +33 -0
  86. package/dist/docs/reference/schedules.html +4 -4
  87. package/dist/docs/reference/sessions.html +4 -4
  88. package/dist/docs/reference/skills.html +5 -5
  89. package/dist/docs/reference/subagents.html +4 -4
  90. package/dist/docs/reference/tools.html +20 -7
  91. package/dist/docs/scaffolding-agents.html +4 -4
  92. package/dist/docs/storage.html +4 -4
  93. package/dist/docs/troubleshooting.html +5 -5
  94. package/dist/evals.d.ts +39 -0
  95. package/dist/evals.d.ts.map +1 -1
  96. package/dist/index.d.ts +3 -1
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +2 -1
  99. package/dist/internal/cli-mcp-oauth.d.ts +16 -0
  100. package/dist/internal/cli-mcp-oauth.d.ts.map +1 -0
  101. package/dist/internal/cli-mcp-oauth.js +194 -0
  102. package/dist/internal/cli-mcp.d.ts +4 -1
  103. package/dist/internal/cli-mcp.d.ts.map +1 -1
  104. package/dist/internal/cli-mcp.js +19 -1
  105. package/dist/internal/corp-ca.d.ts +9 -0
  106. package/dist/internal/corp-ca.d.ts.map +1 -0
  107. package/dist/internal/corp-ca.js +44 -0
  108. package/dist/internal/cursor/account-mcp.d.ts +17 -4
  109. package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
  110. package/dist/internal/cursor/account-mcp.js +105 -17
  111. package/dist/internal/discovery.d.ts.map +1 -1
  112. package/dist/internal/discovery.js +64 -10
  113. package/dist/internal/distribution.d.ts.map +1 -1
  114. package/dist/internal/distribution.js +1 -0
  115. package/dist/internal/json-dir-store.js +100 -0
  116. package/dist/internal/logs-client.js +1 -1
  117. package/dist/internal/mcp-host.d.ts.map +1 -1
  118. package/dist/internal/mcp-host.js +49 -26
  119. package/dist/internal/mcp-oauth.d.ts +88 -0
  120. package/dist/internal/mcp-oauth.d.ts.map +1 -0
  121. package/dist/internal/mcp-oauth.js +385 -0
  122. package/dist/internal/resolved-connections.d.ts.map +1 -1
  123. package/dist/internal/resolved-connections.js +4 -1
  124. package/dist/internal/sdk-runner.d.ts +9 -0
  125. package/dist/internal/sdk-runner.d.ts.map +1 -1
  126. package/dist/internal/sdk-runner.js +28 -2
  127. package/dist/internal/server.d.ts.map +1 -1
  128. package/dist/internal/server.js +13 -5
  129. package/dist/internal/session-engine.d.ts +7 -9
  130. package/dist/internal/session-engine.d.ts.map +1 -1
  131. package/dist/internal/session-engine.js +96 -64
  132. package/dist/internal/sessions-client.d.ts.map +1 -1
  133. package/dist/internal/sessions-client.js +2 -1
  134. package/dist/internal/stream-progress.d.ts.map +1 -1
  135. package/dist/internal/stream-progress.js +8 -4
  136. package/dist/internal/turn-governor.d.ts +54 -0
  137. package/dist/internal/turn-governor.d.ts.map +1 -0
  138. package/dist/internal/turn-governor.js +111 -0
  139. package/dist/playground/assets/{index-C0-QEG4O.js → index-BIGLubwK.js} +46 -46
  140. package/dist/playground/assets/{index-z3JY9O2x.css → index-C17ZQKDG.css} +1 -1
  141. package/dist/playground/index.html +2 -2
  142. package/dist/prompt.d.ts +39 -0
  143. package/dist/prompt.d.ts.map +1 -0
  144. package/dist/prompt.js +70 -0
  145. package/dist/types.d.ts +78 -5
  146. package/dist/types.d.ts.map +1 -1
  147. package/dist/types.js +6 -0
  148. package/docs/.vitepress/cache/deps/package.json +3 -0
  149. package/docs/README.md +4 -0
  150. package/docs/deployment.md +6 -1
  151. package/docs/guides/mcp-oauth.md +157 -0
  152. package/docs/reference/cli.md +28 -0
  153. package/docs/reference/connections.md +28 -0
  154. package/docs/reference/prompt.md +47 -0
  155. package/docs/reference/tools.md +21 -0
  156. package/docs/troubleshooting.md +11 -0
  157. package/package.json +9 -2
  158. package/skills/create-agent/SKILL.md +7 -3
  159. package/skills/framework-map/SKILL.md +1 -0
  160. package/skills/mcp-auth/SKILL.md +138 -0
  161. package/src/bin/agent-serve.ts +20 -3
  162. package/src/channels/github/github-channel.ts +2 -0
  163. package/src/channels/github/types.ts +4 -0
  164. package/src/channels/slack/cursor-account.ts +2 -0
  165. package/src/channels/slack/dispatch.ts +49 -0
  166. package/src/channels/slack/eval-directive.ts +348 -0
  167. package/src/channels/slack/index.ts +1 -0
  168. package/src/channels/slack/slack-channel.ts +3 -0
  169. package/src/channels/slack/types.ts +7 -0
  170. package/src/connections.ts +10 -5
  171. package/src/evals.ts +43 -0
  172. package/src/index.ts +3 -0
  173. package/src/internal/cli-mcp-oauth.ts +254 -0
  174. package/src/internal/cli-mcp.ts +25 -2
  175. package/src/internal/cursor/account-mcp.ts +153 -41
  176. package/src/internal/discovery.ts +70 -8
  177. package/src/internal/distribution.ts +1 -0
  178. package/src/internal/logs-client.ts +1 -1
  179. package/src/internal/mcp-host.ts +33 -9
  180. package/src/internal/mcp-oauth.ts +496 -0
  181. package/src/internal/resolved-connections.ts +6 -1
  182. package/src/internal/sdk-runner.ts +37 -1
  183. package/src/internal/server.ts +13 -5
  184. package/src/internal/session-engine.ts +115 -74
  185. package/src/internal/sessions-client.ts +2 -1
  186. package/src/internal/stream-progress.ts +8 -0
  187. package/src/internal/turn-governor.ts +145 -0
  188. package/src/prompt.ts +83 -0
  189. package/src/types.ts +76 -6
  190. package/dist/docs/assets/chunks/@localSearchIndexroot.OfDZ5CXg.js +0 -1
  191. package/dist/internal/persistence-coordinator.d.ts +0 -127
  192. package/dist/internal/persistence-coordinator.d.ts.map +0 -1
  193. package/dist/persistence.d.ts +0 -184
  194. package/dist/persistence.d.ts.map +0 -1
@@ -82,6 +82,7 @@ import type { AgentRunner } from "./sdk-runner.js";
82
82
  import { SessionStore } from "./session-store.js";
83
83
  import { StorageCoordinator } from "./storage-coordinator.js";
84
84
  import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
85
+ import { TurnGovernor } from "./turn-governor.js";
85
86
  import {
86
87
  buildAgentsMdContent,
87
88
  isNestedInGitRepo,
@@ -252,6 +253,8 @@ export class SessionEngine {
252
253
  private readonly turnAborts = new Map<string, AbortController>();
253
254
  /** Serializes turn admission per session (latest follow-up wins). */
254
255
  private readonly admissionChains = new Map<string, Promise<unknown>>();
256
+ /** FIFO turn slots under `concurrency.maxRunningTurns` (unlimited when unset). */
257
+ private readonly turnGovernor: TurnGovernor;
255
258
  /**
256
259
  * Per-session coalesce queue for `admission: "coalesce"` (Slack). Drained
257
260
  * into one follow-up turn when the active turn finishes without interrupt.
@@ -271,6 +274,7 @@ export class SessionEngine {
271
274
  private readonly approvals = new ApprovalGate();
272
275
  private readonly abCollector: ABCollector;
273
276
  private reminderApi: HostContext["reminders"];
277
+ private evalsApi: HostContext["evals"];
274
278
  private readonly kvApi: HostContext["kv"];
275
279
  /** Serve-time resolved connections (see {@link setResolvedConnections}). */
276
280
  private resolvedConnections: EngineResolvedConnection[] = [];
@@ -280,6 +284,7 @@ export class SessionEngine {
280
284
  this.stateRoot = options.stateRoot;
281
285
  this.runner = options.runner;
282
286
  this.logger = options.logger ?? (() => {});
287
+ this.turnGovernor = new TurnGovernor(options.project.agent.concurrency);
283
288
  this.sessions = new SessionStore(join(options.stateRoot, "sessions"));
284
289
  this.nudgeStore = new SlackNudgeStore(
285
290
  join(options.stateRoot, "slack-nudges")
@@ -350,13 +355,14 @@ export class SessionEngine {
350
355
  }
351
356
  }
352
357
 
353
- /** Shared host services (MCP / GitHub / Slack / reminders / kv / Slack nudges). */
358
+ /** Shared host services (MCP / GitHub / Slack / reminders / evals / kv / Slack nudges). */
354
359
  get host(): HostContext {
355
360
  return {
356
361
  mcp: this.mcpHost,
357
362
  github: this.platforms.github,
358
363
  slack: this.platforms.slack,
359
364
  reminders: this.reminderApi,
365
+ evals: this.evalsApi,
360
366
  kv: this.kvApi,
361
367
  slackNudges: this.slackNudgeHostApi(),
362
368
  };
@@ -384,6 +390,11 @@ export class SessionEngine {
384
390
  this.reminderApi = api;
385
391
  }
386
392
 
393
+ /** Attach the serve-host playground evals API (called from startServer). */
394
+ setEvals(api: HostContext["evals"]): void {
395
+ this.evalsApi = api;
396
+ }
397
+
387
398
  // ==========================================================================
388
399
  // Sending messages
389
400
  // ==========================================================================
@@ -779,11 +790,8 @@ export class SessionEngine {
779
790
  turnId,
780
791
  data: { text: message },
781
792
  });
782
- await this.appendEvent(sessionId, {
783
- type: "turn.started",
784
- turnId,
785
- data: {},
786
- });
793
+ // `turn.started` is appended by executeTurn once the turn holds a
794
+ // governor slot; saturated engines emit `turn.queued` first.
787
795
 
788
796
  const abort = new AbortController();
789
797
  this.turnAborts.set(sessionId, abort);
@@ -1001,65 +1009,91 @@ export class SessionEngine {
1001
1009
  };
1002
1010
  const mapper = new TurnEventMapper(emit);
1003
1011
 
1012
+ // Take a turn slot before any work happens. When every slot is busy the
1013
+ // turn waits FIFO — durable `turn.queued` marks the wait — and a
1014
+ // preempting follow-up (or interrupt) resolves the wait as cancelled
1015
+ // without the turn ever running.
1016
+ const permit = await this.turnGovernor.acquire({
1017
+ sessionId,
1018
+ signal,
1019
+ onQueued: (position) => {
1020
+ const maxRunningTurns = this.turnGovernor.maxRunningTurns ?? 0;
1021
+ emit({ type: "turn.queued", data: { position, maxRunningTurns } });
1022
+ this.logger(
1023
+ `[session] ${sessionId} ${turnId} queued at position ${position} (${maxRunningTurns} turn slot(s) busy)`
1024
+ );
1025
+ },
1026
+ });
1027
+
1004
1028
  let outcome: TurnOutcome;
1005
- try {
1006
- // Scaffold agent files into the session workspace even when the harness
1007
- // cwd is overridden (e.g. a PR worktree). A/B variant instruction
1008
- // overlays are applied via agentForSession.
1009
- const agent = this.agentForSession(record);
1010
- await materializeWorkspace({
1011
- agent,
1012
- workspaceDir,
1013
- seed: isFirstTurn,
1014
- });
1015
- const pendingFiles = record.pendingWorkspaceFiles;
1016
- if (pendingFiles !== undefined && Object.keys(pendingFiles).length > 0) {
1017
- await writeWorkspaceFiles(harnessCwd, pendingFiles);
1018
- await this.updateRecord(sessionId, (current) => {
1019
- const { pendingWorkspaceFiles: _cleared, ...rest } = current;
1020
- return rest;
1029
+ if (permit === undefined) {
1030
+ // Preempted while waiting for a slot; settle as an interrupted turn.
1031
+ outcome = { status: "cancelled" };
1032
+ } else {
1033
+ emit({ type: "turn.started", data: {} });
1034
+ try {
1035
+ // Scaffold agent files into the session workspace even when the harness
1036
+ // cwd is overridden (e.g. a PR worktree). A/B variant instruction
1037
+ // overlays are applied via agentForSession.
1038
+ const agent = this.agentForSession(record);
1039
+ await materializeWorkspace({
1040
+ agent,
1041
+ workspaceDir,
1042
+ seed: isFirstTurn,
1021
1043
  });
1044
+ const pendingFiles = record.pendingWorkspaceFiles;
1045
+ if (
1046
+ pendingFiles !== undefined &&
1047
+ Object.keys(pendingFiles).length > 0
1048
+ ) {
1049
+ await writeWorkspaceFiles(harnessCwd, pendingFiles);
1050
+ await this.updateRecord(sessionId, (current) => {
1051
+ const { pendingWorkspaceFiles: _cleared, ...rest } = current;
1052
+ return rest;
1053
+ });
1054
+ }
1055
+ // 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.
1060
+ const instructionsInvisibleToHarness =
1061
+ isFirstTurn &&
1062
+ (record.workspaceOverride !== undefined ||
1063
+ (await isNestedInGitRepo(harnessCwd)));
1064
+ const request = this.buildTurnRequest(
1065
+ record,
1066
+ agent,
1067
+ turnId,
1068
+ message,
1069
+ isFirstTurn,
1070
+ instructionsInvisibleToHarness,
1071
+ emit,
1072
+ async (update) => {
1073
+ mapper.handleUpdate(update);
1074
+ // Yield so the HTTP NDJSON writer can flush tool start/result
1075
+ // events while sibling parallel tools are still executing.
1076
+ if (
1077
+ update.type === "tool-call-started" ||
1078
+ update.type === "tool-call-completed" ||
1079
+ // Nested subagent progress (newer SDKs); string check keeps
1080
+ // typecheck green against older published `@cursor/sdk` pins.
1081
+ (update as { type: string }).type === "tool-call-delta"
1082
+ ) {
1083
+ await yieldEventLoop();
1084
+ }
1085
+ },
1086
+ signal
1087
+ );
1088
+ outcome = await this.runner.runTurn(request);
1089
+ } catch (error) {
1090
+ this.approvals.cancelSession(sessionId, "deny");
1091
+ outcome = { status: "error", errorMessage: describeError(error) };
1092
+ } finally {
1093
+ permit.release();
1094
+ this.approvals.cancelSession(sessionId, "deny");
1095
+ await this.clearPendingApprovals(sessionId);
1022
1096
  }
1023
- // The harness only loads the scaffolded AGENTS.md when the session
1024
- // workspace is its own project root. With a per-session cwd override
1025
- // (PR worktree) the scaffold lands elsewhere, and a workspace nested
1026
- // inside a git repo re-roots harness rules at that repo — in both
1027
- // cases deliver instructions inline on the first turn instead.
1028
- const instructionsInvisibleToHarness =
1029
- isFirstTurn &&
1030
- (record.workspaceOverride !== undefined ||
1031
- (await isNestedInGitRepo(harnessCwd)));
1032
- const request = this.buildTurnRequest(
1033
- record,
1034
- agent,
1035
- turnId,
1036
- message,
1037
- isFirstTurn,
1038
- instructionsInvisibleToHarness,
1039
- emit,
1040
- async (update) => {
1041
- mapper.handleUpdate(update);
1042
- // Yield so the HTTP NDJSON writer can flush tool start/result
1043
- // events while sibling parallel tools are still executing.
1044
- if (
1045
- update.type === "tool-call-started" ||
1046
- update.type === "tool-call-completed" ||
1047
- // Nested subagent progress (newer SDKs); string check keeps
1048
- // typecheck green against older published `@cursor/sdk` pins.
1049
- (update as { type: string }).type === "tool-call-delta"
1050
- ) {
1051
- await yieldEventLoop();
1052
- }
1053
- },
1054
- signal
1055
- );
1056
- outcome = await this.runner.runTurn(request);
1057
- } catch (error) {
1058
- this.approvals.cancelSession(sessionId, "deny");
1059
- outcome = { status: "error", errorMessage: describeError(error) };
1060
- } finally {
1061
- this.approvals.cancelSession(sessionId, "deny");
1062
- await this.clearPendingApprovals(sessionId);
1063
1097
  }
1064
1098
 
1065
1099
  mapper.finish();
@@ -1224,38 +1258,42 @@ export class SessionEngine {
1224
1258
  };
1225
1259
  }
1226
1260
 
1227
- /**
1228
- * MCP servers for a turn: authored concrete connections plus resolved
1229
- * serve-time resolved connections (peers, Cursor account bridge).
1230
- * Resolved connections pick their URL by runtime — loopback for local
1231
- * turns, `cloudUrl` for cloud turns — and are omitted from cloud turns
1232
- * when no cloud URL exists unless a `servers` filter requires the bridge
1233
- * (see {@link assertCloudCanHonorAccountServersFilters}).
1234
- */
1261
+ /** Authored + resolved MCP servers for a turn; skips `hostOnly`. */
1235
1262
  private buildMcpServers(
1236
1263
  runtime: "local" | "cloud"
1237
1264
  ): Record<string, ConnectionTransport> {
1265
+ const hostOnlyNames = new Set(
1266
+ this.project.agent.connections
1267
+ .filter((connection) => connection.hostOnly === true)
1268
+ .map((connection) => connection.name)
1269
+ );
1238
1270
  assertCloudCanHonorAccountServersFilters({
1239
1271
  runtime,
1240
- resolvedConnections: this.resolvedConnections,
1272
+ resolvedConnections: this.resolvedConnections.filter(
1273
+ (resolved) => !hostOnlyNames.has(resolved.name)
1274
+ ),
1241
1275
  });
1242
1276
  const servers: Record<string, ConnectionTransport> = {};
1243
1277
  for (const connection of this.project.agent.connections) {
1278
+ if (hostOnlyNames.has(connection.name)) {
1279
+ continue;
1280
+ }
1244
1281
  if (isSymbolicConnectionTransport(connection.transport)) {
1245
1282
  continue;
1246
1283
  }
1247
1284
  servers[connection.name] = connection.transport;
1248
1285
  }
1249
1286
  for (const resolved of this.resolvedConnections) {
1287
+ if (hostOnlyNames.has(resolved.name)) {
1288
+ continue;
1289
+ }
1250
1290
  const url = runtime === "cloud" ? resolved.cloudUrl : resolved.localUrl;
1251
1291
  if (url === undefined) {
1252
1292
  continue;
1253
1293
  }
1254
1294
  servers[resolved.name] = {
1255
1295
  url,
1256
- ...(resolved.headers === undefined
1257
- ? {}
1258
- : { headers: resolved.headers }),
1296
+ headers: resolved.headers,
1259
1297
  };
1260
1298
  }
1261
1299
  return servers;
@@ -1923,6 +1961,9 @@ export class SessionEngine {
1923
1961
  running:
1924
1962
  this.activeTurns.has(record.sessionId) ||
1925
1963
  this.pendingTurns.has(record.sessionId),
1964
+ queued: this.turnGovernor.limited
1965
+ ? this.turnGovernor.isQueued(record.sessionId)
1966
+ : undefined,
1926
1967
  createdAt: record.createdAt,
1927
1968
  updatedAt: record.updatedAt,
1928
1969
  continuationToken:
@@ -165,6 +165,7 @@ function parseSessionSummary(value: unknown): SessionSummary | undefined {
165
165
  title: typeof row.title === "string" ? row.title : undefined,
166
166
  turnCount: typeof row.turnCount === "number" ? row.turnCount : 0,
167
167
  running: row.running === true,
168
+ queued: typeof row.queued === "boolean" ? row.queued : undefined,
168
169
  createdAt: typeof row.createdAt === "string" ? row.createdAt : "",
169
170
  updatedAt: typeof row.updatedAt === "string" ? row.updatedAt : "",
170
171
  continuationToken,
@@ -232,7 +233,7 @@ export function formatSessionsTable(
232
233
  s.channelId,
233
234
  s.mode,
234
235
  String(s.turnCount),
235
- s.running ? "yes" : "no",
236
+ s.queued === true ? "queued" : s.running ? "yes" : "no",
236
237
  s.updatedAt,
237
238
  ]);
238
239
  return `${sourceLine}${renderTable(header, rows)}\n`;
@@ -50,6 +50,14 @@ export function createStreamProgress(options?: {
50
50
  }
51
51
  break;
52
52
  }
53
+ case "turn.queued": {
54
+ write(
55
+ c.dim(
56
+ ` turn ${event.turnId ?? "?"} queued (position ${event.data.position}, max ${event.data.maxRunningTurns} running)\n`
57
+ )
58
+ );
59
+ break;
60
+ }
53
61
  case "turn.started": {
54
62
  turnClock = Date.now();
55
63
  if (event.turnId !== undefined) {
@@ -0,0 +1,145 @@
1
+ /**
2
+ * FIFO admission of model turns under `concurrency.maxRunningTurns`
3
+ * (see {@link AgentConcurrencyOptions}). One governor per session engine:
4
+ * every turn acquires a slot before its SDK run starts and releases it when
5
+ * the run settles, so at most `maxRunningTurns` turns execute at once and
6
+ * the rest wait in arrival order instead of failing.
7
+ *
8
+ * Waiting is cancellable: the engine passes the turn's abort signal, so a
9
+ * follow-up that preempts a queued turn (or an explicit `interrupt()`)
10
+ * removes the waiter without ever running it.
11
+ */
12
+
13
+ import type { AgentConcurrencyOptions } from "../types.js";
14
+
15
+ export interface TurnPermit {
16
+ /** Return the slot. Idempotent; wakes the oldest waiter, if any. */
17
+ release(): void;
18
+ }
19
+
20
+ export interface AcquireTurnSlotArgs {
21
+ sessionId: string;
22
+ /** Abort waiting (preemption / interrupt). Ignored once a slot is held. */
23
+ signal?: AbortSignal;
24
+ /**
25
+ * Called synchronously when the turn must wait, with its 1-based
26
+ * position among waiting turns at enqueue time.
27
+ */
28
+ onQueued?: (position: number) => void;
29
+ }
30
+
31
+ interface TurnWaiter {
32
+ sessionId: string;
33
+ grant: (permit: TurnPermit) => void;
34
+ cancel: () => void;
35
+ cleanup: () => void;
36
+ }
37
+
38
+ export class TurnGovernor {
39
+ /** The configured ceiling, or undefined for unlimited. */
40
+ readonly maxRunningTurns: number | undefined;
41
+ private running = 0;
42
+ private readonly waiters: TurnWaiter[] = [];
43
+ /** Sessions currently waiting for a slot (at most one turn per session). */
44
+ private readonly queuedSessions = new Set<string>();
45
+
46
+ constructor(concurrency: AgentConcurrencyOptions | undefined) {
47
+ this.maxRunningTurns = concurrency?.maxRunningTurns;
48
+ }
49
+
50
+ /** True when a `maxRunningTurns` ceiling is configured. */
51
+ get limited(): boolean {
52
+ return this.maxRunningTurns !== undefined;
53
+ }
54
+
55
+ /** True while `sessionId` has a turn waiting for a slot. */
56
+ isQueued(sessionId: string): boolean {
57
+ return this.queuedSessions.has(sessionId);
58
+ }
59
+
60
+ /** Turns currently waiting for a slot. */
61
+ get queuedCount(): number {
62
+ return this.waiters.length;
63
+ }
64
+
65
+ /** Turns currently holding a slot. */
66
+ get runningCount(): number {
67
+ return this.running;
68
+ }
69
+
70
+ /**
71
+ * Acquire a turn slot. Resolves with a permit once a slot is free (FIFO
72
+ * behind earlier waiters), or with `undefined` when the abort signal
73
+ * fires while waiting — the turn must then settle as cancelled without
74
+ * running. Unlimited governors grant immediately.
75
+ */
76
+ acquire(args: AcquireTurnSlotArgs): Promise<TurnPermit | undefined> {
77
+ const limit = this.maxRunningTurns;
78
+ if (limit === undefined) {
79
+ return Promise.resolve(NO_OP_PERMIT);
80
+ }
81
+ if (this.running < limit && this.waiters.length === 0) {
82
+ return Promise.resolve(this.grantSlot());
83
+ }
84
+ // Saturated. A turn that is already preempted never enqueues.
85
+ if (args.signal?.aborted === true) {
86
+ return Promise.resolve(undefined);
87
+ }
88
+ return new Promise<TurnPermit | undefined>((resolve) => {
89
+ const waiter: TurnWaiter = {
90
+ sessionId: args.sessionId,
91
+ grant: (permit) => resolve(permit),
92
+ cancel: () => resolve(undefined),
93
+ cleanup: () => {
94
+ this.queuedSessions.delete(args.sessionId);
95
+ args.signal?.removeEventListener("abort", onAbort);
96
+ },
97
+ };
98
+ const onAbort = (): void => {
99
+ const index = this.waiters.indexOf(waiter);
100
+ if (index === -1) {
101
+ return;
102
+ }
103
+ this.waiters.splice(index, 1);
104
+ waiter.cleanup();
105
+ waiter.cancel();
106
+ };
107
+ this.waiters.push(waiter);
108
+ this.queuedSessions.add(args.sessionId);
109
+ args.signal?.addEventListener("abort", onAbort, { once: true });
110
+ args.onQueued?.(this.waiters.length);
111
+ });
112
+ }
113
+
114
+ /** Take a slot and build its (idempotent) release handle. */
115
+ private grantSlot(): TurnPermit {
116
+ this.running += 1;
117
+ let released = false;
118
+ return {
119
+ release: () => {
120
+ if (released) {
121
+ return;
122
+ }
123
+ released = true;
124
+ this.running -= 1;
125
+ this.wakeNextWaiter();
126
+ },
127
+ };
128
+ }
129
+
130
+ private wakeNextWaiter(): void {
131
+ const limit = this.maxRunningTurns;
132
+ if (limit === undefined || this.running >= limit) {
133
+ return;
134
+ }
135
+ const waiter = this.waiters.shift();
136
+ if (waiter === undefined) {
137
+ return;
138
+ }
139
+ waiter.cleanup();
140
+ waiter.grant(this.grantSlot());
141
+ }
142
+ }
143
+
144
+ /** Permit handed out by unlimited governors. */
145
+ const NO_OP_PERMIT: TurnPermit = { release: () => {} };
package/src/prompt.ts ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Dedented multi-line strings for prompts, errors, and tool descriptions.
3
+ *
4
+ * Indent the body to match surrounding TypeScript; the common leading
5
+ * whitespace is stripped. A leading newline after `` prompt` `` is dropped so
6
+ * the usual multiline form stays flush in source.
7
+ *
8
+ * ```ts
9
+ * import { prompt } from "@cursor/july";
10
+ *
11
+ * export default defineTool({
12
+ * description: prompt`
13
+ * Deploy Anyship services for a merged PR.
14
+ * Enforces HITL outside business hours.
15
+ * `,
16
+ * // ...
17
+ * });
18
+ *
19
+ * throw new Error(prompt`
20
+ * It is outside business hours.
21
+ * Use request_author_approval.
22
+ * `);
23
+ *
24
+ * // GitHub channel `context` wants string[]:
25
+ * context: prompt.lines`
26
+ * Merged PR detected: ${url}.
27
+ *
28
+ * Run the deploy flow.
29
+ * `
30
+ * ```
31
+ */
32
+
33
+ function dedentLines(
34
+ strings: TemplateStringsArray,
35
+ values: readonly unknown[]
36
+ ): string[] {
37
+ let text = "";
38
+ for (let i = 0; i < strings.length; i++) {
39
+ text += strings[i] ?? "";
40
+ if (i < values.length) {
41
+ text += String(values[i]);
42
+ }
43
+ }
44
+ // Common form is prompt`\n …\n`; drop the opening newline only.
45
+ if (text.startsWith("\n")) {
46
+ text = text.slice(1);
47
+ }
48
+ text = text.replace(/(?:\n[ \t]*)+$/u, "");
49
+
50
+ const lines = text.split("\n");
51
+ let minIndent = Number.POSITIVE_INFINITY;
52
+ for (const line of lines) {
53
+ if (line.trim() === "") {
54
+ continue;
55
+ }
56
+ const indent = /^[ \t]*/u.exec(line)?.[0]?.length ?? 0;
57
+ if (indent < minIndent) {
58
+ minIndent = indent;
59
+ }
60
+ }
61
+ if (!Number.isFinite(minIndent)) {
62
+ minIndent = 0;
63
+ }
64
+ return lines.map((line) => (line.trim() === "" ? "" : line.slice(minIndent)));
65
+ }
66
+
67
+ export type PromptTag = {
68
+ (strings: TemplateStringsArray, ...values: unknown[]): string;
69
+ /** Same as {@link prompt}, but as separate lines (e.g. GitHub `context`). */
70
+ lines(strings: TemplateStringsArray, ...values: unknown[]): string[];
71
+ };
72
+
73
+ /** Dedented multi-line string (blank lines preserved). */
74
+ export const prompt: PromptTag = Object.assign(
75
+ function prompt(strings: TemplateStringsArray, ...values: unknown[]): string {
76
+ return dedentLines(strings, values).join("\n");
77
+ },
78
+ {
79
+ lines(strings: TemplateStringsArray, ...values: unknown[]): string[] {
80
+ return dedentLines(strings, values);
81
+ },
82
+ }
83
+ );