cursor-opencode-provider 0.6.8 → 0.7.0
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 +50 -180
- 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 +6 -1
- package/dist/language-model.js +170 -34
- 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 +30 -4
- package/dist/protocol/tools.js +185 -58
- 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,7 +6,7 @@ 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
12
|
import { appendWorkspaceRootGrounding } from "./protocol/workspace-grounding.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, {
|
|
@@ -781,18 +847,13 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
781
847
|
const 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
|
-
const
|
|
853
|
+
const activeMode = getActiveCursorMode(sessionKey);
|
|
854
|
+
const nativePlanPromptOwnsMode = hostAgent === "plan"
|
|
855
|
+
&& (activeMode === "plan" || activeMode === "spec");
|
|
856
|
+
const modeReminder = isCompaction || startedWithCheckpoint || lifecycle || nativePlanPromptOwnsMode
|
|
796
857
|
? undefined
|
|
797
858
|
: takeActiveCursorModeReminder(sessionKey, {
|
|
798
859
|
advertisedTools: cursorTools.map((tool) => tool.name),
|
|
@@ -983,6 +1044,9 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
983
1044
|
stream.destroy();
|
|
984
1045
|
throw error;
|
|
985
1046
|
}
|
|
1047
|
+
const hostToolDialect = hostToolDialectFromTools(tools);
|
|
1048
|
+
trace(`host tool dialect: filePathKey=${hostToolDialect.filePathKey} shellTool=${hostToolDialect.shellTool} ` +
|
|
1049
|
+
`tools=[${tools.map((t) => t.name).join(",")}]`);
|
|
986
1050
|
const session = {
|
|
987
1051
|
sessionId: crypto.randomUUID(),
|
|
988
1052
|
conversationId,
|
|
@@ -1009,6 +1073,8 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
1009
1073
|
execRequests: 0,
|
|
1010
1074
|
},
|
|
1011
1075
|
openCodeSessionId: lifecycle ? undefined : sessionKey,
|
|
1076
|
+
hostAgent,
|
|
1077
|
+
stableSystemPromptHash,
|
|
1012
1078
|
postCompactionRebase: isCompaction,
|
|
1013
1079
|
toolCatalog: snapshotToolCatalog(sessionKey),
|
|
1014
1080
|
stream,
|
|
@@ -1024,6 +1090,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
1024
1090
|
blobs: new Map(),
|
|
1025
1091
|
toolDescriptors,
|
|
1026
1092
|
toolAliases: webToolAliases.aliases,
|
|
1093
|
+
hostToolDialect,
|
|
1027
1094
|
subagentCatalog,
|
|
1028
1095
|
requestContext,
|
|
1029
1096
|
allowTools,
|
|
@@ -1216,7 +1283,7 @@ function buildCreatePlanContinuationFrame(pending, result, sessionKey, cursorSes
|
|
|
1216
1283
|
plan_uri: planUri,
|
|
1217
1284
|
});
|
|
1218
1285
|
}
|
|
1219
|
-
function buildSwitchModeContinuationFrame(pending, result
|
|
1286
|
+
function buildSwitchModeContinuationFrame(pending, result) {
|
|
1220
1287
|
const metadata = pending.resultMetadata ?? {};
|
|
1221
1288
|
const interactionId = metadata.interactionId;
|
|
1222
1289
|
if (typeof interactionId !== "number") {
|
|
@@ -1228,16 +1295,13 @@ function buildSwitchModeContinuationFrame(pending, result, sessionKey) {
|
|
|
1228
1295
|
const answer = metadata.switchModeBridgeKind === "question"
|
|
1229
1296
|
? switchModeResultFromQuestionOutput(result.output, result.error !== undefined)
|
|
1230
1297
|
: switchModeResultFromToolOutput(result.output, result.error !== undefined);
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
return buildSwitchModeInteractionReply(interactionId, answer);
|
|
1298
|
+
const target = "approved" in answer && typeof metadata.switchModeTarget === "string"
|
|
1299
|
+
? metadata.switchModeTarget.trim()
|
|
1300
|
+
: "";
|
|
1301
|
+
return {
|
|
1302
|
+
frame: buildSwitchModeInteractionReply(interactionId, answer),
|
|
1303
|
+
...(target ? { approvedTarget: target, bridgeKind: metadata.switchModeBridgeKind } : {}),
|
|
1304
|
+
};
|
|
1241
1305
|
}
|
|
1242
1306
|
/**
|
|
1243
1307
|
* Deliver trailing tool results onto a live continuation session.
|
|
@@ -1265,6 +1329,7 @@ export function deliverContinuationResults(session, trailingToolResults) {
|
|
|
1265
1329
|
}
|
|
1266
1330
|
const pending = claim.pending;
|
|
1267
1331
|
let frames = [];
|
|
1332
|
+
let deliveredSwitchMode;
|
|
1268
1333
|
if (pending.resultField === ASK_QUESTION_RESULT_FIELD) {
|
|
1269
1334
|
// A bridged Cursor AskQuestion. The host tool result carries the user's
|
|
1270
1335
|
// choices; translate them back into the Cursor result the interaction is
|
|
@@ -1284,7 +1349,14 @@ export function deliverContinuationResults(session, trailingToolResults) {
|
|
|
1284
1349
|
// approved{} or rejected{reason} on the still-open InteractionQuery.
|
|
1285
1350
|
// Approval also arms the CLI-shaped mode reminder for the next Run.
|
|
1286
1351
|
try {
|
|
1287
|
-
|
|
1352
|
+
const built = buildSwitchModeContinuationFrame(pending, r);
|
|
1353
|
+
frames = [built.frame];
|
|
1354
|
+
if (built.approvedTarget) {
|
|
1355
|
+
deliveredSwitchMode = {
|
|
1356
|
+
target: built.approvedTarget,
|
|
1357
|
+
bridgeKind: built.bridgeKind,
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1288
1360
|
}
|
|
1289
1361
|
catch (error) {
|
|
1290
1362
|
trace(`continuation: switch_mode encode FAILED execId=${r.execId} err=${error.message}`);
|
|
@@ -1405,6 +1477,22 @@ export function deliverContinuationResults(session, trailingToolResults) {
|
|
|
1405
1477
|
continue;
|
|
1406
1478
|
return undefined;
|
|
1407
1479
|
}
|
|
1480
|
+
if (deliveredSwitchMode) {
|
|
1481
|
+
const normalized = deliveredSwitchMode.target.toLowerCase();
|
|
1482
|
+
setActiveCursorMode(session.openCodeSessionId, deliveredSwitchMode.target, {
|
|
1483
|
+
bridgedPlanEntered: normalized === "plan" || normalized === "spec",
|
|
1484
|
+
});
|
|
1485
|
+
// A question-emulated exit has no native host plan tool to perform the
|
|
1486
|
+
// actual primary-agent switch. Queue only after Cursor received the
|
|
1487
|
+
// approved response; a failed write must not mutate host mode.
|
|
1488
|
+
if (deliveredSwitchMode.bridgeKind === "question" && session.openCodeSessionId) {
|
|
1489
|
+
queueHostAgentModeSwitch({
|
|
1490
|
+
sessionID: session.openCodeSessionId,
|
|
1491
|
+
targetModeID: deliveredSwitchMode.target,
|
|
1492
|
+
cursorSessionID: session.sessionId,
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1408
1496
|
// A host `todoread` result is the authoritative list. Refresh the mirrored
|
|
1409
1497
|
// snapshot (direct reads and bridged native reads alike) so later merges
|
|
1410
1498
|
// apply onto host truth, not a stale write.
|
|
@@ -1686,7 +1774,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1686
1774
|
const display = parseDisplayToolCall(displayCallId, stored);
|
|
1687
1775
|
if (display?.variant !== "edit_tool_call" || display.bridgeable === false)
|
|
1688
1776
|
return false;
|
|
1689
|
-
const requestedPath =
|
|
1777
|
+
const requestedPath = opencodePathArg(parsed.args) ?? "";
|
|
1690
1778
|
const editPath = typeof display.args.path === "string" ? display.args.path : "";
|
|
1691
1779
|
if (!requestedPath || !editPath)
|
|
1692
1780
|
return false;
|
|
@@ -1784,7 +1872,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1784
1872
|
const rejectMissingReadTarget = (parsed) => {
|
|
1785
1873
|
if (parsed.toolName !== "read")
|
|
1786
1874
|
return false;
|
|
1787
|
-
const requested =
|
|
1875
|
+
const requested = opencodePathArg(parsed.args) ?? "";
|
|
1788
1876
|
if (!requested)
|
|
1789
1877
|
return false;
|
|
1790
1878
|
// A scheme-addressed target is the advertised host executor's to resolve,
|
|
@@ -2084,6 +2172,8 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2084
2172
|
requestContext: session.requestContext,
|
|
2085
2173
|
toolCatalog: session.toolCatalog ?? [],
|
|
2086
2174
|
postCompactionRebase: session.postCompactionRebase,
|
|
2175
|
+
hostAgent: session.hostAgent,
|
|
2176
|
+
systemPromptHash: session.stableSystemPromptHash,
|
|
2087
2177
|
}).catch((error) => {
|
|
2088
2178
|
trace(`conversation persistence: TurnEnded save failed ` +
|
|
2089
2179
|
`sessionKey=${session.openCodeSessionId}: ${String(error)}`);
|
|
@@ -2312,7 +2402,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2312
2402
|
else {
|
|
2313
2403
|
replaySafety.markBarrier("non-control-exec");
|
|
2314
2404
|
const displayCallId = extractExecDisplayCallId(esm);
|
|
2315
|
-
const parsed = parseExecServerMessage(esm);
|
|
2405
|
+
const parsed = parseExecServerMessage(esm, session.hostToolDialect);
|
|
2316
2406
|
if (parsed) {
|
|
2317
2407
|
const executableToolName = resolveCustomWebToolAlias(parsed.toolName, session.toolAliases);
|
|
2318
2408
|
if (executableToolName !== parsed.toolName) {
|
|
@@ -2320,6 +2410,20 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2320
2410
|
parsed.toolName = executableToolName;
|
|
2321
2411
|
}
|
|
2322
2412
|
remapNativeSubagentForCatalog(parsed, advertisedToolNameSet, session.subagentCatalog);
|
|
2413
|
+
// SubagentArgs has no description; Cursor's real title lives on the
|
|
2414
|
+
// correlated display TaskToolCall. Prefer that over the 5-word prompt slice.
|
|
2415
|
+
if (displayCallId && (parsed.toolName === "task" || parsed.toolName === "subagent")) {
|
|
2416
|
+
const stored = session.displayToolCalls.get(displayCallId);
|
|
2417
|
+
const display = parseDisplayToolCall(displayCallId, stored);
|
|
2418
|
+
if (display?.variant === "task_tool_call") {
|
|
2419
|
+
const description = typeof display.args.description === "string" ? display.args.description : undefined;
|
|
2420
|
+
preferCorrelatedTaskDescription(parsed, description);
|
|
2421
|
+
if (description?.trim()) {
|
|
2422
|
+
trace(`exec: preferred TaskToolCall description callId=${displayCallId} ` +
|
|
2423
|
+
`description=${JSON.stringify(description.trim())}`);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2323
2427
|
const editCall = displayCallId
|
|
2324
2428
|
? session.editToolCalls?.get(displayCallId)
|
|
2325
2429
|
: undefined;
|
|
@@ -2605,9 +2709,17 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2605
2709
|
setActiveCursorMode(session.openCodeSessionId, sw.args.targetModeId, {
|
|
2606
2710
|
bridgedPlanEntered: false,
|
|
2607
2711
|
});
|
|
2712
|
+
const hostAgentSwitchQueued = session.openCodeSessionId
|
|
2713
|
+
? queueHostAgentModeSwitch({
|
|
2714
|
+
sessionID: session.openCodeSessionId,
|
|
2715
|
+
targetModeID: sw.args.targetModeId,
|
|
2716
|
+
cursorSessionID: session.sessionId,
|
|
2717
|
+
})
|
|
2718
|
+
: false;
|
|
2608
2719
|
trace(`interaction_query: APPROVED switch_mode id=${handled.id} ` +
|
|
2609
2720
|
`target=${JSON.stringify(sw.args.targetModeId)} (no host plan tool; ` +
|
|
2610
|
-
|
|
2721
|
+
`${hostAgentSwitchQueued ? "native host-agent switch queued" : "provider-owned fallback"}) ` +
|
|
2722
|
+
`cursorToolCallId=${sw.toolCallId || "(none)"}`);
|
|
2611
2723
|
continue;
|
|
2612
2724
|
}
|
|
2613
2725
|
if (handled.outcome === "bridged" && handled.switchMode) {
|
|
@@ -2875,8 +2987,14 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
2875
2987
|
if (names.has(CUSTOM_READ_MCP_RESOURCE_TOOL)) {
|
|
2876
2988
|
instructions.push(`- To read an MCP resource, call \`${CUSTOM_READ_MCP_RESOURCE_TOOL}\`; do not use Cursor's native resource-reading interaction.`);
|
|
2877
2989
|
}
|
|
2878
|
-
if (names.has("
|
|
2879
|
-
const
|
|
2990
|
+
if (names.has("execute")) {
|
|
2991
|
+
const shell = names.has("shell") ? "`shell`" : names.has("bash") ? "`bash`" : undefined;
|
|
2992
|
+
instructions.push(shell
|
|
2993
|
+
? `- OpenCode \`execute\` is Code Mode JavaScript (\`code\`); it is not a shell. For OS commands, call OpenCode ${shell}. Do not pass \`command\` to \`execute\`.`
|
|
2994
|
+
: "- OpenCode `execute` is Code Mode JavaScript (`code`); it is not a shell. Do not pass `command` to `execute`.");
|
|
2995
|
+
}
|
|
2996
|
+
if (names.has("task") || names.has("subagent")) {
|
|
2997
|
+
const target = names.has("task") ? "`task`" : "`subagent`";
|
|
2880
2998
|
const available = subagents.agents.map((agent) => `\`${agent.name}\``).join(", ");
|
|
2881
2999
|
instructions.push(`- Native Cursor Task/subagent requests are executed through OpenCode ${target}. ` +
|
|
2882
3000
|
"Advertised custom subagent names are used exactly; otherwise `unspecified` and `generalPurpose` select host `general`, " +
|
|
@@ -3143,9 +3261,25 @@ export function resolveTurnConversationReset(input) {
|
|
|
3143
3261
|
rememberPostCompactionRebase(sessionKey);
|
|
3144
3262
|
return { reset: true, reason: "compaction" };
|
|
3145
3263
|
}
|
|
3264
|
+
let promptChange;
|
|
3265
|
+
if (sessionKey && input.promptIdentity) {
|
|
3266
|
+
const current = normalizePromptIdentity(input.promptIdentity);
|
|
3267
|
+
const previous = promptIdentityBySession.get(sessionKey);
|
|
3268
|
+
if (previous && promptIdentityChanged(previous, current)) {
|
|
3269
|
+
promptChange = current.hostAgent !== undefined && previous.hostAgent !== current.hostAgent
|
|
3270
|
+
? "agent-change"
|
|
3271
|
+
: "system-prompt-change";
|
|
3272
|
+
}
|
|
3273
|
+
rememberPromptIdentity(sessionKey, {
|
|
3274
|
+
...previous,
|
|
3275
|
+
...current,
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3146
3278
|
if (sessionKey && postCompactionRebaseBySession.delete(sessionKey)) {
|
|
3147
3279
|
return { reset: true, reason: "post-compaction-rebase" };
|
|
3148
3280
|
}
|
|
3281
|
+
if (promptChange)
|
|
3282
|
+
return { reset: true, reason: promptChange };
|
|
3149
3283
|
return { reset: false };
|
|
3150
3284
|
}
|
|
3151
3285
|
export function resetTurnStateForTests() {
|
|
@@ -3156,6 +3290,8 @@ export function resetTurnStateForTests() {
|
|
|
3156
3290
|
toolCatalogWaitersBySession.clear();
|
|
3157
3291
|
toolCatalogBySession.clear();
|
|
3158
3292
|
postCompactionRebaseBySession.clear();
|
|
3293
|
+
promptIdentityBySession.clear();
|
|
3294
|
+
mirroredTodosBySession.clear();
|
|
3159
3295
|
}
|
|
3160
3296
|
function extractUserText(lastUser) {
|
|
3161
3297
|
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;
|