@cursor/july 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/dist/bin/agent-serve.js +17 -3
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +2 -1
- package/dist/channels/github/types.d.ts +4 -0
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/channels/slack/cursor-account.d.ts.map +1 -1
- package/dist/channels/slack/cursor-account.js +1 -1
- package/dist/channels/slack/dispatch.d.ts +13 -0
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +36 -11
- package/dist/channels/slack/eval-directive.d.ts +63 -0
- package/dist/channels/slack/eval-directive.d.ts.map +1 -0
- package/dist/channels/slack/eval-directive.js +283 -0
- package/dist/channels/slack/index.d.ts +1 -0
- package/dist/channels/slack/index.d.ts.map +1 -1
- package/dist/channels/slack/index.js +1 -0
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +4 -2
- package/dist/channels/slack/types.d.ts +7 -0
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/connections.d.ts +10 -5
- package/dist/connections.d.ts.map +1 -1
- package/dist/connections.js +10 -5
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +4 -4
- package/dist/docs/assets/{app.CY2buaEE.js → app.Dh_JkJjI.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.Er_R50vY.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.Ix954SFJ.js → VPLocalSearchBox.CptQNLkR.js} +1 -1
- package/dist/docs/assets/chunks/{theme.PDwuJbyi.js → theme.DWflH-LQ.js} +2 -2
- package/dist/docs/assets/{deployment.md.DC5-ImcM.js → deployment.md.TecHo0_2.js} +3 -3
- package/dist/docs/assets/{deployment.md.DC5-ImcM.lean.js → deployment.md.TecHo0_2.lean.js} +1 -1
- package/dist/docs/assets/guides_mcp-oauth.md.DqmEOQY1.js +29 -0
- package/dist/docs/assets/guides_mcp-oauth.md.DqmEOQY1.lean.js +1 -0
- package/dist/docs/assets/{index.md.COiu-1jL.js → index.md.t0TM2Qzz.js} +3 -3
- package/dist/docs/assets/{index.md.COiu-1jL.lean.js → index.md.t0TM2Qzz.lean.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.CjfiV2Q-.js → reference_cli.md.B74NrVzb.js} +3 -3
- package/dist/docs/assets/{reference_cli.md.CjfiV2Q-.lean.js → reference_cli.md.B74NrVzb.lean.js} +1 -1
- package/dist/docs/assets/{reference_connections.md.FbWupyzW.js → reference_connections.md.CsSr3rDz.js} +8 -3
- package/dist/docs/assets/{reference_connections.md.FbWupyzW.lean.js → reference_connections.md.CsSr3rDz.lean.js} +1 -1
- package/dist/docs/assets/reference_prompt.md.BaiweQxE.js +1 -0
- package/dist/docs/assets/reference_prompt.md.BaiweQxE.lean.js +1 -0
- package/dist/docs/assets/{reference_tools.md.CqgJroI0.js → reference_tools.md.BRSDnTbN.js} +16 -3
- package/dist/docs/assets/{reference_tools.md.CqgJroI0.lean.js → reference_tools.md.BRSDnTbN.lean.js} +1 -1
- package/dist/docs/assets/{troubleshooting.md.DkovjS7h.js → troubleshooting.md.CmQkmnzC.js} +1 -1
- package/dist/docs/assets/{troubleshooting.md.DkovjS7h.lean.js → troubleshooting.md.CmQkmnzC.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +7 -7
- package/dist/docs/evals.html +4 -4
- package/dist/docs/example-agents/approval-buddy.html +4 -4
- package/dist/docs/example-agents/benny.html +4 -4
- package/dist/docs/example-agents/bugbot.html +4 -4
- package/dist/docs/example-agents/codebase-wiki.html +4 -4
- package/dist/docs/example-agents/codeowners-review.html +4 -4
- package/dist/docs/example-agents/concierge.html +4 -4
- package/dist/docs/example-agents/fsd.html +4 -4
- package/dist/docs/example-agents/index.html +4 -4
- package/dist/docs/example-agents/knowledge-base.html +4 -4
- package/dist/docs/example-agents/oncall.html +4 -4
- package/dist/docs/example-agents/security-reviewer.html +4 -4
- package/dist/docs/example-agents/slack-agent.html +4 -4
- package/dist/docs/example-agents/weather-agent.html +4 -4
- package/dist/docs/guides/agent-to-agent.html +4 -4
- package/dist/docs/guides/cloud-runtime.html +4 -4
- package/dist/docs/guides/github.html +4 -4
- package/dist/docs/guides/human-in-the-loop.html +4 -4
- package/dist/docs/guides/mcp-oauth.html +53 -0
- package/dist/docs/guides/slack.html +4 -4
- package/dist/docs/guides/webhooks.html +4 -4
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +4 -4
- package/dist/docs/index.html +6 -6
- package/dist/docs/quickstart.html +4 -4
- package/dist/docs/reference/agent-config.html +4 -4
- package/dist/docs/reference/channels.html +4 -4
- package/dist/docs/reference/cli.html +6 -6
- package/dist/docs/reference/connections.html +12 -7
- package/dist/docs/reference/hooks.html +4 -4
- package/dist/docs/reference/http-api.html +4 -4
- package/dist/docs/reference/instructions.html +4 -4
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/prompt.html +33 -0
- package/dist/docs/reference/schedules.html +4 -4
- package/dist/docs/reference/sessions.html +4 -4
- package/dist/docs/reference/skills.html +5 -5
- package/dist/docs/reference/subagents.html +4 -4
- package/dist/docs/reference/tools.html +20 -7
- package/dist/docs/scaffolding-agents.html +4 -4
- package/dist/docs/storage.html +4 -4
- package/dist/docs/troubleshooting.html +5 -5
- package/dist/evals.d.ts +39 -0
- package/dist/evals.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/internal/cli-mcp-oauth.d.ts +16 -0
- package/dist/internal/cli-mcp-oauth.d.ts.map +1 -0
- package/dist/internal/cli-mcp-oauth.js +194 -0
- package/dist/internal/cli-mcp.d.ts +4 -1
- package/dist/internal/cli-mcp.d.ts.map +1 -1
- package/dist/internal/cli-mcp.js +19 -1
- package/dist/internal/cursor/account-mcp.d.ts +17 -4
- package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
- package/dist/internal/cursor/account-mcp.js +105 -17
- package/dist/internal/cursor-event-relay.d.ts +2 -0
- package/dist/internal/cursor-event-relay.d.ts.map +1 -1
- package/dist/internal/cursor-event-relay.js +3 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +64 -10
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +1 -0
- package/dist/internal/json-dir-store.js +100 -0
- package/dist/internal/logs-client.js +1 -1
- package/dist/internal/mcp-host.d.ts.map +1 -1
- package/dist/internal/mcp-host.js +49 -26
- package/dist/internal/mcp-oauth.d.ts +88 -0
- package/dist/internal/mcp-oauth.d.ts.map +1 -0
- package/dist/internal/mcp-oauth.js +385 -0
- package/dist/internal/resolved-connections.d.ts.map +1 -1
- package/dist/internal/resolved-connections.js +4 -1
- package/dist/internal/sdk-runner.d.ts +9 -0
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +28 -2
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +13 -5
- package/dist/internal/session-engine.d.ts +7 -9
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +96 -64
- package/dist/internal/sessions-client.d.ts.map +1 -1
- package/dist/internal/sessions-client.js +2 -1
- package/dist/internal/stream-progress.d.ts.map +1 -1
- package/dist/internal/stream-progress.js +8 -4
- package/dist/internal/turn-governor.d.ts +54 -0
- package/dist/internal/turn-governor.d.ts.map +1 -0
- package/dist/internal/turn-governor.js +111 -0
- package/dist/playground/assets/index-1dzluIcW.js +86 -0
- package/dist/playground/assets/index-kZjeJs1v.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/prompt.d.ts +39 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +70 -0
- package/dist/types.d.ts +78 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/docs/README.md +4 -0
- package/docs/deployment.md +6 -1
- package/docs/guides/mcp-oauth.md +157 -0
- package/docs/reference/cli.md +28 -0
- package/docs/reference/connections.md +28 -0
- package/docs/reference/prompt.md +47 -0
- package/docs/reference/tools.md +21 -0
- package/docs/troubleshooting.md +11 -0
- package/package.json +9 -2
- package/skills/create-agent/SKILL.md +7 -3
- package/skills/framework-map/SKILL.md +1 -0
- package/skills/mcp-auth/SKILL.md +138 -0
- package/src/bin/agent-serve.ts +20 -3
- package/src/channels/github/github-channel.ts +2 -0
- package/src/channels/github/types.ts +4 -0
- package/src/channels/slack/cursor-account.ts +2 -0
- package/src/channels/slack/dispatch.ts +49 -0
- package/src/channels/slack/eval-directive.ts +348 -0
- package/src/channels/slack/index.ts +1 -0
- package/src/channels/slack/slack-channel.ts +3 -0
- package/src/channels/slack/types.ts +7 -0
- package/src/connections.ts +10 -5
- package/src/evals.ts +43 -0
- package/src/index.ts +3 -0
- package/src/internal/cli-mcp-oauth.ts +254 -0
- package/src/internal/cli-mcp.ts +25 -2
- package/src/internal/cursor/account-mcp.ts +153 -41
- package/src/internal/cursor-event-relay.ts +5 -0
- package/src/internal/discovery.ts +70 -8
- package/src/internal/distribution.ts +1 -0
- package/src/internal/logs-client.ts +1 -1
- package/src/internal/mcp-host.ts +33 -9
- package/src/internal/mcp-oauth.ts +496 -0
- package/src/internal/resolved-connections.ts +6 -1
- package/src/internal/sdk-runner.ts +37 -1
- package/src/internal/server.ts +13 -5
- package/src/internal/session-engine.ts +115 -74
- package/src/internal/sessions-client.ts +2 -1
- package/src/internal/stream-progress.ts +8 -0
- package/src/internal/turn-governor.ts +145 -0
- package/src/prompt.ts +83 -0
- package/src/types.ts +76 -6
- package/dist/channels/slack/post-update-delivery.d.ts +0 -85
- package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.OfDZ5CXg.js +0 -1
- package/dist/internal/persistence-coordinator.d.ts +0 -127
- package/dist/internal/persistence-coordinator.d.ts.map +0 -1
- package/dist/persistence.d.ts +0 -184
- package/dist/persistence.d.ts.map +0 -1
- package/dist/playground/assets/index-C0-QEG4O.js +0 -86
- package/dist/playground/assets/index-z3JY9O2x.css +0 -1
package/src/internal/server.ts
CHANGED
|
@@ -49,7 +49,6 @@ import {
|
|
|
49
49
|
} from "./cursor/account-mcp.js";
|
|
50
50
|
import { CursorBackendClient } from "./cursor/backend-client.js";
|
|
51
51
|
import {
|
|
52
|
-
cursorBackendUrl,
|
|
53
52
|
cursorExternalApiUrl,
|
|
54
53
|
type ResolvedApiKey,
|
|
55
54
|
resolveApiKey,
|
|
@@ -286,9 +285,10 @@ export async function startServer(
|
|
|
286
285
|
}
|
|
287
286
|
const runner =
|
|
288
287
|
options.runner ??
|
|
289
|
-
new CursorSdkRunner(
|
|
290
|
-
accountKey === undefined ? {} : { apiKey: accountKey.apiKey }
|
|
291
|
-
|
|
288
|
+
new CursorSdkRunner({
|
|
289
|
+
...(accountKey === undefined ? {} : { apiKey: accountKey.apiKey }),
|
|
290
|
+
logger,
|
|
291
|
+
});
|
|
292
292
|
|
|
293
293
|
const router = new Router();
|
|
294
294
|
const runtimes: MountedRuntime[] = [];
|
|
@@ -387,6 +387,12 @@ export async function startServer(
|
|
|
387
387
|
reminderRunner.cancelForContinuation(token, reason),
|
|
388
388
|
dispatch: (id) => reminderRunner.dispatch(id),
|
|
389
389
|
});
|
|
390
|
+
engine.setEvals({
|
|
391
|
+
list: () => evalRuns.list(),
|
|
392
|
+
get: (runId) => evalRuns.get(runId),
|
|
393
|
+
listRunsWithActive: () => evalRuns.listRunsWithActive(),
|
|
394
|
+
start: (input) => evalRuns.start(input),
|
|
395
|
+
});
|
|
390
396
|
const runtime: MountedRuntime = {
|
|
391
397
|
slug,
|
|
392
398
|
project,
|
|
@@ -1609,7 +1615,9 @@ async function startCursorSlackRelay(input: {
|
|
|
1609
1615
|
`This project has Slack channels using the Cursor account connection (slackChannel({ cursorAccount: true })) but the host is not signed in to Cursor. ${SIGN_IN_HINT}`
|
|
1610
1616
|
);
|
|
1611
1617
|
}
|
|
1612
|
-
|
|
1618
|
+
// Same host split as scm-events: `/v0/slack-events/*` is on api.cursor.com,
|
|
1619
|
+
// not api2 (accountKey.backendUrl 404s these routes).
|
|
1620
|
+
const baseUrl = cursorExternalApiUrl();
|
|
1613
1621
|
const baseStateRoot =
|
|
1614
1622
|
options.stateRoot ??
|
|
1615
1623
|
join(targets[0].runtime.project.rootDir, ".agent-serve");
|
|
@@ -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
|
-
|
|
783
|
-
|
|
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
|
-
|
|
1006
|
-
//
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
agent
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
-
|
|
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
|
+
);
|