cursor-opencode-provider 0.6.8 → 0.7.1
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 +51 -181
- package/dist/compaction-marker.d.ts +3 -2
- package/dist/compaction-marker.js +3 -2
- package/dist/host-agent-mode.d.ts +32 -0
- package/dist/host-agent-mode.js +92 -0
- package/dist/language-model.d.ts +15 -1
- package/dist/language-model.js +190 -36
- package/dist/models.d.ts +12 -1
- package/dist/models.js +16 -6
- package/dist/opencode2/catalog.d.ts +46 -11
- package/dist/opencode2/catalog.js +60 -45
- package/dist/opencode2/todo-tools.d.ts +64 -0
- package/dist/opencode2/todo-tools.js +165 -0
- package/dist/opencode2/types.d.ts +138 -83
- package/dist/opencode2/types.js +8 -10
- package/dist/plan-execution-kickoff.d.ts +2 -0
- package/dist/plan-execution-kickoff.js +25 -1
- package/dist/plugin-opencode2.d.ts +4 -1
- package/dist/plugin-opencode2.js +335 -154
- package/dist/plugin.js +5 -1
- package/dist/protocol/conversation-persistence.d.ts +2 -0
- package/dist/protocol/conversation-persistence.js +19 -1
- package/dist/protocol/conversation-state.d.ts +4 -0
- package/dist/protocol/conversation-state.js +4 -0
- package/dist/protocol/switch-mode.d.ts +5 -4
- package/dist/protocol/switch-mode.js +12 -4
- package/dist/protocol/tool-call-bridge.js +1 -1
- package/dist/protocol/tools.d.ts +36 -11
- package/dist/protocol/tools.js +747 -117
- package/dist/protocol/workspace-grounding.d.ts +17 -1
- package/dist/protocol/workspace-grounding.js +34 -5
- package/dist/session.d.ts +7 -1
- package/dist/shared.d.ts +3 -1
- package/dist/shared.js +3 -1
- package/dist/shell-timeout.d.ts +13 -0
- package/dist/shell-timeout.js +38 -0
- package/dist/todo-store.d.ts +29 -0
- package/dist/todo-store.js +78 -0
- package/dist/web-tools.d.ts +13 -0
- package/dist/web-tools.js +83 -5
- package/package.json +6 -3
package/dist/language-model.js
CHANGED
|
@@ -6,10 +6,10 @@ import { trace, traceRequestContextPaths } from "./debug.js";
|
|
|
6
6
|
import { buildRunRequest, buildHeartbeat } from "./protocol/request.js";
|
|
7
7
|
import { decodeFramePayload } from "./protocol/framing.js";
|
|
8
8
|
import { debugWalkTurnEnded, decodeMessage, encodeMessage } from "./protocol/messages.js";
|
|
9
|
-
import { parseExecServerMessage, buildToolCallPart, buildExecClientMessages, buildReadRejectionMessages, buildUnsupportedExecDeny, classifyMissingReadTarget, isUriReadTarget, resolveReadTargetPath, parseExecIdFromToolCallId, detectExecVariantField, buildRequestContextResult, buildMcpStateResult, buildListMcpResourcesFallback, buildReadMcpResourceFallback, buildCustomWebToolAliases, extractHostSubagentCatalog, resolveCustomWebToolAlias, remapNativeSubagentForCatalog, remapCorrelatedEditWriteForCatalog, remapEditToolsForCatalog, buildCompleteEditReadMessages, rejectPartialReadMutation, binaryWritePayload, CUSTOM_WEBFETCH_TOOL, CUSTOM_WEBSEARCH_TOOL, CUSTOM_LIST_MCP_RESOURCES_TOOL, CUSTOM_READ_MCP_RESOURCE_TOOL, } from "./protocol/tools.js";
|
|
9
|
+
import { parseExecServerMessage, buildToolCallPart, buildExecClientMessages, buildReadRejectionMessages, buildUnsupportedExecDeny, classifyMissingReadTarget, isUriReadTarget, resolveReadTargetPath, parseExecIdFromToolCallId, detectExecVariantField, buildRequestContextResult, buildMcpStateResult, buildListMcpResourcesFallback, buildReadMcpResourceFallback, buildCustomWebToolAliases, extractHostSubagentCatalog, resolveCustomWebToolAlias, remapNativeSubagentForCatalog, preferCorrelatedTaskDescription, remapCorrelatedEditWriteForCatalog, remapEditToolsForCatalog, buildCompleteEditReadMessages, rejectPartialReadMutation, binaryWritePayload, CUSTOM_WEBFETCH_TOOL, CUSTOM_WEBSEARCH_TOOL, CUSTOM_LIST_MCP_RESOURCES_TOOL, CUSTOM_READ_MCP_RESOURCE_TOOL, hostToolDialectFromTools, opencodePathArg, } from "./protocol/tools.js";
|
|
10
10
|
import { buildGitDiffExecMessages } from "./protocol/git-diff.js";
|
|
11
11
|
import { cursorExecVariantByRequestField, describeCursorExecVariant } from "./protocol/exec-variants.js";
|
|
12
|
-
import { appendWorkspaceRootGrounding } from "./protocol/workspace-grounding.js";
|
|
12
|
+
import { appendCheckpointUserGrounding, appendWorkspaceRootGrounding } from "./protocol/workspace-grounding.js";
|
|
13
13
|
import { progressOnlyContinuationPrompt, shouldContinueProgressOnlyTurn, } from "./protocol/progress-continuation.js";
|
|
14
14
|
import { advertisedToolNamesFromDescriptors, extractExecDisplayCallId, extractProtobufSubmessage, listProtobufFieldNumbers, parseDisplayToolCall, resolveBridgedOpenCodeToolCall, snapshotMirroredTodos, snapshotMirroredTodosFromReadOutput, } from "./protocol/tool-call-bridge.js";
|
|
15
15
|
import { handleKvServerMessage } from "./protocol/kv.js";
|
|
@@ -18,6 +18,7 @@ import { ASK_QUESTION_RESULT_FIELD, askQuestionResultFromToolOutput, askQuestion
|
|
|
18
18
|
import { SWITCH_MODE_RESULT_FIELD, getActiveCursorMode, isBridgedCursorPlanModeActive, isCursorPlanModeActive, setActiveCursorMode, switchModeResultFromQuestionOutput, switchModeResultFromToolOutput, switchModeToolInput, takeActiveCursorModeReminder, } from "./protocol/switch-mode.js";
|
|
19
19
|
import { CREATE_PLAN_NOT_APPROVED_REASON, CREATE_PLAN_RESULT_FIELD, CURSOR_PLAN_STAGE_TOOL, createPlanApproved, createPlanStageInput, } from "./protocol/create-plan.js";
|
|
20
20
|
import { flushPlanExecutionKickoff, formatPlanKickoffPath, hasPlanExecutionKickoff, planExecutionKickoffState, planPathFromUri, queuePlanExecutionKickoff, takePlanExecutionKickoffWarning, } from "./plan-execution-kickoff.js";
|
|
21
|
+
import { flushHostAgentModeSwitch, queueHostAgentModeSwitch, } from "./host-agent-mode.js";
|
|
21
22
|
import { CURSOR_IMAGE_SAVE_TOOL, imageMimeForPath, remapCursorImageWritePath, } from "./protocol/generate-image.js";
|
|
22
23
|
import { stageCursorImage } from "./image-staging.js";
|
|
23
24
|
import { IMAGE_PERMISSION_DENIED_PREFIX } from "./image-save.js";
|
|
@@ -34,7 +35,7 @@ import { getOrBuildRequestContext } from "./context/frozen.js";
|
|
|
34
35
|
import { workspaceRootFromRequestContext } from "./context/env.js";
|
|
35
36
|
import { ensureOpencodeProjectDir, opencodeGlobalCacheDir, setHostCacheDirOverride, } from "./context/paths.js";
|
|
36
37
|
import { resolveAgentUrl } from "./agent-url.js";
|
|
37
|
-
import { CURSOR_API_HOST, CURSOR_COMPACTION_OPTION } from "./shared.js";
|
|
38
|
+
import { CURSOR_API_HOST, CURSOR_COMPACTION_OPTION, CURSOR_HOST_AGENT_OPTION, } from "./shared.js";
|
|
38
39
|
import { isCompactionSession } from "./compaction-marker.js";
|
|
39
40
|
import { getSessionDirectory } from "./session-directory.js";
|
|
40
41
|
import { assertCursorUserImageSupport, extractCursorPromptImages } from "./image-input.js";
|
|
@@ -57,6 +58,7 @@ const sentHistoryImageHashesBySession = new Map();
|
|
|
57
58
|
// suppresses OpenCode's compacted prompt/system seed and makes Cursor narrate
|
|
58
59
|
// tool use instead of emitting exec requests. Rebase once on the next turn.
|
|
59
60
|
const postCompactionRebaseBySession = new Set();
|
|
61
|
+
const promptIdentityBySession = new Map();
|
|
60
62
|
export const MAX_TURN_STATE_SESSIONS = 256;
|
|
61
63
|
const MAX_SENT_HISTORY_IMAGES_PER_SESSION = 256;
|
|
62
64
|
const DEFAULT_RETRY_POLICY = {
|
|
@@ -373,6 +375,35 @@ function rememberPostCompactionRebase(sessionKey) {
|
|
|
373
375
|
postCompactionRebaseBySession.delete(oldest);
|
|
374
376
|
}
|
|
375
377
|
}
|
|
378
|
+
function normalizePromptIdentity(value) {
|
|
379
|
+
const hostAgent = value.hostAgent?.trim();
|
|
380
|
+
const systemPromptHash = value.systemPromptHash?.trim();
|
|
381
|
+
return {
|
|
382
|
+
...(hostAgent ? { hostAgent } : {}),
|
|
383
|
+
...(systemPromptHash ? { systemPromptHash } : {}),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function rememberPromptIdentity(sessionKey, value) {
|
|
387
|
+
const normalized = normalizePromptIdentity(value);
|
|
388
|
+
if (!normalized.hostAgent && !normalized.systemPromptHash)
|
|
389
|
+
return;
|
|
390
|
+
promptIdentityBySession.delete(sessionKey);
|
|
391
|
+
promptIdentityBySession.set(sessionKey, normalized);
|
|
392
|
+
while (promptIdentityBySession.size > MAX_TURN_STATE_SESSIONS) {
|
|
393
|
+
const oldest = promptIdentityBySession.keys().next().value;
|
|
394
|
+
if (!oldest)
|
|
395
|
+
break;
|
|
396
|
+
promptIdentityBySession.delete(oldest);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function promptIdentityChanged(previous, current) {
|
|
400
|
+
// Compare only facts the current host supplied. This keeps standalone calls
|
|
401
|
+
// and older compatibility surfaces from invalidating a valid checkpoint just
|
|
402
|
+
// because they cannot expose an agent id.
|
|
403
|
+
return ((current.hostAgent !== undefined && previous.hostAgent !== current.hostAgent)
|
|
404
|
+
|| (current.systemPromptHash !== undefined
|
|
405
|
+
&& previous.systemPromptHash !== current.systemPromptHash));
|
|
406
|
+
}
|
|
376
407
|
function sentHistoryImageHashes(sessionKey) {
|
|
377
408
|
if (!sessionKey)
|
|
378
409
|
return undefined;
|
|
@@ -538,6 +569,12 @@ async function doStreamImpl(modelId, options, callOptions) {
|
|
|
538
569
|
setActiveCursorMode(activeSession.openCodeSessionId, "plan");
|
|
539
570
|
}
|
|
540
571
|
}
|
|
572
|
+
await flushHostAgentModeSwitch(activeSession.openCodeSessionId, {
|
|
573
|
+
cursorSessionID: activeSession.sessionId,
|
|
574
|
+
terminal: activeSession.closed,
|
|
575
|
+
pumpActive: activeSession.pumpActive || activeSession.pumpOwner != null,
|
|
576
|
+
pendingExecs: activeSession.pending.size,
|
|
577
|
+
});
|
|
541
578
|
}
|
|
542
579
|
catch (e) {
|
|
543
580
|
activeSession.pumpActive = false;
|
|
@@ -693,13 +730,25 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
693
730
|
rememberPostCompactionRebase(sessionKey);
|
|
694
731
|
if (restored?.toolCatalog.length)
|
|
695
732
|
restoreTurnToolCatalog(sessionKey, restored.toolCatalog);
|
|
733
|
+
if (restored?.hostAgent || restored?.systemPromptHash) {
|
|
734
|
+
rememberPromptIdentity(sessionKey, {
|
|
735
|
+
...(restored.hostAgent ? { hostAgent: restored.hostAgent } : {}),
|
|
736
|
+
...(restored.systemPromptHash ? { systemPromptHash: restored.systemPromptHash } : {}),
|
|
737
|
+
});
|
|
738
|
+
}
|
|
696
739
|
}
|
|
697
740
|
const providerOptions = callOptions.providerOptions?.cursor;
|
|
741
|
+
const hostAgent = typeof providerOptions?.[CURSOR_HOST_AGENT_OPTION] === "string"
|
|
742
|
+
? String(providerOptions[CURSOR_HOST_AGENT_OPTION]).trim() || undefined
|
|
743
|
+
: undefined;
|
|
698
744
|
// The classic plugin marks OpenCode's agent="compaction" through chat.params.
|
|
699
|
-
// OpenCode 2.0 removed that hook, so its plugin
|
|
700
|
-
//
|
|
745
|
+
// OpenCode 2.0 removed that hook, so its plugin writes the same request-local
|
|
746
|
+
// option from session hooks. The session marker is a fallback only when a
|
|
747
|
+
// host does not preserve mutable options; explicit false prevents concurrent
|
|
748
|
+
// title/generate calls from inheriting a compaction marker.
|
|
701
749
|
// Do not infer this from tools/toolChoice: standalone no-tool calls are valid.
|
|
702
|
-
const
|
|
750
|
+
const compactionOption = providerOptions?.[CURSOR_COMPACTION_OPTION];
|
|
751
|
+
const isCompaction = compactionOption === true || (compactionOption === undefined && isCompactionSession(sessionKey));
|
|
703
752
|
const toolState = await resolveTurnToolState({
|
|
704
753
|
sessionKey,
|
|
705
754
|
incomingTools,
|
|
@@ -718,14 +767,31 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
718
767
|
}
|
|
719
768
|
const allowTools = toolState.allowTools;
|
|
720
769
|
const discoveredSubagentCatalog = extractHostSubagentCatalog(cursorTools);
|
|
721
|
-
const resetState = resolveTurnConversationReset({ sessionKey, isCompaction });
|
|
722
770
|
let recovery = startOptions?.recovery;
|
|
723
771
|
let resumeRecovery = recovery?.kind === "resume" ? recovery : undefined;
|
|
724
772
|
let resuming = !!resumeRecovery;
|
|
773
|
+
const lifecycle = !allowTools && !isCompaction && !recovery;
|
|
774
|
+
// v1 sets `options.workspaceRoot` correctly per invocation (`input.directory`,
|
|
775
|
+
// one plugin instance per project). OpenCode 2.0 runs one daemon across many
|
|
776
|
+
// projects, so its static option is only a fallback for the directory recorded
|
|
777
|
+
// from `session.hook("context")`.
|
|
778
|
+
const workspaceRoot = path.resolve(getSessionDirectory(sessionKey) ?? (options.workspaceRoot || process.cwd()));
|
|
779
|
+
const baseSystemPrompt = extractSystemPrompt(prompt);
|
|
780
|
+
const interactionGuidance = buildOpenCodeInteractionGuidance(cursorTools, isCompaction, workspaceRoot);
|
|
781
|
+
const stableSystemPrompt = [baseSystemPrompt, interactionGuidance].filter(Boolean).join("\n\n");
|
|
782
|
+
const stableSystemPromptHash = stableSystemPrompt
|
|
783
|
+
? createHash("sha256").update(stableSystemPrompt).digest("hex")
|
|
784
|
+
: undefined;
|
|
785
|
+
const resetState = resolveTurnConversationReset({
|
|
786
|
+
sessionKey,
|
|
787
|
+
isCompaction,
|
|
788
|
+
...(lifecycle
|
|
789
|
+
? {}
|
|
790
|
+
: { promptIdentity: { hostAgent, systemPromptHash: stableSystemPromptHash } }),
|
|
791
|
+
});
|
|
725
792
|
// Compaction must not reuse the prior conversation; its first normal turn
|
|
726
793
|
// must also rebase so the summary-agent checkpoint cannot replace the normal
|
|
727
794
|
// system prompt and OpenCode's newly compacted history.
|
|
728
|
-
const lifecycle = !allowTools && !isCompaction && !recovery;
|
|
729
795
|
let bound = resuming
|
|
730
796
|
? { conversationId: resumeRecovery.conversationId, reset: false, previousId: undefined }
|
|
731
797
|
: bindConversationId(sessionKey, {
|
|
@@ -778,21 +844,19 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
778
844
|
`previousId=${bound.previousId ?? "-"} → conversationId=${conversationId}`);
|
|
779
845
|
}
|
|
780
846
|
const lastUser = [...prompt].reverse().find((message) => message.role === "user");
|
|
781
|
-
|
|
847
|
+
let userText = recovery?.kind === "rebase"
|
|
782
848
|
? "Continue the interrupted turn from the conversation history above. Do not repeat completed work."
|
|
783
849
|
: (extractUserText(lastUser) || ".");
|
|
784
|
-
// v1 sets `options.workspaceRoot` correctly per invocation (`input.directory`,
|
|
785
|
-
// one plugin instance per project). OpenCode 2.0 runs one daemon across many
|
|
786
|
-
// projects, so its static `options.workspaceRoot` is only a last-resort
|
|
787
|
-
// fallback; `getSessionDirectory` carries the real per-session directory
|
|
788
|
-
// recorded from `session.hook("context")` in `plugin-opencode2.ts`.
|
|
789
|
-
const workspaceRoot = path.resolve(getSessionDirectory(sessionKey) ?? (options.workspaceRoot || process.cwd()));
|
|
790
|
-
const baseSystemPrompt = extractSystemPrompt(prompt);
|
|
791
|
-
const interactionGuidance = buildOpenCodeInteractionGuidance(cursorTools, isCompaction, workspaceRoot);
|
|
792
850
|
// After an approved SwitchMode, inject the Cursor CLI-shaped mode reminder
|
|
793
851
|
// (same <system_reminder> contract the CLI uses after flipping unifiedMode).
|
|
794
852
|
const startedWithCheckpoint = !!conversationState;
|
|
795
|
-
|
|
853
|
+
if (startedWithCheckpoint) {
|
|
854
|
+
userText = groundCheckpointTurnText(userText, true, workspaceRoot, cursorTools);
|
|
855
|
+
}
|
|
856
|
+
const activeMode = getActiveCursorMode(sessionKey);
|
|
857
|
+
const nativePlanPromptOwnsMode = hostAgent === "plan"
|
|
858
|
+
&& (activeMode === "plan" || activeMode === "spec");
|
|
859
|
+
const modeReminder = isCompaction || startedWithCheckpoint || lifecycle || nativePlanPromptOwnsMode
|
|
796
860
|
? undefined
|
|
797
861
|
: takeActiveCursorModeReminder(sessionKey, {
|
|
798
862
|
advertisedTools: cursorTools.map((tool) => tool.name),
|
|
@@ -983,6 +1047,9 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
983
1047
|
stream.destroy();
|
|
984
1048
|
throw error;
|
|
985
1049
|
}
|
|
1050
|
+
const hostToolDialect = hostToolDialectFromTools(tools);
|
|
1051
|
+
trace(`host tool dialect: filePathKey=${hostToolDialect.filePathKey} shellTool=${hostToolDialect.shellTool} ` +
|
|
1052
|
+
`tools=[${tools.map((t) => t.name).join(",")}]`);
|
|
986
1053
|
const session = {
|
|
987
1054
|
sessionId: crypto.randomUUID(),
|
|
988
1055
|
conversationId,
|
|
@@ -1009,6 +1076,8 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
1009
1076
|
execRequests: 0,
|
|
1010
1077
|
},
|
|
1011
1078
|
openCodeSessionId: lifecycle ? undefined : sessionKey,
|
|
1079
|
+
hostAgent,
|
|
1080
|
+
stableSystemPromptHash,
|
|
1012
1081
|
postCompactionRebase: isCompaction,
|
|
1013
1082
|
toolCatalog: snapshotToolCatalog(sessionKey),
|
|
1014
1083
|
stream,
|
|
@@ -1024,6 +1093,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
1024
1093
|
blobs: new Map(),
|
|
1025
1094
|
toolDescriptors,
|
|
1026
1095
|
toolAliases: webToolAliases.aliases,
|
|
1096
|
+
hostToolDialect,
|
|
1027
1097
|
subagentCatalog,
|
|
1028
1098
|
requestContext,
|
|
1029
1099
|
allowTools,
|
|
@@ -1216,7 +1286,7 @@ function buildCreatePlanContinuationFrame(pending, result, sessionKey, cursorSes
|
|
|
1216
1286
|
plan_uri: planUri,
|
|
1217
1287
|
});
|
|
1218
1288
|
}
|
|
1219
|
-
function buildSwitchModeContinuationFrame(pending, result
|
|
1289
|
+
function buildSwitchModeContinuationFrame(pending, result) {
|
|
1220
1290
|
const metadata = pending.resultMetadata ?? {};
|
|
1221
1291
|
const interactionId = metadata.interactionId;
|
|
1222
1292
|
if (typeof interactionId !== "number") {
|
|
@@ -1228,16 +1298,13 @@ function buildSwitchModeContinuationFrame(pending, result, sessionKey) {
|
|
|
1228
1298
|
const answer = metadata.switchModeBridgeKind === "question"
|
|
1229
1299
|
? switchModeResultFromQuestionOutput(result.output, result.error !== undefined)
|
|
1230
1300
|
: switchModeResultFromToolOutput(result.output, result.error !== undefined);
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
return buildSwitchModeInteractionReply(interactionId, answer);
|
|
1301
|
+
const target = "approved" in answer && typeof metadata.switchModeTarget === "string"
|
|
1302
|
+
? metadata.switchModeTarget.trim()
|
|
1303
|
+
: "";
|
|
1304
|
+
return {
|
|
1305
|
+
frame: buildSwitchModeInteractionReply(interactionId, answer),
|
|
1306
|
+
...(target ? { approvedTarget: target, bridgeKind: metadata.switchModeBridgeKind } : {}),
|
|
1307
|
+
};
|
|
1241
1308
|
}
|
|
1242
1309
|
/**
|
|
1243
1310
|
* Deliver trailing tool results onto a live continuation session.
|
|
@@ -1265,6 +1332,7 @@ export function deliverContinuationResults(session, trailingToolResults) {
|
|
|
1265
1332
|
}
|
|
1266
1333
|
const pending = claim.pending;
|
|
1267
1334
|
let frames = [];
|
|
1335
|
+
let deliveredSwitchMode;
|
|
1268
1336
|
if (pending.resultField === ASK_QUESTION_RESULT_FIELD) {
|
|
1269
1337
|
// A bridged Cursor AskQuestion. The host tool result carries the user's
|
|
1270
1338
|
// choices; translate them back into the Cursor result the interaction is
|
|
@@ -1284,7 +1352,14 @@ export function deliverContinuationResults(session, trailingToolResults) {
|
|
|
1284
1352
|
// approved{} or rejected{reason} on the still-open InteractionQuery.
|
|
1285
1353
|
// Approval also arms the CLI-shaped mode reminder for the next Run.
|
|
1286
1354
|
try {
|
|
1287
|
-
|
|
1355
|
+
const built = buildSwitchModeContinuationFrame(pending, r);
|
|
1356
|
+
frames = [built.frame];
|
|
1357
|
+
if (built.approvedTarget) {
|
|
1358
|
+
deliveredSwitchMode = {
|
|
1359
|
+
target: built.approvedTarget,
|
|
1360
|
+
bridgeKind: built.bridgeKind,
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1288
1363
|
}
|
|
1289
1364
|
catch (error) {
|
|
1290
1365
|
trace(`continuation: switch_mode encode FAILED execId=${r.execId} err=${error.message}`);
|
|
@@ -1405,6 +1480,22 @@ export function deliverContinuationResults(session, trailingToolResults) {
|
|
|
1405
1480
|
continue;
|
|
1406
1481
|
return undefined;
|
|
1407
1482
|
}
|
|
1483
|
+
if (deliveredSwitchMode) {
|
|
1484
|
+
const normalized = deliveredSwitchMode.target.toLowerCase();
|
|
1485
|
+
setActiveCursorMode(session.openCodeSessionId, deliveredSwitchMode.target, {
|
|
1486
|
+
bridgedPlanEntered: normalized === "plan" || normalized === "spec",
|
|
1487
|
+
});
|
|
1488
|
+
// A question-emulated exit has no native host plan tool to perform the
|
|
1489
|
+
// actual primary-agent switch. Queue only after Cursor received the
|
|
1490
|
+
// approved response; a failed write must not mutate host mode.
|
|
1491
|
+
if (deliveredSwitchMode.bridgeKind === "question" && session.openCodeSessionId) {
|
|
1492
|
+
queueHostAgentModeSwitch({
|
|
1493
|
+
sessionID: session.openCodeSessionId,
|
|
1494
|
+
targetModeID: deliveredSwitchMode.target,
|
|
1495
|
+
cursorSessionID: session.sessionId,
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1408
1499
|
// A host `todoread` result is the authoritative list. Refresh the mirrored
|
|
1409
1500
|
// snapshot (direct reads and bridged native reads alike) so later merges
|
|
1410
1501
|
// apply onto host truth, not a stale write.
|
|
@@ -1686,7 +1777,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1686
1777
|
const display = parseDisplayToolCall(displayCallId, stored);
|
|
1687
1778
|
if (display?.variant !== "edit_tool_call" || display.bridgeable === false)
|
|
1688
1779
|
return false;
|
|
1689
|
-
const requestedPath =
|
|
1780
|
+
const requestedPath = opencodePathArg(parsed.args) ?? "";
|
|
1690
1781
|
const editPath = typeof display.args.path === "string" ? display.args.path : "";
|
|
1691
1782
|
if (!requestedPath || !editPath)
|
|
1692
1783
|
return false;
|
|
@@ -1784,7 +1875,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1784
1875
|
const rejectMissingReadTarget = (parsed) => {
|
|
1785
1876
|
if (parsed.toolName !== "read")
|
|
1786
1877
|
return false;
|
|
1787
|
-
const requested =
|
|
1878
|
+
const requested = opencodePathArg(parsed.args) ?? "";
|
|
1788
1879
|
if (!requested)
|
|
1789
1880
|
return false;
|
|
1790
1881
|
// A scheme-addressed target is the advertised host executor's to resolve,
|
|
@@ -2084,6 +2175,8 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2084
2175
|
requestContext: session.requestContext,
|
|
2085
2176
|
toolCatalog: session.toolCatalog ?? [],
|
|
2086
2177
|
postCompactionRebase: session.postCompactionRebase,
|
|
2178
|
+
hostAgent: session.hostAgent,
|
|
2179
|
+
systemPromptHash: session.stableSystemPromptHash,
|
|
2087
2180
|
}).catch((error) => {
|
|
2088
2181
|
trace(`conversation persistence: TurnEnded save failed ` +
|
|
2089
2182
|
`sessionKey=${session.openCodeSessionId}: ${String(error)}`);
|
|
@@ -2312,7 +2405,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2312
2405
|
else {
|
|
2313
2406
|
replaySafety.markBarrier("non-control-exec");
|
|
2314
2407
|
const displayCallId = extractExecDisplayCallId(esm);
|
|
2315
|
-
const parsed = parseExecServerMessage(esm);
|
|
2408
|
+
const parsed = parseExecServerMessage(esm, session.hostToolDialect);
|
|
2316
2409
|
if (parsed) {
|
|
2317
2410
|
const executableToolName = resolveCustomWebToolAlias(parsed.toolName, session.toolAliases);
|
|
2318
2411
|
if (executableToolName !== parsed.toolName) {
|
|
@@ -2320,6 +2413,20 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2320
2413
|
parsed.toolName = executableToolName;
|
|
2321
2414
|
}
|
|
2322
2415
|
remapNativeSubagentForCatalog(parsed, advertisedToolNameSet, session.subagentCatalog);
|
|
2416
|
+
// SubagentArgs has no description; Cursor's real title lives on the
|
|
2417
|
+
// correlated display TaskToolCall. Prefer that over the 5-word prompt slice.
|
|
2418
|
+
if (displayCallId && (parsed.toolName === "task" || parsed.toolName === "subagent")) {
|
|
2419
|
+
const stored = session.displayToolCalls.get(displayCallId);
|
|
2420
|
+
const display = parseDisplayToolCall(displayCallId, stored);
|
|
2421
|
+
if (display?.variant === "task_tool_call") {
|
|
2422
|
+
const description = typeof display.args.description === "string" ? display.args.description : undefined;
|
|
2423
|
+
preferCorrelatedTaskDescription(parsed, description);
|
|
2424
|
+
if (description?.trim()) {
|
|
2425
|
+
trace(`exec: preferred TaskToolCall description callId=${displayCallId} ` +
|
|
2426
|
+
`description=${JSON.stringify(description.trim())}`);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2323
2430
|
const editCall = displayCallId
|
|
2324
2431
|
? session.editToolCalls?.get(displayCallId)
|
|
2325
2432
|
: undefined;
|
|
@@ -2605,9 +2712,17 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2605
2712
|
setActiveCursorMode(session.openCodeSessionId, sw.args.targetModeId, {
|
|
2606
2713
|
bridgedPlanEntered: false,
|
|
2607
2714
|
});
|
|
2715
|
+
const hostAgentSwitchQueued = session.openCodeSessionId
|
|
2716
|
+
? queueHostAgentModeSwitch({
|
|
2717
|
+
sessionID: session.openCodeSessionId,
|
|
2718
|
+
targetModeID: sw.args.targetModeId,
|
|
2719
|
+
cursorSessionID: session.sessionId,
|
|
2720
|
+
})
|
|
2721
|
+
: false;
|
|
2608
2722
|
trace(`interaction_query: APPROVED switch_mode id=${handled.id} ` +
|
|
2609
2723
|
`target=${JSON.stringify(sw.args.targetModeId)} (no host plan tool; ` +
|
|
2610
|
-
|
|
2724
|
+
`${hostAgentSwitchQueued ? "native host-agent switch queued" : "provider-owned fallback"}) ` +
|
|
2725
|
+
`cursorToolCallId=${sw.toolCallId || "(none)"}`);
|
|
2611
2726
|
continue;
|
|
2612
2727
|
}
|
|
2613
2728
|
if (handled.outcome === "bridged" && handled.switchMode) {
|
|
@@ -2822,6 +2937,18 @@ function extractSystemPrompt(prompt) {
|
|
|
2822
2937
|
}
|
|
2823
2938
|
return parts.length > 0 ? parts.join("\n\n") : undefined;
|
|
2824
2939
|
}
|
|
2940
|
+
/**
|
|
2941
|
+
* Checkpointed Runs do not resend the system prompt. Keep the workspace root
|
|
2942
|
+
* on the live user message, and require absolute `path` arguments when that is
|
|
2943
|
+
* the host's file-tool dialect.
|
|
2944
|
+
*/
|
|
2945
|
+
export function groundCheckpointTurnText(userText, checkpoint, workspaceRoot, tools) {
|
|
2946
|
+
if (!checkpoint)
|
|
2947
|
+
return userText;
|
|
2948
|
+
return appendCheckpointUserGrounding(userText, workspaceRoot, {
|
|
2949
|
+
requireAbsolutePathArg: hostToolDialectFromTools(tools).filePathKey === "path",
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2825
2952
|
/**
|
|
2826
2953
|
* Cursor's native UI interactions cannot be surfaced through the AI SDK.
|
|
2827
2954
|
* Redirect only to OpenCode tools that are genuinely advertised this turn;
|
|
@@ -2875,8 +3002,14 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
2875
3002
|
if (names.has(CUSTOM_READ_MCP_RESOURCE_TOOL)) {
|
|
2876
3003
|
instructions.push(`- To read an MCP resource, call \`${CUSTOM_READ_MCP_RESOURCE_TOOL}\`; do not use Cursor's native resource-reading interaction.`);
|
|
2877
3004
|
}
|
|
2878
|
-
if (names.has("
|
|
2879
|
-
const
|
|
3005
|
+
if (names.has("execute")) {
|
|
3006
|
+
const shell = names.has("shell") ? "`shell`" : names.has("bash") ? "`bash`" : undefined;
|
|
3007
|
+
instructions.push(shell
|
|
3008
|
+
? `- OpenCode \`execute\` is Code Mode JavaScript (\`code\`); it is not a shell. For OS commands, call OpenCode ${shell}. Do not pass \`command\` to \`execute\`.`
|
|
3009
|
+
: "- OpenCode `execute` is Code Mode JavaScript (`code`); it is not a shell. Do not pass `command` to `execute`.");
|
|
3010
|
+
}
|
|
3011
|
+
if (names.has("task") || names.has("subagent")) {
|
|
3012
|
+
const target = names.has("task") ? "`task`" : "`subagent`";
|
|
2880
3013
|
const available = subagents.agents.map((agent) => `\`${agent.name}\``).join(", ");
|
|
2881
3014
|
instructions.push(`- Native Cursor Task/subagent requests are executed through OpenCode ${target}. ` +
|
|
2882
3015
|
"Advertised custom subagent names are used exactly; otherwise `unspecified` and `generalPurpose` select host `general`, " +
|
|
@@ -2897,6 +3030,9 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
2897
3030
|
// leaving the turn with no file-editing guidance at all.
|
|
2898
3031
|
instructions.push("- Use OpenCode `apply_patch` for file-content changes; do not use shell, Python, or heredocs to change file content while it is available. Cursor-native write and edit requests are accepted and converted to `apply_patch` automatically.");
|
|
2899
3032
|
}
|
|
3033
|
+
if (hostToolDialectFromTools(tools).filePathKey === "path") {
|
|
3034
|
+
instructions.push("- OpenCode file tools take `path` as an absolute path under the workspace root above. Do not pass a project-relative path, and do not invent a different absolute prefix.");
|
|
3035
|
+
}
|
|
2900
3036
|
if (names.has("edit") || names.has("write") || names.has("apply_patch")) {
|
|
2901
3037
|
instructions.push("- Never use a read result as complete file content when it says the output is capped, partial, or requires another offset. Read the remaining ranges first, or make a targeted edit/patch from complete context; do not pass a partial read back as a whole-file replacement.");
|
|
2902
3038
|
}
|
|
@@ -3143,9 +3279,25 @@ export function resolveTurnConversationReset(input) {
|
|
|
3143
3279
|
rememberPostCompactionRebase(sessionKey);
|
|
3144
3280
|
return { reset: true, reason: "compaction" };
|
|
3145
3281
|
}
|
|
3282
|
+
let promptChange;
|
|
3283
|
+
if (sessionKey && input.promptIdentity) {
|
|
3284
|
+
const current = normalizePromptIdentity(input.promptIdentity);
|
|
3285
|
+
const previous = promptIdentityBySession.get(sessionKey);
|
|
3286
|
+
if (previous && promptIdentityChanged(previous, current)) {
|
|
3287
|
+
promptChange = current.hostAgent !== undefined && previous.hostAgent !== current.hostAgent
|
|
3288
|
+
? "agent-change"
|
|
3289
|
+
: "system-prompt-change";
|
|
3290
|
+
}
|
|
3291
|
+
rememberPromptIdentity(sessionKey, {
|
|
3292
|
+
...previous,
|
|
3293
|
+
...current,
|
|
3294
|
+
});
|
|
3295
|
+
}
|
|
3146
3296
|
if (sessionKey && postCompactionRebaseBySession.delete(sessionKey)) {
|
|
3147
3297
|
return { reset: true, reason: "post-compaction-rebase" };
|
|
3148
3298
|
}
|
|
3299
|
+
if (promptChange)
|
|
3300
|
+
return { reset: true, reason: promptChange };
|
|
3149
3301
|
return { reset: false };
|
|
3150
3302
|
}
|
|
3151
3303
|
export function resetTurnStateForTests() {
|
|
@@ -3156,6 +3308,8 @@ export function resetTurnStateForTests() {
|
|
|
3156
3308
|
toolCatalogWaitersBySession.clear();
|
|
3157
3309
|
toolCatalogBySession.clear();
|
|
3158
3310
|
postCompactionRebaseBySession.clear();
|
|
3311
|
+
promptIdentityBySession.clear();
|
|
3312
|
+
mirroredTodosBySession.clear();
|
|
3159
3313
|
}
|
|
3160
3314
|
function extractUserText(lastUser) {
|
|
3161
3315
|
if (!lastUser)
|
package/dist/models.d.ts
CHANGED
|
@@ -84,9 +84,20 @@ export declare function fetchModels(token: string, options?: {
|
|
|
84
84
|
headers?: Record<string, string>;
|
|
85
85
|
timeoutMs?: number;
|
|
86
86
|
}): Promise<ModelInfo[]>;
|
|
87
|
-
export declare function refreshModelCache(cacheDir: string, fetcher: () => Promise<ModelInfo[]
|
|
87
|
+
export declare function refreshModelCache(cacheDir: string, fetcher: () => Promise<ModelInfo[]>, options?: {
|
|
88
|
+
forceAfterInflight?: boolean;
|
|
89
|
+
}): Promise<ModelInfo[]>;
|
|
88
90
|
export declare function discoverModels(token: string, cacheDir: string, options?: {
|
|
89
91
|
baseURL?: string;
|
|
90
92
|
headers?: Record<string, string>;
|
|
91
93
|
timeoutMs?: number;
|
|
94
|
+
/**
|
|
95
|
+
* Bypass even a fresh cache and require an authoritative response.
|
|
96
|
+
*
|
|
97
|
+
* Model availability is account-scoped. Callers use this after a
|
|
98
|
+
* credential switch so a fresh cache produced by the previous account is
|
|
99
|
+
* never rebound to the new connection. A forced refresh rejects on fetch
|
|
100
|
+
* failure; the caller can keep the already-published inventory unchanged.
|
|
101
|
+
*/
|
|
102
|
+
forceRefresh?: boolean;
|
|
92
103
|
}): Promise<ModelInfo[]>;
|
package/dist/models.js
CHANGED
|
@@ -396,11 +396,18 @@ export async function fetchModels(token, options = {}) {
|
|
|
396
396
|
return mapAvailableModelsResponse(raw);
|
|
397
397
|
}
|
|
398
398
|
const refreshesByDirectory = new Map();
|
|
399
|
-
export async function refreshModelCache(cacheDir, fetcher) {
|
|
399
|
+
export async function refreshModelCache(cacheDir, fetcher, options = {}) {
|
|
400
400
|
const key = path.resolve(cacheDir);
|
|
401
401
|
const existing = refreshesByDirectory.get(key);
|
|
402
|
-
if (existing)
|
|
403
|
-
|
|
402
|
+
if (existing) {
|
|
403
|
+
if (!options.forceAfterInflight)
|
|
404
|
+
return existing;
|
|
405
|
+
// An account switch must not join a background refresh authenticated as
|
|
406
|
+
// the previous account. Wait for that write to finish, then run this
|
|
407
|
+
// caller's fetch so its account inventory is authoritative on disk.
|
|
408
|
+
await existing.catch(() => { });
|
|
409
|
+
return refreshModelCache(cacheDir, fetcher, options);
|
|
410
|
+
}
|
|
404
411
|
const refresh = (async () => {
|
|
405
412
|
const models = await fetcher();
|
|
406
413
|
await writeCache(cacheDir, {
|
|
@@ -420,10 +427,11 @@ export async function refreshModelCache(cacheDir, fetcher) {
|
|
|
420
427
|
}
|
|
421
428
|
}
|
|
422
429
|
export async function discoverModels(token, cacheDir, options = {}) {
|
|
430
|
+
const { forceRefresh = false, ...fetchOptions } = options;
|
|
423
431
|
const cached = await readCache(cacheDir);
|
|
424
|
-
const refresh = () => refreshModelCache(cacheDir, () => fetchModels(token,
|
|
432
|
+
const refresh = () => refreshModelCache(cacheDir, () => fetchModels(token, fetchOptions), { forceAfterInflight: forceRefresh });
|
|
425
433
|
// Cache is fresh → return it; refresh in background
|
|
426
|
-
if (cached && isCacheFresh(cached)) {
|
|
434
|
+
if (!forceRefresh && cached && isCacheFresh(cached)) {
|
|
427
435
|
// Background refresh (fire and forget)
|
|
428
436
|
void refresh()
|
|
429
437
|
.catch(() => {
|
|
@@ -436,7 +444,9 @@ export async function discoverModels(token, cacheDir, options = {}) {
|
|
|
436
444
|
try {
|
|
437
445
|
return await refresh();
|
|
438
446
|
}
|
|
439
|
-
catch {
|
|
447
|
+
catch (error) {
|
|
448
|
+
if (forceRefresh)
|
|
449
|
+
throw error;
|
|
440
450
|
return cached.models;
|
|
441
451
|
}
|
|
442
452
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type ModelInfo } from "../models.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type OpenCode2ModelCost } from "../pricing.js";
|
|
3
|
+
import type { ConnectionInfo, ModelVariantInfo, ProviderEditor } from "./types.js";
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
* classic plugin's `config` hook.
|
|
5
|
+
* In-memory provider registration for the OpenCode 2.0 plugin — the replacement
|
|
6
|
+
* for the classic plugin's `config` hook.
|
|
6
7
|
*
|
|
7
8
|
* Model naming, thinking suffixes, and long-context tiering are NOT reimplemented
|
|
8
9
|
* here: we run the shared `modelsToConfig` and translate its output into the 2.0
|
|
@@ -14,10 +15,10 @@ export declare const CURSOR_INTEGRATION_ID = "cursor";
|
|
|
14
15
|
* `aisdk:` selects OpenCode 2.0's AI SDK path, which is what surfaces the
|
|
15
16
|
* `aisdk.hook("sdk")` / `("language")` extension points we supply the provider
|
|
16
17
|
* through. The suffix is this package's npm name so the host's built-in
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* fallback can still resolve it if our own hook is ever bypassed — that
|
|
19
|
+
* fallback runs `npm.add(pkg)` against the *published* registry into
|
|
20
|
+
* `<host-cache>/packages/<pkg>/node_modules/<pkg>`, ignoring any local
|
|
21
|
+
* `file://` plugin path this process was loaded from.
|
|
21
22
|
*
|
|
22
23
|
* `CURSOR_OPENCODE2_DEV_ENTRY` overrides the suffix with an `aisdk:file://…`
|
|
23
24
|
* spec instead, pointed at a local built entry file (e.g. `dist/index.js`,
|
|
@@ -27,7 +28,41 @@ export declare const CURSOR_INTEGRATION_ID = "cursor";
|
|
|
27
28
|
* Unset in production; only meant for local `opencode2 run` testing.
|
|
28
29
|
*/
|
|
29
30
|
export declare const CURSOR_AISDK_PACKAGE: string;
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
export
|
|
31
|
+
/**
|
|
32
|
+
* Plain-object `Model.Info` equivalent used by `ctx.provider.transform`.
|
|
33
|
+
*/
|
|
34
|
+
export type CatalogModelInfo = {
|
|
35
|
+
id: string;
|
|
36
|
+
modelID: string;
|
|
37
|
+
providerID: string;
|
|
38
|
+
name: string;
|
|
39
|
+
capabilities: {
|
|
40
|
+
tools: boolean;
|
|
41
|
+
input: string[];
|
|
42
|
+
output: string[];
|
|
43
|
+
};
|
|
44
|
+
limit: {
|
|
45
|
+
context: number;
|
|
46
|
+
output: number;
|
|
47
|
+
};
|
|
48
|
+
variants: ModelVariantInfo[];
|
|
49
|
+
status: "active";
|
|
50
|
+
enabled: true;
|
|
51
|
+
time: {
|
|
52
|
+
released: number;
|
|
53
|
+
};
|
|
54
|
+
cost: OpenCode2ModelCost[];
|
|
55
|
+
settings?: Record<string, unknown>;
|
|
56
|
+
};
|
|
57
|
+
/** Translate one `modelsToConfig` entry into the 2.0 `Model.Info` shape. */
|
|
58
|
+
export declare function modelConfigEntryToInfo(id: string, entry: Record<string, any>): CatalogModelInfo;
|
|
59
|
+
/** Full model map for the in-memory provider inventory. */
|
|
60
|
+
export declare function modelsToCatalogModelMap(models: ModelInfo[]): Record<string, CatalogModelInfo>;
|
|
61
|
+
/**
|
|
62
|
+
* Publish discovered Cursor models into the live provider inventory.
|
|
63
|
+
*
|
|
64
|
+
* Skip while empty (keeps the last successful inventory through a no-op
|
|
65
|
+
* transform on first register), replace the definition with `editor.add`,
|
|
66
|
+
* then `ctx.provider.reload()`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function applyCursorProviderInventory(editor: ProviderEditor, models: ModelInfo[], sourceConnection?: ConnectionInfo): void;
|