@cursor/july 0.1.82 → 0.1.84
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/README.md +2 -2
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.BEHVaPKt.js → app.kqdC1N7p.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.BqhvUA76.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.D2-5PKih.js → VPLocalSearchBox.DXDE2WgG.js} +1 -1
- package/dist/docs/assets/chunks/{theme.uNqJ4MUO.js → theme.DFYqbyta.js} +2 -2
- package/dist/docs/assets/{guides_cloud-runtime.md.V5igN4Sq.js → guides_cloud-runtime.md.CbklWfzh.js} +1 -1
- package/dist/docs/assets/{guides_cloud-runtime.md.V5igN4Sq.lean.js → guides_cloud-runtime.md.CbklWfzh.lean.js} +1 -1
- package/dist/docs/assets/{reference_agent-config.md.BZnEKfEc.js → reference_agent-config.md.CTWp4DnU.js} +1 -1
- package/dist/docs/assets/{reference_skills.md.B_jHN7JL.js → reference_skills.md.BRF2nDv9.js} +2 -2
- package/dist/docs/assets/{reference_skills.md.B_jHN7JL.lean.js → reference_skills.md.BRF2nDv9.lean.js} +1 -1
- package/dist/docs/assets/troubleshooting.md.KgmmaCgw.js +1 -0
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +5 -5
- package/dist/docs/guides/convert-automation.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/opentelemetry.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +5 -5
- package/dist/docs/reference/artifacts.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +5 -5
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +3 -3
- package/dist/docs/templates/agentic-owners.html +3 -3
- package/dist/docs/templates/demo.html +3 -3
- package/dist/docs/templates/pr-autofixer.html +3 -3
- package/dist/docs/templates/security-reviewer.html +3 -3
- package/dist/docs/templates/triage.html +3 -3
- package/dist/docs/troubleshooting.html +5 -5
- package/dist/files-backends/cursor-hosted.d.ts +6 -0
- package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/files-backends/cursor-hosted.js +19 -0
- package/dist/files.d.ts +14 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +14 -1
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +17 -2
- package/dist/internal/event-mapper.d.ts +7 -1
- package/dist/internal/event-mapper.d.ts.map +1 -1
- package/dist/internal/event-mapper.js +8 -8
- package/dist/internal/http-channel.d.ts.map +1 -1
- package/dist/internal/http-channel.js +1 -1
- package/dist/internal/mcp-endpoint.js +9 -0
- package/dist/internal/prompt-context.d.ts +4 -1
- package/dist/internal/prompt-context.d.ts.map +1 -1
- package/dist/internal/prompt-context.js +20 -2
- package/dist/internal/review-comments.js +15 -7
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +6 -9
- package/dist/internal/session-engine.d.ts +36 -2
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +143 -52
- package/dist/internal/skills-sync.d.ts +7 -5
- package/dist/internal/skills-sync.d.ts.map +1 -1
- package/dist/internal/skills-sync.js +9 -7
- package/dist/internal/tool-policy.d.ts +50 -0
- package/dist/internal/tool-policy.d.ts.map +1 -0
- package/dist/internal/tool-policy.js +51 -0
- package/dist/playground/assets/{index-CkuIrhg4.js → index-Cspgk9jC.js} +45 -45
- package/dist/playground/index.html +1 -1
- package/dist/tools.d.ts +16 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/types.d.ts +72 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/guides/cloud-runtime.md +6 -1
- package/docs/reference/agent-config.md +6 -5
- package/docs/reference/skills.md +12 -3
- package/docs/troubleshooting.md +1 -1
- package/package.json +1 -1
- package/skills/framework-map/SKILL.md +2 -1
- package/src/files-backends/cursor-hosted.ts +21 -0
- package/src/files.ts +21 -2
- package/src/internal/discovery.ts +21 -1
- package/src/internal/event-mapper.ts +10 -1
- package/src/internal/http-channel.ts +1 -0
- package/src/internal/mcp-endpoint.ts +9 -0
- package/src/internal/prompt-context.ts +25 -2
- package/src/internal/review-comments.ts +17 -7
- package/src/internal/sdk-runner.ts +10 -11
- package/src/internal/session-engine.ts +174 -39
- package/src/internal/skills-sync.ts +10 -7
- package/src/internal/tool-policy.ts +92 -0
- package/src/tools.ts +19 -0
- package/src/types.ts +78 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.Vvio4stV.js +0 -1
- package/dist/docs/assets/troubleshooting.md.DQ-hYuZT.js +0 -1
- /package/dist/docs/assets/{reference_agent-config.md.BZnEKfEc.lean.js → reference_agent-config.md.CTWp4DnU.lean.js} +0 -0
- /package/dist/docs/assets/{troubleshooting.md.DQ-hYuZT.lean.js → troubleshooting.md.KgmmaCgw.lean.js} +0 -0
|
@@ -88,7 +88,15 @@ export class TurnEventMapper {
|
|
|
88
88
|
/** Per-parent-call accumulators for nested subagent streams. */
|
|
89
89
|
private readonly nested = new Map<string, NestedAccumulators>();
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
/**
|
|
92
|
+
* @param isStubbed Reports whether a finished tool call was answered by the
|
|
93
|
+
* host rather than executed. The harness reports an answered call exactly
|
|
94
|
+
* like a real one, so the engine is the only place that knows.
|
|
95
|
+
*/
|
|
96
|
+
constructor(
|
|
97
|
+
private readonly emit: (payload: SessionEventPayload) => void,
|
|
98
|
+
private readonly isStubbed: (callId: string) => boolean = () => false
|
|
99
|
+
) {}
|
|
92
100
|
|
|
93
101
|
/** Per-turn token usage summed across model steps. */
|
|
94
102
|
get usage(): TurnUsage | undefined {
|
|
@@ -303,6 +311,7 @@ export class TurnEventMapper {
|
|
|
303
311
|
toolName: description.toolName,
|
|
304
312
|
output: result.output,
|
|
305
313
|
isError: result.isError,
|
|
314
|
+
...(this.isStubbed(callId) ? { stubbed: true as const } : {}),
|
|
306
315
|
parentCallId,
|
|
307
316
|
},
|
|
308
317
|
});
|
|
@@ -109,6 +109,7 @@ export function buildHttpChannelRoutes(args: {
|
|
|
109
109
|
...(body.state === undefined ? {} : { state: body.state as JsonValue }),
|
|
110
110
|
...(typeof body.title === "string" ? { title: body.title } : {}),
|
|
111
111
|
...(body.purpose === "eval" ? { purpose: "eval" as const } : {}),
|
|
112
|
+
...(body.dryRun === true ? { dryRun: true } : {}),
|
|
112
113
|
...(workspaceFiles === undefined ? {} : { workspaceFiles }),
|
|
113
114
|
...(cloud === undefined ? {} : { cloud }),
|
|
114
115
|
});
|
|
@@ -312,6 +312,15 @@ function buildConnectionBridgeMcpServer(
|
|
|
312
312
|
if (reason !== undefined) {
|
|
313
313
|
return errorResult(reason);
|
|
314
314
|
}
|
|
315
|
+
// The one tool path that never reaches `executeServerTool`, so it asks
|
|
316
|
+
// the policy itself.
|
|
317
|
+
const answer = engine.bridgedCallAnswer({
|
|
318
|
+
sessionId: sessionId ?? "",
|
|
319
|
+
toolName: request.params.name,
|
|
320
|
+
});
|
|
321
|
+
if (answer.answered) {
|
|
322
|
+
return sdkResultToMcpResult(answer.result);
|
|
323
|
+
}
|
|
315
324
|
try {
|
|
316
325
|
const result = await engine.host.mcp.callTool(
|
|
317
326
|
connectionName,
|
|
@@ -39,17 +39,40 @@ export interface SessionContextInput {
|
|
|
39
39
|
*/
|
|
40
40
|
channelKind?: string;
|
|
41
41
|
environment: PromptEnvironment;
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Injectable clock for tests; defaults to the mocked prompt time when
|
|
44
|
+
* `CURSOR_SDK_MOCK_PROMPT_TIME` is set, then to now.
|
|
45
|
+
*/
|
|
43
46
|
now?: Date;
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Development-only override for the wall clock rendered into prompt content.
|
|
51
|
+
* Same variable the Cursor SDK harness reads for its prompt clock (the
|
|
52
|
+
* user-message <timestamp> prefix and user_info date), so one knob keeps the
|
|
53
|
+
* session context block and the harness-rendered clock consistent. Read on
|
|
54
|
+
* every render so a simulation harness can advance the instant between
|
|
55
|
+
* sessions. Never affects runtime clocks (schedules, reminders) or tools.
|
|
56
|
+
*/
|
|
57
|
+
const MOCK_PROMPT_TIME_ENV = "CURSOR_SDK_MOCK_PROMPT_TIME";
|
|
58
|
+
|
|
59
|
+
function readMockPromptTimeFromEnv(): Date | undefined {
|
|
60
|
+
const raw = process.env[MOCK_PROMPT_TIME_ENV];
|
|
61
|
+
if (raw === undefined || raw === "") {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const parsed = new Date(raw);
|
|
65
|
+
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
export function buildSessionContextBlock(input: SessionContextInput): string {
|
|
69
|
+
const now = input.now ?? readMockPromptTimeFromEnv() ?? new Date();
|
|
47
70
|
const lines = [
|
|
48
71
|
"<agentkit_context>",
|
|
49
72
|
`session: ${input.sessionId}`,
|
|
50
73
|
`channel: ${input.channelId}`,
|
|
51
74
|
`environment: ${input.environment}`,
|
|
52
|
-
`time: ${
|
|
75
|
+
`time: ${now.toISOString()}`,
|
|
53
76
|
"</agentkit_context>",
|
|
54
77
|
];
|
|
55
78
|
const presence = channelPresenceLine(input);
|
|
@@ -690,10 +690,13 @@ async function postReviewViaHostGithub(
|
|
|
690
690
|
/**
|
|
691
691
|
* Ids of the inline comments a just-created review owns.
|
|
692
692
|
*
|
|
693
|
-
* `pulls.createReview` returns the review, not its comments
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
693
|
+
* `pulls.createReview` returns the review, not its comments.
|
|
694
|
+
* `listCommentsForReview` either omits those rows or returns them with
|
|
695
|
+
* `line` / `original_line` unset, so the line filter drops every id, the
|
|
696
|
+
* store records no findings, and the next run cannot judge or resolve
|
|
697
|
+
* those threads. Read the PR-wide list instead and keep only this
|
|
698
|
+
* review's rows. Newest-first so a busy PR's older comments cannot push
|
|
699
|
+
* ours past the page cap.
|
|
697
700
|
* Best-effort: any failure yields an empty list, which costs the next run
|
|
698
701
|
* its prior findings rather than risking a wrong id. Never throws — the
|
|
699
702
|
* review is already posted by this point.
|
|
@@ -714,22 +717,29 @@ async function readPostedComments(args: {
|
|
|
714
717
|
const posted: PostedReviewComment[] = [];
|
|
715
718
|
for (let page = 1; page <= REVIEW_COMMENTS_MAX_PAGES; page++) {
|
|
716
719
|
args.signal?.throwIfAborted();
|
|
717
|
-
const response = await args.octokit.rest.pulls.
|
|
720
|
+
const response = await args.octokit.rest.pulls.listReviewComments({
|
|
718
721
|
owner: args.owner,
|
|
719
722
|
repo: args.repo,
|
|
720
723
|
pull_number: args.prNumber,
|
|
721
|
-
|
|
724
|
+
sort: "created",
|
|
725
|
+
direction: "desc",
|
|
722
726
|
per_page: REVIEW_COMMENTS_PAGE_SIZE,
|
|
723
727
|
page,
|
|
724
728
|
});
|
|
725
729
|
for (const comment of response.data) {
|
|
730
|
+
if (comment.pull_request_review_id !== args.reviewId) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
726
733
|
const line = comment.original_line ?? comment.line;
|
|
727
734
|
if (line === null || line === undefined) {
|
|
728
735
|
continue;
|
|
729
736
|
}
|
|
730
737
|
posted.push({ id: comment.id, file: comment.path, line });
|
|
731
738
|
}
|
|
732
|
-
if (
|
|
739
|
+
if (
|
|
740
|
+
posted.length >= args.expected ||
|
|
741
|
+
response.data.length < REVIEW_COMMENTS_PAGE_SIZE
|
|
742
|
+
) {
|
|
733
743
|
break;
|
|
734
744
|
}
|
|
735
745
|
}
|
|
@@ -514,7 +514,7 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
514
514
|
request: RunnerTurnRequest,
|
|
515
515
|
shared: Pick<AgentOptions, "model" | "apiKey" | "mcpServers" | "agents">
|
|
516
516
|
): Promise<SDKAgent> {
|
|
517
|
-
const cloud = toCloudAgentOptions(request.cloud);
|
|
517
|
+
const cloud = toCloudAgentOptions(request.cloud, request.agentServeAgent);
|
|
518
518
|
const hasEnvVars =
|
|
519
519
|
cloud.envVars !== undefined && Object.keys(cloud.envVars).length > 0;
|
|
520
520
|
const options: AgentOptions = {
|
|
@@ -603,24 +603,23 @@ export function toModelSelection(model: ModelSetting): ModelSelection {
|
|
|
603
603
|
}
|
|
604
604
|
|
|
605
605
|
function toCloudAgentOptions(
|
|
606
|
-
cloud: RunnerTurnRequest["cloud"]
|
|
606
|
+
cloud: RunnerTurnRequest["cloud"],
|
|
607
|
+
agentServeAgent?: string
|
|
607
608
|
): CloudAgentOptions {
|
|
608
|
-
if (cloud === undefined) {
|
|
609
|
-
return {};
|
|
610
|
-
}
|
|
611
609
|
return {
|
|
612
|
-
...(cloud
|
|
613
|
-
...(cloud
|
|
614
|
-
...(cloud
|
|
610
|
+
...(cloud?.env === undefined ? {} : { env: cloud.env }),
|
|
611
|
+
...(cloud?.repos === undefined ? {} : { repos: cloud.repos }),
|
|
612
|
+
...(cloud?.workOnCurrentBranch === undefined
|
|
615
613
|
? {}
|
|
616
614
|
: { workOnCurrentBranch: cloud.workOnCurrentBranch }),
|
|
617
|
-
...(cloud
|
|
615
|
+
...(cloud?.autoCreatePR === undefined
|
|
618
616
|
? {}
|
|
619
617
|
: { autoCreatePR: cloud.autoCreatePR }),
|
|
620
|
-
...(cloud
|
|
618
|
+
...(cloud?.skipReviewerRequest === undefined
|
|
621
619
|
? {}
|
|
622
620
|
: { skipReviewerRequest: cloud.skipReviewerRequest }),
|
|
623
|
-
...(cloud
|
|
621
|
+
...(cloud?.envVars === undefined ? {} : { envVars: cloud.envVars }),
|
|
622
|
+
...(agentServeAgent === undefined ? {} : { agentServeAgent }),
|
|
624
623
|
};
|
|
625
624
|
}
|
|
626
625
|
|
|
@@ -24,9 +24,14 @@ import {
|
|
|
24
24
|
SlackNudgeStore,
|
|
25
25
|
} from "../channels/slack/nudge-store.js";
|
|
26
26
|
import { resolveDedicatedSlackHostBinding } from "../channels/slack/slack-channel.js";
|
|
27
|
+
import {
|
|
28
|
+
AGENT_SERVE_USER_STORE_FILES_SOURCE_ID,
|
|
29
|
+
agentStoreKeys,
|
|
30
|
+
} from "../files.js";
|
|
27
31
|
import {
|
|
28
32
|
cursorHostedFiles,
|
|
29
33
|
isCursorHostedFilesAvailable,
|
|
34
|
+
isCursorUserStoreFilesAvailable,
|
|
30
35
|
} from "../files-backends/cursor-hosted.js";
|
|
31
36
|
import {
|
|
32
37
|
type AgentProject,
|
|
@@ -129,6 +134,7 @@ import { SessionStore } from "./session-store.js";
|
|
|
129
134
|
import { syncSkillsToStore } from "./skills-sync.js";
|
|
130
135
|
import { StorageCoordinator } from "./storage-coordinator.js";
|
|
131
136
|
import type { FrameworkSessionStore } from "./storage-roles.js";
|
|
137
|
+
import { decideToolCall } from "./tool-policy.js";
|
|
132
138
|
import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
|
|
133
139
|
import {
|
|
134
140
|
type EstimateTurnCostFn,
|
|
@@ -316,7 +322,7 @@ export class SessionEngine {
|
|
|
316
322
|
>();
|
|
317
323
|
/** Once-per-process cache for the A/B snapshot backfill (see abSnapshot). */
|
|
318
324
|
private restoredAbSnapshot: Promise<ABSnapshot | undefined> | undefined;
|
|
319
|
-
/** In-flight skills →
|
|
325
|
+
/** In-flight skills → Agent Store sync (see ensureCloudSkillsSynced). */
|
|
320
326
|
private cloudSkillsSync: Promise<boolean> | undefined;
|
|
321
327
|
|
|
322
328
|
private readonly runner: AgentRunner;
|
|
@@ -328,6 +334,15 @@ export class SessionEngine {
|
|
|
328
334
|
private readonly activeTurns = new Map<string, Promise<TurnOutcome>>();
|
|
329
335
|
/** Context used by the cloud turn's HTTP MCP callbacks. */
|
|
330
336
|
private readonly activeToolContexts = new Map<string, ActiveToolContext>();
|
|
337
|
+
/**
|
|
338
|
+
* Tool call ids the host answered instead of running, per session.
|
|
339
|
+
*
|
|
340
|
+
* The mapper emits the result event from the harness's view of a finished
|
|
341
|
+
* call, which cannot tell an answered call from a real one — so the decision
|
|
342
|
+
* point records the id here and the mapper stamps the event from it.
|
|
343
|
+
* Registered only while a turn can invoke a tool.
|
|
344
|
+
*/
|
|
345
|
+
private readonly stubbedCallIds = new Map<string, Set<string>>();
|
|
331
346
|
/**
|
|
332
347
|
* Delivered-nudge count per session at the last tool boundary. The carrier
|
|
333
348
|
* drains out-of-process, so a growing count is how the engine learns a
|
|
@@ -877,6 +892,7 @@ export class SessionEngine {
|
|
|
877
892
|
channelId,
|
|
878
893
|
mode: sendOptions.mode ?? "chat",
|
|
879
894
|
purpose,
|
|
895
|
+
...(sendOptions.dryRun === true ? { dryRun: true } : {}),
|
|
880
896
|
continuationKey: sendOptions.continuationToken ?? newContinuationKey(),
|
|
881
897
|
auth,
|
|
882
898
|
title: sendOptions.title,
|
|
@@ -1591,7 +1607,10 @@ export class SessionEngine {
|
|
|
1591
1607
|
);
|
|
1592
1608
|
}
|
|
1593
1609
|
};
|
|
1594
|
-
|
|
1610
|
+
// Read by closure, so the mapper still sees it while draining after the
|
|
1611
|
+
// registry entry below is gone.
|
|
1612
|
+
const stubbed = new Set<string>();
|
|
1613
|
+
const mapper = new TurnEventMapper(emit, (callId) => stubbed.has(callId));
|
|
1595
1614
|
|
|
1596
1615
|
// Take a turn slot before any work happens. When every slot is busy the
|
|
1597
1616
|
// turn waits FIFO — durable `turn.queued` marks the wait — and a
|
|
@@ -1701,10 +1720,12 @@ export class SessionEngine {
|
|
|
1701
1720
|
pendingImages
|
|
1702
1721
|
);
|
|
1703
1722
|
this.activeToolContexts.set(sessionId, { record, turnId, emit });
|
|
1723
|
+
this.stubbedCallIds.set(sessionId, stubbed);
|
|
1704
1724
|
try {
|
|
1705
1725
|
outcome = await this.runner.runTurn(request);
|
|
1706
1726
|
} finally {
|
|
1707
1727
|
this.activeToolContexts.delete(sessionId);
|
|
1728
|
+
this.stubbedCallIds.delete(sessionId);
|
|
1708
1729
|
}
|
|
1709
1730
|
} catch (error) {
|
|
1710
1731
|
this.approvals.cancelSession(sessionId, "deny");
|
|
@@ -1796,7 +1817,8 @@ export class SessionEngine {
|
|
|
1796
1817
|
}
|
|
1797
1818
|
|
|
1798
1819
|
/**
|
|
1799
|
-
* Sync authored skills onto the
|
|
1820
|
+
* Sync authored skills onto the Agent Store at `skills/<name>/…` (hosted)
|
|
1821
|
+
* or `agent-serve/<agent>/skills/<name>/…` (personal-key local+cloud)
|
|
1800
1822
|
* before the first cloud prompt, so `--agent-store-skills-dir` and the
|
|
1801
1823
|
* first-turn S3 scan can discover them. False when the sync fails: the
|
|
1802
1824
|
* turn then runs without store skills rather than pointing discovery at
|
|
@@ -1817,16 +1839,22 @@ export class SessionEngine {
|
|
|
1817
1839
|
if (this.cloudSkillsSync !== undefined) {
|
|
1818
1840
|
return this.cloudSkillsSync;
|
|
1819
1841
|
}
|
|
1842
|
+
const target = this.cloudSkillsStore();
|
|
1843
|
+
if (target === undefined) {
|
|
1844
|
+
this.cloudSkillsSync = Promise.resolve(false);
|
|
1845
|
+
return this.cloudSkillsSync;
|
|
1846
|
+
}
|
|
1820
1847
|
const skills = this.project.agent.skills;
|
|
1821
1848
|
let sync = syncSkillsToStore({
|
|
1822
|
-
sink:
|
|
1849
|
+
sink: target.sink,
|
|
1823
1850
|
agentName: this.project.name,
|
|
1824
1851
|
skills,
|
|
1852
|
+
skillsPrefix: target.skillsPrefix,
|
|
1825
1853
|
}).then(
|
|
1826
1854
|
() => true,
|
|
1827
1855
|
(error) => {
|
|
1828
1856
|
this.logger(
|
|
1829
|
-
`[skills]
|
|
1857
|
+
`[skills] ${target.label} sync failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1830
1858
|
);
|
|
1831
1859
|
return false;
|
|
1832
1860
|
}
|
|
@@ -1840,6 +1868,36 @@ export class SessionEngine {
|
|
|
1840
1868
|
return sync;
|
|
1841
1869
|
}
|
|
1842
1870
|
|
|
1871
|
+
/**
|
|
1872
|
+
* Hosted deployments own the deployment store. Local `serve`/`run` with
|
|
1873
|
+
* a personal API key writes into the USER store under `agent-serve/`.
|
|
1874
|
+
*/
|
|
1875
|
+
private cloudSkillsStore():
|
|
1876
|
+
| {
|
|
1877
|
+
sink: ReturnType<typeof cursorHostedFiles>;
|
|
1878
|
+
skillsPrefix: string;
|
|
1879
|
+
label: string;
|
|
1880
|
+
}
|
|
1881
|
+
| undefined {
|
|
1882
|
+
if (isCursorHostedFilesAvailable()) {
|
|
1883
|
+
return {
|
|
1884
|
+
sink: cursorHostedFiles(),
|
|
1885
|
+
skillsPrefix: agentStoreKeys.skillsPrefix(),
|
|
1886
|
+
label: "deployment store",
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
if (isCursorUserStoreFilesAvailable()) {
|
|
1890
|
+
return {
|
|
1891
|
+
sink: cursorHostedFiles({
|
|
1892
|
+
storeSourceId: AGENT_SERVE_USER_STORE_FILES_SOURCE_ID,
|
|
1893
|
+
}),
|
|
1894
|
+
skillsPrefix: agentStoreKeys.userSkillsPrefix(this.project.name),
|
|
1895
|
+
label: "user store",
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
return undefined;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1843
1901
|
private async buildTurnRequest(
|
|
1844
1902
|
record: SessionRecord,
|
|
1845
1903
|
agent: ResolvedAgent,
|
|
@@ -1917,10 +1975,11 @@ export class SessionEngine {
|
|
|
1917
1975
|
: SERVER_TOOLS_MCP_SERVER_NAME,
|
|
1918
1976
|
});
|
|
1919
1977
|
const hosted = isCursorHostedFilesAvailable();
|
|
1920
|
-
// Synced whenever
|
|
1921
|
-
// last skill has to delete its store copies too.
|
|
1922
|
-
// (`--agent-store-skills-dir`); this only has
|
|
1923
|
-
|
|
1978
|
+
// Synced whenever a store is reachable, not just when skills exist:
|
|
1979
|
+
// removing the last skill has to delete its store copies too.
|
|
1980
|
+
// Discovery is native (`--agent-store-skills-dir`); this only has
|
|
1981
|
+
// to land the bytes.
|
|
1982
|
+
if (hosted || isCursorUserStoreFilesAvailable()) {
|
|
1924
1983
|
await this.ensureCloudSkillsSynced();
|
|
1925
1984
|
}
|
|
1926
1985
|
const memorySection = hosted
|
|
@@ -1954,6 +2013,9 @@ export class SessionEngine {
|
|
|
1954
2013
|
...(agent.model === undefined ? {} : { model: agent.model }),
|
|
1955
2014
|
runtime,
|
|
1956
2015
|
...(mergedCloud === undefined ? {} : { cloud: mergedCloud }),
|
|
2016
|
+
...(runtime === "cloud" && !isCursorHostedFilesAvailable()
|
|
2017
|
+
? { agentServeAgent: this.project.name }
|
|
2018
|
+
: {}),
|
|
1957
2019
|
customTools:
|
|
1958
2020
|
runtime === "local"
|
|
1959
2021
|
? await this.buildCustomTools(record, agent, turnId, emit)
|
|
@@ -2388,6 +2450,21 @@ export class SessionEngine {
|
|
|
2388
2450
|
emit: args.emit,
|
|
2389
2451
|
};
|
|
2390
2452
|
try {
|
|
2453
|
+
// Before the approval gate: a dry run is unattended, so parking a write
|
|
2454
|
+
// for a human would hang the turn rather than answer it.
|
|
2455
|
+
const decision = decideToolCall({
|
|
2456
|
+
dryRun: args.record.dryRun === true,
|
|
2457
|
+
effect: args.tool.definition.effect,
|
|
2458
|
+
dryRunResult: args.tool.definition.dryRunResult,
|
|
2459
|
+
input: validation.value,
|
|
2460
|
+
});
|
|
2461
|
+
if (decision.kind === "answer") {
|
|
2462
|
+
this.stubbedCallIds.get(args.record.sessionId)?.add(toolCallId);
|
|
2463
|
+
this.logger(
|
|
2464
|
+
`[session] tool ${args.tool.name} answered by host (${toolCallId})`
|
|
2465
|
+
);
|
|
2466
|
+
return normalizeToolResult(decision.result);
|
|
2467
|
+
}
|
|
2391
2468
|
const needsGate = await evaluateNeedsApproval(
|
|
2392
2469
|
args.tool.definition.needsApproval,
|
|
2393
2470
|
validation.value
|
|
@@ -2670,10 +2747,21 @@ export class SessionEngine {
|
|
|
2670
2747
|
});
|
|
2671
2748
|
}
|
|
2672
2749
|
|
|
2750
|
+
// A separate body from the model path, so it asks the same policy: a
|
|
2751
|
+
// session-bound `POST /v1/tools/:name`, `call --session`, or
|
|
2752
|
+
// `handle.callTool` performs a write just as readily.
|
|
2753
|
+
const decision = decideToolCall({
|
|
2754
|
+
dryRun: record?.dryRun === true,
|
|
2755
|
+
effect: tool.definition.effect,
|
|
2756
|
+
dryRunResult: tool.definition.dryRunResult,
|
|
2757
|
+
input: validation.value,
|
|
2758
|
+
});
|
|
2759
|
+
|
|
2673
2760
|
const started = Date.now();
|
|
2674
2761
|
this.logger(`[session] tool ${toolName} start (direct ${callId})`);
|
|
2675
2762
|
let result: unknown;
|
|
2676
2763
|
let isError: boolean;
|
|
2764
|
+
const stubbed = decision.kind === "answer";
|
|
2677
2765
|
const live = record;
|
|
2678
2766
|
const emit =
|
|
2679
2767
|
live === undefined
|
|
@@ -2685,35 +2773,43 @@ export class SessionEngine {
|
|
|
2685
2773
|
}).catch(() => {});
|
|
2686
2774
|
};
|
|
2687
2775
|
try {
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
})
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2776
|
+
if (decision.kind === "answer") {
|
|
2777
|
+
this.logger(
|
|
2778
|
+
`[session] tool ${toolName} answered by host (direct ${callId})`
|
|
2779
|
+
);
|
|
2780
|
+
result = normalizeToolResult(decision.result);
|
|
2781
|
+
isError = false;
|
|
2782
|
+
} else {
|
|
2783
|
+
result = normalizeToolResult(
|
|
2784
|
+
await execute(validation.value as never, {
|
|
2785
|
+
toolCallId: callId,
|
|
2786
|
+
session,
|
|
2787
|
+
workspaceDir,
|
|
2788
|
+
stateRoot: this.stateRoot,
|
|
2789
|
+
host: this.hostForSession(record?.sessionId),
|
|
2790
|
+
// Scratch calls have no real session; their facade stays unbound.
|
|
2791
|
+
artifacts:
|
|
2792
|
+
record === undefined
|
|
2793
|
+
? this.artifactsApi()
|
|
2794
|
+
: this.artifactsApi({
|
|
2795
|
+
sessionId: record.sessionId,
|
|
2796
|
+
turnId: callId,
|
|
2797
|
+
}),
|
|
2798
|
+
send: (channelId, message, sendOptions = {}) =>
|
|
2799
|
+
this.send(channelId, message, {
|
|
2800
|
+
...sendOptions,
|
|
2801
|
+
auth:
|
|
2802
|
+
sendOptions.auth !== undefined
|
|
2803
|
+
? sendOptions.auth
|
|
2804
|
+
: session.auth,
|
|
2805
|
+
}),
|
|
2806
|
+
getSession: (channelId, sessionId) =>
|
|
2807
|
+
this.getChannelSession(channelId, sessionId),
|
|
2808
|
+
emit,
|
|
2809
|
+
})
|
|
2810
|
+
);
|
|
2811
|
+
isError = isErrorEnvelope(result);
|
|
2812
|
+
}
|
|
2717
2813
|
} catch (error) {
|
|
2718
2814
|
this.logger(
|
|
2719
2815
|
`[session] tool ${toolName} failed (direct ${callId}): ${describeError(error)}`
|
|
@@ -2743,7 +2839,13 @@ export class SessionEngine {
|
|
|
2743
2839
|
await this.appendEvent(record.sessionId, {
|
|
2744
2840
|
type: "action.result",
|
|
2745
2841
|
turnId: callId,
|
|
2746
|
-
data: {
|
|
2842
|
+
data: {
|
|
2843
|
+
callId,
|
|
2844
|
+
toolName,
|
|
2845
|
+
output: result,
|
|
2846
|
+
isError,
|
|
2847
|
+
...(stubbed ? { stubbed: true as const } : {}),
|
|
2848
|
+
},
|
|
2747
2849
|
});
|
|
2748
2850
|
}
|
|
2749
2851
|
|
|
@@ -2759,6 +2861,38 @@ export class SessionEngine {
|
|
|
2759
2861
|
};
|
|
2760
2862
|
}
|
|
2761
2863
|
|
|
2864
|
+
/**
|
|
2865
|
+
* Whether a dry run answers a bridged MCP call, for the cloud host bridge —
|
|
2866
|
+
* the one tool path that does not reach {@link executeServerTool}, because
|
|
2867
|
+
* it forwards to `host.mcp` instead of invoking a tool body.
|
|
2868
|
+
*
|
|
2869
|
+
* A bridged tool carries no `defineTool`, so it declares no effect, and the
|
|
2870
|
+
* policy reads an undeclared tool as a write. In a dry run that answers
|
|
2871
|
+
* bridged reads too; resolving effects from a server's own listing is the
|
|
2872
|
+
* next unit's work.
|
|
2873
|
+
*/
|
|
2874
|
+
bridgedCallAnswer(args: {
|
|
2875
|
+
sessionId: string;
|
|
2876
|
+
toolName: string;
|
|
2877
|
+
}): { answered: true; result: SDKCustomToolResult } | { answered: false } {
|
|
2878
|
+
// Read off the in-flight turn's context rather than the session store:
|
|
2879
|
+
// the bridge already refuses a call with no active turn, so the record is
|
|
2880
|
+
// in memory. That keeps this free for the sessions that are not dry runs,
|
|
2881
|
+
// which is every session today.
|
|
2882
|
+
const record = this.activeToolContexts.get(args.sessionId)?.record;
|
|
2883
|
+
const decision = decideToolCall({
|
|
2884
|
+
dryRun: record?.dryRun === true,
|
|
2885
|
+
effect: undefined,
|
|
2886
|
+
});
|
|
2887
|
+
if (decision.kind === "run") {
|
|
2888
|
+
return { answered: false };
|
|
2889
|
+
}
|
|
2890
|
+
this.logger(
|
|
2891
|
+
`[session] bridged tool ${args.toolName} answered by host (${args.sessionId})`
|
|
2892
|
+
);
|
|
2893
|
+
return { answered: true, result: normalizeToolResult(decision.result) };
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2762
2896
|
// ==========================================================================
|
|
2763
2897
|
// Events: append, dispatch to channel handlers + hooks
|
|
2764
2898
|
// ==========================================================================
|
|
@@ -3399,6 +3533,7 @@ export class SessionEngine {
|
|
|
3399
3533
|
channelId: record.channelId,
|
|
3400
3534
|
mode: record.mode,
|
|
3401
3535
|
purpose: record.purpose === "eval" ? "eval" : "live",
|
|
3536
|
+
...(record.dryRun === true ? { dryRun: true } : {}),
|
|
3402
3537
|
auth: record.auth,
|
|
3403
3538
|
continuationKey: record.continuationKey,
|
|
3404
3539
|
...(record.abs === undefined ? {} : { abs: record.abs }),
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sync authored skills
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Sync authored skills onto an Agent Store so cloud-runtime turns discover
|
|
3
|
+
* them natively. Hosted deployments write store-root `skills/<name>/…` (the
|
|
4
|
+
* VM's `--agent-store-skills-dir /cursor/stores/deployment/skills`). Local
|
|
5
|
+
* `serve`/`run` with `runtime: "cloud"` writes
|
|
6
|
+
* `agent-serve/<agent>/skills/<name>/…` on the USER store.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { readFile } from "node:fs/promises";
|
|
@@ -32,11 +32,14 @@ export async function syncSkillsToStore(args: {
|
|
|
32
32
|
/** Sweeps leftover keys from the pre-native `agentkit/v1/{agent}/skills/` prefix. */
|
|
33
33
|
agentName: string;
|
|
34
34
|
skills: readonly DiscoveredSkill[];
|
|
35
|
+
/** Store-root directory that holds SKILL.md files. Defaults to `skills/`. */
|
|
36
|
+
skillsPrefix?: string;
|
|
35
37
|
}): Promise<void> {
|
|
38
|
+
const skillsPrefix = args.skillsPrefix ?? agentStoreKeys.skillsPrefix();
|
|
36
39
|
const desired = new Map<string, Uint8Array>();
|
|
37
40
|
for (const skill of args.skills) {
|
|
38
41
|
const key = (relPath: string): string =>
|
|
39
|
-
agentStoreKeys.skillFile(skill.name, relPath);
|
|
42
|
+
agentStoreKeys.skillFile(skill.name, relPath, skillsPrefix);
|
|
40
43
|
desired.set(key("SKILL.md"), Buffer.from(buildSkillMd(skill), "utf8"));
|
|
41
44
|
for (const [relPath, source] of Object.entries(skill.files)) {
|
|
42
45
|
desired.set(key(storeRelPath(relPath)), await sourceBytes(source));
|
|
@@ -46,7 +49,7 @@ export async function syncSkillsToStore(args: {
|
|
|
46
49
|
[...desired].map(([key, body]) => putLastWriterWins(args.sink, key, body))
|
|
47
50
|
);
|
|
48
51
|
const [existing, leftover] = await Promise.all([
|
|
49
|
-
args.sink.list(
|
|
52
|
+
args.sink.list(skillsPrefix),
|
|
50
53
|
args.sink.list(legacySkillsPrefix(args.agentName)),
|
|
51
54
|
]);
|
|
52
55
|
await Promise.all(
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one decision every tool call passes through.
|
|
3
|
+
*
|
|
4
|
+
* A tool body can be reached by more than one path — the model's own calls on
|
|
5
|
+
* a local turn, a cloud turn's calls over the MCP endpoint, a direct
|
|
6
|
+
* `callTool`, the host connection bridge. Each of those is a separate code
|
|
7
|
+
* path, and a guarantee that holds on some of them is not a guarantee. So the
|
|
8
|
+
* decision lives here, in one pure function, and every path asks it rather
|
|
9
|
+
* than re-implementing it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
ToolEffect,
|
|
14
|
+
ToolEffectDeclaration,
|
|
15
|
+
ToolExecuteResult,
|
|
16
|
+
} from "../types.js";
|
|
17
|
+
|
|
18
|
+
/** What the host does with one tool call. */
|
|
19
|
+
export type ToolDecision =
|
|
20
|
+
/** Run the tool body. */
|
|
21
|
+
| { kind: "run" }
|
|
22
|
+
/** Do not run it; return this to the model instead. */
|
|
23
|
+
| { kind: "answer"; result: ToolExecuteResult };
|
|
24
|
+
|
|
25
|
+
/** What a policy decision needs to know about the call. */
|
|
26
|
+
export interface ToolCallFacts {
|
|
27
|
+
/** The session answers writes instead of running them. */
|
|
28
|
+
dryRun: boolean;
|
|
29
|
+
/** The tool's declaration, if it made one. */
|
|
30
|
+
effect: ToolEffectDeclaration<never> | undefined;
|
|
31
|
+
/** What a stubbed write should answer with, if the tool supplied one. */
|
|
32
|
+
dryRunResult?: ToolExecuteResult | ((input: never) => ToolExecuteResult);
|
|
33
|
+
/**
|
|
34
|
+
* Validated input, as the tool body would receive it. Only read by a
|
|
35
|
+
* per-call `effect` predicate and by a `dryRunResult` function, so a caller
|
|
36
|
+
* with neither — a bridged MCP tool, which carries no declaration at all —
|
|
37
|
+
* can omit it.
|
|
38
|
+
*/
|
|
39
|
+
input?: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What a stubbed write answers. Deliberately not self-describing: a model told
|
|
44
|
+
* mid-turn that its writes are not real starts hedging its conclusions, and
|
|
45
|
+
* the conclusion is the thing a dry run exists to read. Tell the agent once,
|
|
46
|
+
* in its opening prompt, not in a tool result.
|
|
47
|
+
*/
|
|
48
|
+
const ACKNOWLEDGED = "Operation acknowledged.";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The effect of one specific call.
|
|
52
|
+
*
|
|
53
|
+
* Undeclared resolves to `"write"`. A dry run promises that nothing outside
|
|
54
|
+
* the session changed, and it cannot promise that about a tool it knows
|
|
55
|
+
* nothing about — so the unknown case is the one that costs a stubbed read
|
|
56
|
+
* rather than the one that leaks a real write.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveEffect(
|
|
59
|
+
effect: ToolEffectDeclaration<never> | undefined,
|
|
60
|
+
input: unknown
|
|
61
|
+
): ToolEffect {
|
|
62
|
+
const declared =
|
|
63
|
+
typeof effect === "function" ? effect(input as never) : effect;
|
|
64
|
+
return declared === "read" ? "read" : "write";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Decide what to do with one tool call. Pure; no I/O, no session state. */
|
|
68
|
+
export function decideToolCall(facts: ToolCallFacts): ToolDecision {
|
|
69
|
+
if (!facts.dryRun) {
|
|
70
|
+
return { kind: "run" };
|
|
71
|
+
}
|
|
72
|
+
if (resolveEffect(facts.effect, facts.input) === "read") {
|
|
73
|
+
return { kind: "run" };
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
kind: "answer",
|
|
77
|
+
result: dryRunAnswer(facts.dryRunResult, facts.input),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function dryRunAnswer(
|
|
82
|
+
dryRunResult: ToolCallFacts["dryRunResult"],
|
|
83
|
+
input: unknown
|
|
84
|
+
): ToolExecuteResult {
|
|
85
|
+
if (dryRunResult === undefined) {
|
|
86
|
+
return ACKNOWLEDGED;
|
|
87
|
+
}
|
|
88
|
+
if (typeof dryRunResult === "function") {
|
|
89
|
+
return dryRunResult(input as never);
|
|
90
|
+
}
|
|
91
|
+
return dryRunResult;
|
|
92
|
+
}
|