cursor-opencode-provider 0.6.3 → 0.6.6
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 +17 -6
- package/dist/auth.js +28 -11
- package/dist/language-model.d.ts +2 -0
- package/dist/language-model.js +280 -69
- package/dist/model-config.d.ts +3 -1
- package/dist/model-config.js +83 -19
- package/dist/models.d.ts +8 -0
- package/dist/models.js +8 -4
- package/dist/opencode2/catalog.js +4 -3
- package/dist/pricing-data.d.ts +51 -11
- package/dist/pricing-data.js +51 -11
- package/dist/pricing.d.ts +10 -4
- package/dist/pricing.js +17 -8
- package/dist/protocol/exec-variants.d.ts +5 -0
- package/dist/protocol/exec-variants.js +17 -2
- package/dist/protocol/git-diff.d.ts +33 -0
- package/dist/protocol/git-diff.js +372 -0
- package/dist/protocol/messages.js +229 -6
- package/dist/protocol/progress-continuation.d.ts +25 -0
- package/dist/protocol/progress-continuation.js +92 -0
- package/dist/protocol/tools.d.ts +6 -0
- package/dist/protocol/tools.js +216 -3
- package/dist/protocol/workspace-grounding.d.ts +1 -0
- package/dist/protocol/workspace-grounding.js +8 -0
- package/dist/session.d.ts +11 -2
- package/dist/session.js +34 -5
- package/dist/usage.d.ts +14 -0
- package/dist/usage.js +26 -0
- package/package.json +1 -1
package/dist/language-model.js
CHANGED
|
@@ -6,8 +6,11 @@ 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, 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";
|
|
10
|
-
import {
|
|
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";
|
|
10
|
+
import { buildGitDiffExecMessages } from "./protocol/git-diff.js";
|
|
11
|
+
import { cursorExecVariantByRequestField, describeCursorExecVariant } from "./protocol/exec-variants.js";
|
|
12
|
+
import { appendWorkspaceRootGrounding } from "./protocol/workspace-grounding.js";
|
|
13
|
+
import { progressOnlyContinuationPrompt, shouldContinueProgressOnlyTurn, } from "./protocol/progress-continuation.js";
|
|
11
14
|
import { advertisedToolNamesFromDescriptors, extractExecDisplayCallId, extractProtobufSubmessage, listProtobufFieldNumbers, parseDisplayToolCall, resolveBridgedOpenCodeToolCall, } from "./protocol/tool-call-bridge.js";
|
|
12
15
|
import { handleKvServerMessage } from "./protocol/kv.js";
|
|
13
16
|
import { buildAskQuestionInteractionReply, buildAsyncAskQuestionCompletion, buildCreatePlanInteractionReply, buildSwitchModeInteractionReply, handleInteractionQuery, } from "./protocol/interactions.js";
|
|
@@ -26,7 +29,7 @@ import { clearPersistedConversationState, hydrateConversationState, persistConve
|
|
|
26
29
|
import { initializeConversationPersistence } from "./protocol/conversation-persistence.js";
|
|
27
30
|
import { resolveContinuationPolicy, sessionManager, } from "./session.js";
|
|
28
31
|
import { CursorAuthError, CursorLocalCancellationError, CursorProtocolError, CursorProviderError, CursorRetryExhaustedError, CursorServerError, CursorTransportError, isTransientGrpcStatus, retrySuppressedError, toCursorProviderError, } from "./errors.js";
|
|
29
|
-
import { readCache, cacheFilePath, resolveVariantParameters,
|
|
32
|
+
import { readCache, cacheFilePath, resolveVariantParameters, resolveVariantMaxMode, extractCursorVariantParameters, resolveCursorWireModelId } from "./models.js";
|
|
30
33
|
import { getOrBuildRequestContext } from "./context/frozen.js";
|
|
31
34
|
import { workspaceRootFromRequestContext } from "./context/env.js";
|
|
32
35
|
import { ensureOpencodeProjectDir, opencodeGlobalCacheDir, setHostCacheDirOverride, } from "./context/paths.js";
|
|
@@ -39,7 +42,7 @@ import { resolveCursorModelSupportsImages } from "./model-metadata.js";
|
|
|
39
42
|
import { consumeCursorShellResult, registerCursorShellCall, } from "./shell-timeout.js";
|
|
40
43
|
import { analyzeReplayFrame, AttemptReplaySafety } from "./replay-safety.js";
|
|
41
44
|
import { readAllFieldsStrict } from "./protocol/struct.js";
|
|
42
|
-
import { buildLanguageModelV3UsageFromCounters, cursorUsageCountersFromTurnEnded, emptyLanguageModelV3Usage, formatCursorCacheDiagnostics, formatCursorTokenCategories, formatTurnUsageValidation, turnEndedCounter, } from "./usage.js";
|
|
45
|
+
import { buildLanguageModelV3UsageFromCounters, cursorUsageCountersFromTurnEnded, emptyLanguageModelV3Usage, formatCursorCacheDiagnostics, formatCursorTokenCategories, formatTurnUsageValidation, occupancyUsageFromTokenDetails, OPENCODE_DISPLAY_ONLY_COST_METADATA, turnEndedCounter, } from "./usage.js";
|
|
43
46
|
let _availableModels;
|
|
44
47
|
// mtime of the cache file the last time we loaded it. Compared on each call
|
|
45
48
|
// so discoverModels' background refresh is picked up without a process restart.
|
|
@@ -578,6 +581,64 @@ export async function pumpWithRecovery(input) {
|
|
|
578
581
|
}
|
|
579
582
|
}
|
|
580
583
|
}
|
|
584
|
+
const heartbeatWritePendingBySession = new WeakMap();
|
|
585
|
+
const heartbeatGenerationBySession = new WeakMap();
|
|
586
|
+
function bumpHeartbeatGeneration(session) {
|
|
587
|
+
const generation = (heartbeatGenerationBySession.get(session) ?? 0) + 1;
|
|
588
|
+
heartbeatGenerationBySession.set(session, generation);
|
|
589
|
+
heartbeatWritePendingBySession.set(session, false);
|
|
590
|
+
return generation;
|
|
591
|
+
}
|
|
592
|
+
function isCurrentHeartbeatGeneration(session, generation) {
|
|
593
|
+
return heartbeatGenerationBySession.get(session) === generation;
|
|
594
|
+
}
|
|
595
|
+
/** Start (or replace) the per-session heartbeat. In-flight writes from a prior attach are ignored. */
|
|
596
|
+
export function attachSessionHeartbeat(session) {
|
|
597
|
+
session.heartbeatCancel?.();
|
|
598
|
+
if (session.heartbeat) {
|
|
599
|
+
clearInterval(session.heartbeat);
|
|
600
|
+
session.heartbeat = null;
|
|
601
|
+
}
|
|
602
|
+
const generation = bumpHeartbeatGeneration(session);
|
|
603
|
+
const interval = setInterval(() => {
|
|
604
|
+
if (session.closed || !isCurrentHeartbeatGeneration(session, generation))
|
|
605
|
+
return;
|
|
606
|
+
if (heartbeatWritePendingBySession.get(session)) {
|
|
607
|
+
trace(`heartbeat skipped: prior heartbeat write is still pending sessionId=${session.sessionId}`);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
heartbeatWritePendingBySession.set(session, true);
|
|
611
|
+
const stream = session.stream;
|
|
612
|
+
void writeWithBackpressure(stream, buildHeartbeat(), "heartbeat")
|
|
613
|
+
.then(() => {
|
|
614
|
+
if (!isCurrentHeartbeatGeneration(session, generation) || session.closed)
|
|
615
|
+
return;
|
|
616
|
+
sessionManager.recordHeartbeatWrite(session);
|
|
617
|
+
})
|
|
618
|
+
.catch((cause) => {
|
|
619
|
+
if (!isCurrentHeartbeatGeneration(session, generation) || session.closed)
|
|
620
|
+
return;
|
|
621
|
+
sessionManager.close(session, "heartbeat-write-failed", toCursorProviderError(cause, {
|
|
622
|
+
replaySafe: false,
|
|
623
|
+
fallback: "Cursor heartbeat write failed",
|
|
624
|
+
}));
|
|
625
|
+
})
|
|
626
|
+
.finally(() => {
|
|
627
|
+
if (isCurrentHeartbeatGeneration(session, generation)) {
|
|
628
|
+
heartbeatWritePendingBySession.set(session, false);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
}, session.policy.heartbeatMs);
|
|
632
|
+
interval.unref?.();
|
|
633
|
+
session.heartbeat = interval;
|
|
634
|
+
session.heartbeatCancel = () => {
|
|
635
|
+
if (session.heartbeat)
|
|
636
|
+
clearInterval(session.heartbeat);
|
|
637
|
+
session.heartbeat = null;
|
|
638
|
+
if (isCurrentHeartbeatGeneration(session, generation))
|
|
639
|
+
bumpHeartbeatGeneration(session);
|
|
640
|
+
};
|
|
641
|
+
}
|
|
581
642
|
async function startSession(modelId, token, callOptions, options, startOptions) {
|
|
582
643
|
const continuationPolicy = resolveContinuationPolicy(options.continuation);
|
|
583
644
|
const prompt = callOptions.prompt;
|
|
@@ -760,9 +821,12 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
760
821
|
maxMode: hintMaxMode,
|
|
761
822
|
picked,
|
|
762
823
|
});
|
|
763
|
-
//
|
|
764
|
-
// paramMap does not include
|
|
765
|
-
const maxMode =
|
|
824
|
+
// An explicit pick owns the context tier; otherwise honor the maxMode hint.
|
|
825
|
+
// OpenCode's variant paramMap does not include maxMode when the user selects 1m.
|
|
826
|
+
const maxMode = resolveVariantMaxMode(parameterValues, {
|
|
827
|
+
picked,
|
|
828
|
+
maxMode: hintMaxMode,
|
|
829
|
+
});
|
|
766
830
|
// Do NOT pass callOptions.abortSignal into the h2 Run stream. OpenCode aborts
|
|
767
831
|
// that signal when a turn ends with tool-calls; the Cursor stream must stay
|
|
768
832
|
// open until we write the exec results on the next doStream.
|
|
@@ -938,32 +1002,61 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
938
1002
|
closed: false,
|
|
939
1003
|
};
|
|
940
1004
|
sessionManager.registerSession(session);
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
if (session.closed)
|
|
944
|
-
return;
|
|
945
|
-
if (heartbeatWritePending) {
|
|
946
|
-
// The pending write has its own bounded drain wait and will close the
|
|
947
|
-
// session with the actual failing operation. Do not race it with another
|
|
948
|
-
// heartbeat or misattribute an existing KV backlog to the heartbeat.
|
|
949
|
-
trace(`heartbeat skipped: prior heartbeat write is still pending sessionId=${session.sessionId}`);
|
|
1005
|
+
attachSessionHeartbeat(session);
|
|
1006
|
+
const abortIfNeeded = (stream) => {
|
|
1007
|
+
if (!session.closed && !callOptions.abortSignal?.aborted)
|
|
950
1008
|
return;
|
|
1009
|
+
try {
|
|
1010
|
+
stream?.destroy();
|
|
951
1011
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
})
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1012
|
+
catch { /* already closed */ }
|
|
1013
|
+
if (callOptions.abortSignal?.aborted) {
|
|
1014
|
+
throw new CursorLocalCancellationError("Cursor progress-only continuation cancelled");
|
|
1015
|
+
}
|
|
1016
|
+
throw new CursorProtocolError("Cannot reopen a closed Cursor session");
|
|
1017
|
+
};
|
|
1018
|
+
session.reopenWithUserMessage = async (text) => {
|
|
1019
|
+
abortIfNeeded();
|
|
1020
|
+
const { resolveBearerToken } = await import("./auth.js");
|
|
1021
|
+
const freshToken = await resolveBearerToken({
|
|
1022
|
+
accessToken: options.accessToken,
|
|
1023
|
+
apiKey: options.apiKey,
|
|
1024
|
+
baseUrl: resolveApiBaseURL(options),
|
|
1025
|
+
});
|
|
1026
|
+
abortIfNeeded();
|
|
1027
|
+
// Do not pass OpenCode's abortSignal into the h2 stream: a tool-calls abort
|
|
1028
|
+
// must not tear down a Run we still need to pump. Check abort around open.
|
|
1029
|
+
const next = await bidiRunStream(freshToken, { baseURL: agentBaseUrl, headers: options.headers });
|
|
1030
|
+
abortIfNeeded(next);
|
|
1031
|
+
const conversationState = session.resumeCheckpoint ?? getCheckpoint(session.conversationId);
|
|
1032
|
+
const reqBytes = buildRunRequest({
|
|
1033
|
+
text,
|
|
1034
|
+
modelId: cursorModelId,
|
|
1035
|
+
conversationId: session.conversationId,
|
|
1036
|
+
conversationGroupId: session.cacheDiagnostics?.conversationGroupId ?? session.conversationId,
|
|
1037
|
+
conversationState,
|
|
1038
|
+
parameterValues,
|
|
1039
|
+
maxMode,
|
|
1040
|
+
toolDescriptors: session.toolDescriptors,
|
|
1041
|
+
requestContext: session.requestContext,
|
|
1042
|
+
action: "user",
|
|
1043
|
+
});
|
|
1044
|
+
try {
|
|
1045
|
+
await writeWithBackpressure(next, reqBytes, "progress-only continuation Run");
|
|
1046
|
+
}
|
|
1047
|
+
catch (error) {
|
|
1048
|
+
try {
|
|
1049
|
+
next.destroy();
|
|
1050
|
+
}
|
|
1051
|
+
catch { }
|
|
1052
|
+
throw error;
|
|
1053
|
+
}
|
|
1054
|
+
abortIfNeeded(next);
|
|
1055
|
+
session.heartbeatCancel?.();
|
|
1056
|
+
await waitForStreamWrites(session.stream);
|
|
1057
|
+
abortIfNeeded(next);
|
|
1058
|
+
sessionManager.replaceStream(session, next);
|
|
1059
|
+
attachSessionHeartbeat(session);
|
|
967
1060
|
};
|
|
968
1061
|
callOptions.abortSignal?.addEventListener("abort", () => {
|
|
969
1062
|
// Abort after tool-calls is normal — preserve pending sessions.
|
|
@@ -1169,6 +1262,7 @@ export function deliverContinuationResults(session, trailingToolResults) {
|
|
|
1169
1262
|
else if (!pending.bridged) {
|
|
1170
1263
|
try {
|
|
1171
1264
|
const shellResult = pending.resultField === "shell_stream"
|
|
1265
|
+
|| pending.resultField === "shell_result"
|
|
1172
1266
|
|| pending.resultField === "background_shell_spawn_result"
|
|
1173
1267
|
? consumeCursorShellResult(r.toolCallId, r.output)
|
|
1174
1268
|
: undefined;
|
|
@@ -1326,6 +1420,11 @@ function isTruthyEnv(value) {
|
|
|
1326
1420
|
return value === "1" || value === "true";
|
|
1327
1421
|
}
|
|
1328
1422
|
const streamWriteChains = new WeakMap();
|
|
1423
|
+
async function waitForStreamWrites(stream) {
|
|
1424
|
+
const pending = streamWriteChains.get(stream);
|
|
1425
|
+
if (pending)
|
|
1426
|
+
await pending.catch(() => undefined);
|
|
1427
|
+
}
|
|
1329
1428
|
/**
|
|
1330
1429
|
* Keep awaited protocol writes ordered per Run stream. In particular, a large
|
|
1331
1430
|
* KV get_blob reply must drain before another KV reply or heartbeat is queued;
|
|
@@ -1436,6 +1535,9 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1436
1535
|
const advertisedToolNameSet = new Set(advertisedToolNames.map((name) => resolveCustomWebToolAlias(name, session.toolAliases)));
|
|
1437
1536
|
let textStarted = false;
|
|
1438
1537
|
let reasoningStarted = false;
|
|
1538
|
+
let assistantText = "";
|
|
1539
|
+
let progressContinuationAttempts = 0;
|
|
1540
|
+
let emittedHostTools = 0;
|
|
1439
1541
|
const replaySafety = new AttemptReplaySafety(session.sessionId);
|
|
1440
1542
|
const failRunProtocol = (message, code) => {
|
|
1441
1543
|
replaySafety.markBarrier("unknown-or-malformed-frame");
|
|
@@ -1447,6 +1549,22 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1447
1549
|
if (error instanceof CursorProviderError && error.origin !== "protocol")
|
|
1448
1550
|
throw error;
|
|
1449
1551
|
};
|
|
1552
|
+
const writeExecFrames = async (frames, operation) => {
|
|
1553
|
+
try {
|
|
1554
|
+
for (const frame of frames) {
|
|
1555
|
+
await writeWithBackpressure(session.stream, frame, operation);
|
|
1556
|
+
}
|
|
1557
|
+
return true;
|
|
1558
|
+
}
|
|
1559
|
+
catch (error) {
|
|
1560
|
+
rethrowTransportWriteFailure(error);
|
|
1561
|
+
const wrapped = new Error(`Failed to ${operation}: ${error.message}`);
|
|
1562
|
+
trace(`exec: reply FAILED ${wrapped.message}`);
|
|
1563
|
+
safeError(wrapped);
|
|
1564
|
+
sessionManager.close(session);
|
|
1565
|
+
return false;
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1450
1568
|
// OpenCode cancels the ReadableStream between turns (see the cancel handler
|
|
1451
1569
|
// in doStreamImpl). The frames iterator can still yield a final `done` after
|
|
1452
1570
|
// the cancel lands — controller.enqueue on a cancelled controller throws.
|
|
@@ -1477,29 +1595,20 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1477
1595
|
streamClosed = true;
|
|
1478
1596
|
};
|
|
1479
1597
|
/** Reply on Cursor's correlated exec channel without exposing a host tool call. */
|
|
1480
|
-
const rejectExec = (parsed, reason, label) => {
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
}
|
|
1598
|
+
const rejectExec = async (parsed, reason, label) => {
|
|
1599
|
+
const groundedReason = appendWorkspaceRootGrounding(reason, workspaceRootFromRequestContext(session.requestContext));
|
|
1600
|
+
const ok = await writeExecFrames(buildExecClientMessages({
|
|
1601
|
+
execId: parsed.id,
|
|
1602
|
+
resultField: parsed.resultField,
|
|
1603
|
+
output: "",
|
|
1604
|
+
error: groundedReason,
|
|
1605
|
+
toolName: parsed.toolName,
|
|
1606
|
+
resultMetadata: parsed.resultMetadata,
|
|
1607
|
+
workspaceRoot: workspaceRootFromRequestContext(session.requestContext),
|
|
1608
|
+
}), `reject ${label} id=${parsed.id}`);
|
|
1609
|
+
if (ok)
|
|
1493
1610
|
trace(`exec: REFUSED ${label} toolName=${parsed.toolName} id=${parsed.id}`);
|
|
1494
|
-
|
|
1495
|
-
}
|
|
1496
|
-
catch (e) {
|
|
1497
|
-
const error = new Error(`Failed to reject Cursor tool request (${label}): ${e.message}`);
|
|
1498
|
-
trace(`exec: REFUSED reply FAILED ${error.message}`);
|
|
1499
|
-
safeError(error);
|
|
1500
|
-
sessionManager.close(session);
|
|
1501
|
-
return false;
|
|
1502
|
-
}
|
|
1611
|
+
return ok;
|
|
1503
1612
|
};
|
|
1504
1613
|
/**
|
|
1505
1614
|
* Cursor's streamed edit handshake needs the complete target before it sends
|
|
@@ -1665,6 +1774,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1665
1774
|
const emitText = (text) => {
|
|
1666
1775
|
if (!text)
|
|
1667
1776
|
return;
|
|
1777
|
+
assistantText += text;
|
|
1668
1778
|
replaySafety.markBarrier("visible-text");
|
|
1669
1779
|
// Close reasoning before text (hosts expect reasoning-end before text-start).
|
|
1670
1780
|
if (reasoningStarted && !textStarted) {
|
|
@@ -1692,13 +1802,13 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1692
1802
|
const emitFinish = (te, reason, settledUsage, settledSource) => {
|
|
1693
1803
|
closeOpenSpans();
|
|
1694
1804
|
const est = session.usageEstimate;
|
|
1695
|
-
// OpenCode
|
|
1696
|
-
//
|
|
1697
|
-
//
|
|
1698
|
-
//
|
|
1699
|
-
//
|
|
1700
|
-
|
|
1701
|
-
const
|
|
1805
|
+
// OpenCode TUI/GUI replace each assistant message's tokens (they do not
|
|
1806
|
+
// sum occupancy) and the TUI footer requires tokens.output > 0. Cost is
|
|
1807
|
+
// added per step-finish. Emit checkpoint occupancy snapshots at tool-call
|
|
1808
|
+
// boundaries with a $0 Copilot cost override, then the billed TurnEnded
|
|
1809
|
+
// snapshot once at stop. Char/4 usageEstimate stays traces-only.
|
|
1810
|
+
const tokenDetails = session.tokenDetails;
|
|
1811
|
+
const occupancyDetails = !te && tokenDetails && tokenDetails.usedTokens > 0 ? tokenDetails : undefined;
|
|
1702
1812
|
const contextSource = tokenDetails
|
|
1703
1813
|
? session.tokenDetailsFresh
|
|
1704
1814
|
? "checkpoint-current-run"
|
|
@@ -1712,15 +1822,29 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1712
1822
|
priorContextTokens: session.cacheDiagnostics?.priorTokenDetails?.usedTokens,
|
|
1713
1823
|
})
|
|
1714
1824
|
: emptyLanguageModelV3Usage()
|
|
1715
|
-
:
|
|
1825
|
+
: occupancyDetails
|
|
1826
|
+
? occupancyUsageFromTokenDetails(occupancyDetails, session.cacheDiagnostics?.priorTokenDetails)
|
|
1827
|
+
: emptyLanguageModelV3Usage());
|
|
1716
1828
|
const providerMetadata = te
|
|
1717
1829
|
? cursorTurnEndedProviderMetadata(te, tokenDetails, contextSource)
|
|
1718
|
-
:
|
|
1830
|
+
: occupancyDetails && contextSource
|
|
1831
|
+
? {
|
|
1832
|
+
...OPENCODE_DISPLAY_ONLY_COST_METADATA,
|
|
1833
|
+
cursor: {
|
|
1834
|
+
usageVersion: 3,
|
|
1835
|
+
occupancyOnly: true,
|
|
1836
|
+
context: cursorContextUsageMetadata(occupancyDetails, contextSource),
|
|
1837
|
+
},
|
|
1838
|
+
}
|
|
1839
|
+
: undefined;
|
|
1719
1840
|
const reasonLabel = typeof reason === "object" && reason && "unified" in reason
|
|
1720
1841
|
? String(reason.unified ?? "unknown")
|
|
1721
1842
|
: String(reason);
|
|
1722
1843
|
const inTotal = usage.inputTokens?.total ?? 0;
|
|
1723
1844
|
const outTotal = usage.outputTokens?.total ?? 0;
|
|
1845
|
+
const occupancySource = occupancyDetails
|
|
1846
|
+
? `occupancy-${contextSource ?? "unavailable"}`
|
|
1847
|
+
: "intermediate-zero";
|
|
1724
1848
|
trace(`finish: reason=${reasonLabel} ` +
|
|
1725
1849
|
`v3In=${inTotal} v3Out=${outTotal} ` +
|
|
1726
1850
|
`v3CacheRead=${usage.inputTokens?.cacheRead ?? 0} v3CacheWrite=${usage.inputTokens?.cacheWrite ?? 0} ` +
|
|
@@ -1731,11 +1855,20 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1731
1855
|
`rawCacheWrite=${te ? turnEndedCounter(te, "cache_write") : est.cacheWrite} ` +
|
|
1732
1856
|
`source=${te
|
|
1733
1857
|
? settledSource ?? (contextSource ?? "unavailable")
|
|
1734
|
-
:
|
|
1858
|
+
: occupancySource}`);
|
|
1735
1859
|
if (counters) {
|
|
1736
1860
|
trace(formatTurnUsageValidation(counters, usage, tokenDetails, contextSource));
|
|
1737
1861
|
trace(formatCursorCacheDiagnostics(counters, tokenDetails, cacheDiagnostics.priorTokenDetails, cacheDiagnostics));
|
|
1738
1862
|
}
|
|
1863
|
+
else if (occupancyDetails) {
|
|
1864
|
+
trace(formatTurnUsageValidation({
|
|
1865
|
+
inputTokens: occupancyDetails.usedTokens,
|
|
1866
|
+
outputTokens: 1,
|
|
1867
|
+
cacheRead: session.cacheDiagnostics?.priorTokenDetails?.usedTokens ?? 0,
|
|
1868
|
+
cacheWrite: 0,
|
|
1869
|
+
reasoningTokens: 0,
|
|
1870
|
+
}, usage, occupancyDetails, contextSource));
|
|
1871
|
+
}
|
|
1739
1872
|
safeEnqueue({
|
|
1740
1873
|
type: "finish",
|
|
1741
1874
|
usage,
|
|
@@ -1903,6 +2036,31 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1903
2036
|
`sessionKey=${session.openCodeSessionId}: ${String(error)}`);
|
|
1904
2037
|
});
|
|
1905
2038
|
}
|
|
2039
|
+
const checkpoint = session.resumeCheckpoint ?? getCheckpoint(session.conversationId);
|
|
2040
|
+
if (typeof session.reopenWithUserMessage === "function"
|
|
2041
|
+
&& checkpoint
|
|
2042
|
+
&& checkpoint.length > 0
|
|
2043
|
+
&& shouldContinueProgressOnlyTurn({
|
|
2044
|
+
allowTools: session.allowTools,
|
|
2045
|
+
advertisedToolCount: advertisedToolNames.length,
|
|
2046
|
+
assistantText,
|
|
2047
|
+
emittedHostTools,
|
|
2048
|
+
continuationAttempts: progressContinuationAttempts,
|
|
2049
|
+
pendingExecs: session.pending.size,
|
|
2050
|
+
})) {
|
|
2051
|
+
progressContinuationAttempts += 1;
|
|
2052
|
+
trace("progress-only: continuing attempt=1");
|
|
2053
|
+
try {
|
|
2054
|
+
await session.reopenWithUserMessage(progressOnlyContinuationPrompt(workspaceRootFromRequestContext(session.requestContext)));
|
|
2055
|
+
assistantText = "";
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
catch (error) {
|
|
2059
|
+
// Cursor already completed this turn. A failed nudge must not discard
|
|
2060
|
+
// the valid turn_ended the user already received as assistant text.
|
|
2061
|
+
trace(`progress-only: continuation failed, finishing original turn: ${error.message}`);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
1906
2064
|
emitFinish(turnEnded, { unified: "stop", raw: undefined });
|
|
1907
2065
|
sessionManager.close(session);
|
|
1908
2066
|
return;
|
|
@@ -1982,6 +2140,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1982
2140
|
const input = JSON.stringify(bridged.args ?? {});
|
|
1983
2141
|
trace(`display BRIDGED tool-call toolCallId=${toolCallId} toolName=${bridged.toolName} ` +
|
|
1984
2142
|
`variant=${bridged.variant} callId=${callId} inputLen=${input.length}`);
|
|
2143
|
+
emittedHostTools++;
|
|
1985
2144
|
closeOpenSpans();
|
|
1986
2145
|
safeEnqueue({
|
|
1987
2146
|
type: "tool-call",
|
|
@@ -2072,6 +2231,24 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2072
2231
|
failRunProtocol("Cursor read_mcp_resource reply failed", RUN_REPLY_FAILED);
|
|
2073
2232
|
}
|
|
2074
2233
|
}
|
|
2234
|
+
else if (esm.git_diff_request) {
|
|
2235
|
+
const request = (esm.git_diff_request ?? {});
|
|
2236
|
+
try {
|
|
2237
|
+
const frames = await buildGitDiffExecMessages({
|
|
2238
|
+
execId: esmId,
|
|
2239
|
+
request,
|
|
2240
|
+
workspaceRoot: workspaceRootFromRequestContext(session.requestContext),
|
|
2241
|
+
});
|
|
2242
|
+
for (const frame of frames) {
|
|
2243
|
+
await writeWithBackpressure(session.stream, frame, `git_diff reply id=${esmId}`);
|
|
2244
|
+
}
|
|
2245
|
+
trace(`exec git_diff: replied id=${esmId}`);
|
|
2246
|
+
}
|
|
2247
|
+
catch (error) {
|
|
2248
|
+
rethrowTransportWriteFailure(error);
|
|
2249
|
+
failRunProtocol("Cursor git_diff reply failed", RUN_REPLY_FAILED);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2075
2252
|
else {
|
|
2076
2253
|
replaySafety.markBarrier("non-control-exec");
|
|
2077
2254
|
const displayCallId = extractExecDisplayCallId(esm);
|
|
@@ -2109,7 +2286,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2109
2286
|
trace(`exec: id=${parsed?.id} variant=${parsed ? Object.keys(parsed).join(",") : "none"} toolName=${parsed?.toolName} resultField=${parsed?.resultField}`);
|
|
2110
2287
|
if (parsed) {
|
|
2111
2288
|
if (parsed.localError) {
|
|
2112
|
-
if (!rejectExec(parsed, parsed.localError, "invalid mapping"))
|
|
2289
|
+
if (!await rejectExec(parsed, parsed.localError, "invalid mapping"))
|
|
2113
2290
|
return;
|
|
2114
2291
|
continue;
|
|
2115
2292
|
}
|
|
@@ -2119,7 +2296,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2119
2296
|
// pumping for text / turn_ended instead of emitting tool-call.
|
|
2120
2297
|
if (!session.allowTools) {
|
|
2121
2298
|
const reason = "Tool calls are not available during this turn (summary/compaction).";
|
|
2122
|
-
if (!rejectExec(parsed, reason, "allowTools=false"))
|
|
2299
|
+
if (!await rejectExec(parsed, reason, "allowTools=false"))
|
|
2123
2300
|
return;
|
|
2124
2301
|
continue;
|
|
2125
2302
|
}
|
|
@@ -2133,7 +2310,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2133
2310
|
if (!advertisedToolNameSet.has(CURSOR_IMAGE_SAVE_TOOL)) {
|
|
2134
2311
|
const reason = "This OpenCode agent cannot write binary file content. "
|
|
2135
2312
|
+ "Do not retry this write with the same bytes.";
|
|
2136
|
-
if (!rejectExec(parsed, reason, "binary write unsupported"))
|
|
2313
|
+
if (!await rejectExec(parsed, reason, "binary write unsupported"))
|
|
2137
2314
|
return;
|
|
2138
2315
|
continue;
|
|
2139
2316
|
}
|
|
@@ -2154,7 +2331,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2154
2331
|
});
|
|
2155
2332
|
}
|
|
2156
2333
|
catch (error) {
|
|
2157
|
-
if (!rejectExec(parsed, error.message, "binary write too large"))
|
|
2334
|
+
if (!await rejectExec(parsed, error.message, "binary write too large"))
|
|
2158
2335
|
return;
|
|
2159
2336
|
continue;
|
|
2160
2337
|
}
|
|
@@ -2170,6 +2347,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2170
2347
|
trace(`exec: binary write STAGED toolCallId=${toolCallId} ` +
|
|
2171
2348
|
`requested=${JSON.stringify(binaryWrite.path)} target=${JSON.stringify(target)} ` +
|
|
2172
2349
|
`bytes=${binaryWrite.data.length}`);
|
|
2350
|
+
emittedHostTools++;
|
|
2173
2351
|
closeOpenSpans();
|
|
2174
2352
|
safeEnqueue({
|
|
2175
2353
|
type: "tool-call",
|
|
@@ -2195,7 +2373,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2195
2373
|
`'${parsed.toolName}'.`;
|
|
2196
2374
|
trace(`exec: unavailable catalog target toolName=${parsed.toolName} ` +
|
|
2197
2375
|
`advertised=[${advertisedToolNames.join(",")}]`);
|
|
2198
|
-
if (!rejectExec(parsed, reason, "unavailable tool"))
|
|
2376
|
+
if (!await rejectExec(parsed, reason, "unavailable tool"))
|
|
2199
2377
|
return;
|
|
2200
2378
|
continue;
|
|
2201
2379
|
}
|
|
@@ -2212,6 +2390,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2212
2390
|
}
|
|
2213
2391
|
const tc = buildToolCallPart(parsed, session.sessionId);
|
|
2214
2392
|
if (parsed.resultField === "shell_stream"
|
|
2393
|
+
|| parsed.resultField === "shell_result"
|
|
2215
2394
|
|| parsed.resultField === "background_shell_spawn_result") {
|
|
2216
2395
|
registerCursorShellCall(tc.toolCallId, parsed.resultMetadata);
|
|
2217
2396
|
}
|
|
@@ -2219,6 +2398,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2219
2398
|
sessionManager.registerPending(parsed.id, session, parsed.resultField, parsed.toolName, false, parsed.resultMetadata);
|
|
2220
2399
|
// tc.input is already a JSON string (LanguageModelV3ToolCall.input).
|
|
2221
2400
|
trace(`exec: EMITTED tool-call toolCallId=${tc.toolCallId} toolName=${tc.toolName} inputLen=${tc.input.length}`);
|
|
2401
|
+
emittedHostTools++;
|
|
2222
2402
|
// Close open text/reasoning spans before tool-call (required by AI SDK V3).
|
|
2223
2403
|
closeOpenSpans();
|
|
2224
2404
|
safeEnqueue({
|
|
@@ -2230,11 +2410,36 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2230
2410
|
emitFinish(undefined, { unified: "tool-calls", raw: undefined });
|
|
2231
2411
|
return;
|
|
2232
2412
|
}
|
|
2413
|
+
// Known Cursor-native exec variants with no safe OpenCode bridge are
|
|
2414
|
+
// soft-denied with a populated typed result or throw, so the turn
|
|
2415
|
+
// continues with remaining tools. Unknown fields still hard-fail.
|
|
2416
|
+
const variantField = detectExecVariantField(payload);
|
|
2417
|
+
const variant = variantField !== undefined
|
|
2418
|
+
? cursorExecVariantByRequestField(variantField)
|
|
2419
|
+
: undefined;
|
|
2420
|
+
if (variant?.handling === "unsupported") {
|
|
2421
|
+
const advertised = advertisedToolNames.length > 0 ? advertisedToolNames.join(", ") : "none";
|
|
2422
|
+
const rawReason = `Cursor-native '${variant.requestName}' is not available on this host. Continue with listed tools: ${advertised}; do not retry '${variant.requestName}'.`;
|
|
2423
|
+
const grounded = appendWorkspaceRootGrounding(rawReason, workspaceRootFromRequestContext(session.requestContext));
|
|
2424
|
+
// Allowlist/status denies have no string channel; buildUnsupportedExecDeny
|
|
2425
|
+
// ignores `reason` for those shapes and still selects the typed oneof.
|
|
2426
|
+
const frames = buildUnsupportedExecDeny({ execId: esmId, variant, reason: grounded });
|
|
2427
|
+
try {
|
|
2428
|
+
for (const frame of frames) {
|
|
2429
|
+
await writeWithBackpressure(session.stream, frame, `unsupported deny ${variant.requestName} id=${esmId}`);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
catch (error) {
|
|
2433
|
+
rethrowTransportWriteFailure(error);
|
|
2434
|
+
failRunProtocol("Cursor unsupported exec deny reply failed", RUN_REPLY_FAILED);
|
|
2435
|
+
}
|
|
2436
|
+
trace(`exec: SOFT-DENIED ${variant.requestName} id=${esmId}`);
|
|
2437
|
+
continue;
|
|
2438
|
+
}
|
|
2233
2439
|
// Never guess a response type for an unknown exec variant. Request and
|
|
2234
2440
|
// result field numbers are not universally identical; a structurally
|
|
2235
2441
|
// wrong reply recreates the heartbeat-only deadlock. Fail promptly so
|
|
2236
2442
|
// schema drift is actionable.
|
|
2237
|
-
const variantField = detectExecVariantField(payload);
|
|
2238
2443
|
const variantDescription = describeCursorExecVariant(variantField);
|
|
2239
2444
|
const hex = Array.from(payload.subarray(0, 48))
|
|
2240
2445
|
.map((x) => x.toString(16).padStart(2, "0"))
|
|
@@ -2310,6 +2515,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2310
2515
|
trace(`interaction_query: BRIDGED ask_question id=${handled.id} toolCallId=${toolCallId} ` +
|
|
2311
2516
|
`questions=${ask.args.questions.length} runAsync=${ask.args.runAsync} ` +
|
|
2312
2517
|
`cursorToolCallId=${ask.toolCallId || "(none)"}`);
|
|
2518
|
+
emittedHostTools++;
|
|
2313
2519
|
closeOpenSpans();
|
|
2314
2520
|
safeEnqueue({
|
|
2315
2521
|
type: "tool-call",
|
|
@@ -2356,6 +2562,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2356
2562
|
`bridge=${bridgeKind} hostTool=${toolName} ` +
|
|
2357
2563
|
`target=${JSON.stringify(sw.args.targetModeId)} ` +
|
|
2358
2564
|
`cursorToolCallId=${sw.toolCallId || "(none)"}`);
|
|
2565
|
+
emittedHostTools++;
|
|
2359
2566
|
closeOpenSpans();
|
|
2360
2567
|
safeEnqueue({
|
|
2361
2568
|
type: "tool-call",
|
|
@@ -2397,6 +2604,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2397
2604
|
// before asking — approving a plan you cannot read is not approval.
|
|
2398
2605
|
if (plan.planReview)
|
|
2399
2606
|
emitText(plan.planReview);
|
|
2607
|
+
emittedHostTools++;
|
|
2400
2608
|
closeOpenSpans();
|
|
2401
2609
|
safeEnqueue({
|
|
2402
2610
|
type: "tool-call",
|
|
@@ -2627,6 +2835,9 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
2627
2835
|
: []),
|
|
2628
2836
|
...instructions,
|
|
2629
2837
|
"Emit the actual tool call and wait for its result; never merely claim or summarize that a tool was used.",
|
|
2838
|
+
"A progress update such as \"Checking…\", \"Inspecting…\", or \"Let me look…\" is not a final answer.",
|
|
2839
|
+
"After progress narration, call a listed tool in the same turn; if no tool is needed, provide the complete user-facing answer before finishing.",
|
|
2840
|
+
"Never end a turn with progress narration alone.",
|
|
2630
2841
|
].join("\n");
|
|
2631
2842
|
}
|
|
2632
2843
|
/** Rough char→token estimate for mid-turn usage before TurnEnded arrives. */
|
package/dist/model-config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ModelInfo } from "./models.js";
|
|
1
|
+
import { type ModelInfo, type ModelVariant } from "./models.js";
|
|
2
2
|
/**
|
|
3
3
|
* Display names shared by both a thinking and a non-thinking model. A thinking
|
|
4
4
|
* model with such a name needs a "Thinking" tag to disambiguate it from its
|
|
@@ -11,5 +11,7 @@ export declare function thinkingSuffixBaseNames(models: ModelInfo[]): Set<string
|
|
|
11
11
|
export declare function modelInfoToConfig(mi: ModelInfo, options?: {
|
|
12
12
|
thinkingSuffix?: boolean;
|
|
13
13
|
contextTier?: "base" | "long";
|
|
14
|
+
variants?: ModelVariant[];
|
|
15
|
+
fast?: boolean;
|
|
14
16
|
}): Record<string, any>;
|
|
15
17
|
export declare function modelsToConfig(models: ModelInfo[]): Record<string, any>;
|