cursor-opencode-provider 0.6.4 → 0.6.7
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 +273 -65
- package/dist/model-config.d.ts +3 -1
- package/dist/model-config.js +83 -19
- package/dist/opencode2/catalog.js +4 -3
- package/dist/pricing-data.d.ts +81 -0
- package/dist/pricing-data.js +85 -4
- 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";
|
|
@@ -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;
|
|
@@ -941,32 +1002,61 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
941
1002
|
closed: false,
|
|
942
1003
|
};
|
|
943
1004
|
sessionManager.registerSession(session);
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
if (session.closed)
|
|
947
|
-
return;
|
|
948
|
-
if (heartbeatWritePending) {
|
|
949
|
-
// The pending write has its own bounded drain wait and will close the
|
|
950
|
-
// session with the actual failing operation. Do not race it with another
|
|
951
|
-
// heartbeat or misattribute an existing KV backlog to the heartbeat.
|
|
952
|
-
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)
|
|
953
1008
|
return;
|
|
1009
|
+
try {
|
|
1010
|
+
stream?.destroy();
|
|
954
1011
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
})
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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);
|
|
970
1060
|
};
|
|
971
1061
|
callOptions.abortSignal?.addEventListener("abort", () => {
|
|
972
1062
|
// Abort after tool-calls is normal — preserve pending sessions.
|
|
@@ -1172,6 +1262,7 @@ export function deliverContinuationResults(session, trailingToolResults) {
|
|
|
1172
1262
|
else if (!pending.bridged) {
|
|
1173
1263
|
try {
|
|
1174
1264
|
const shellResult = pending.resultField === "shell_stream"
|
|
1265
|
+
|| pending.resultField === "shell_result"
|
|
1175
1266
|
|| pending.resultField === "background_shell_spawn_result"
|
|
1176
1267
|
? consumeCursorShellResult(r.toolCallId, r.output)
|
|
1177
1268
|
: undefined;
|
|
@@ -1329,6 +1420,11 @@ function isTruthyEnv(value) {
|
|
|
1329
1420
|
return value === "1" || value === "true";
|
|
1330
1421
|
}
|
|
1331
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
|
+
}
|
|
1332
1428
|
/**
|
|
1333
1429
|
* Keep awaited protocol writes ordered per Run stream. In particular, a large
|
|
1334
1430
|
* KV get_blob reply must drain before another KV reply or heartbeat is queued;
|
|
@@ -1439,6 +1535,9 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1439
1535
|
const advertisedToolNameSet = new Set(advertisedToolNames.map((name) => resolveCustomWebToolAlias(name, session.toolAliases)));
|
|
1440
1536
|
let textStarted = false;
|
|
1441
1537
|
let reasoningStarted = false;
|
|
1538
|
+
let assistantText = "";
|
|
1539
|
+
let progressContinuationAttempts = 0;
|
|
1540
|
+
let emittedHostTools = 0;
|
|
1442
1541
|
const replaySafety = new AttemptReplaySafety(session.sessionId);
|
|
1443
1542
|
const failRunProtocol = (message, code) => {
|
|
1444
1543
|
replaySafety.markBarrier("unknown-or-malformed-frame");
|
|
@@ -1450,6 +1549,22 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1450
1549
|
if (error instanceof CursorProviderError && error.origin !== "protocol")
|
|
1451
1550
|
throw error;
|
|
1452
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
|
+
};
|
|
1453
1568
|
// OpenCode cancels the ReadableStream between turns (see the cancel handler
|
|
1454
1569
|
// in doStreamImpl). The frames iterator can still yield a final `done` after
|
|
1455
1570
|
// the cancel lands — controller.enqueue on a cancelled controller throws.
|
|
@@ -1480,29 +1595,20 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1480
1595
|
streamClosed = true;
|
|
1481
1596
|
};
|
|
1482
1597
|
/** Reply on Cursor's correlated exec channel without exposing a host tool call. */
|
|
1483
|
-
const rejectExec = (parsed, reason, label) => {
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
}
|
|
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)
|
|
1496
1610
|
trace(`exec: REFUSED ${label} toolName=${parsed.toolName} id=${parsed.id}`);
|
|
1497
|
-
|
|
1498
|
-
}
|
|
1499
|
-
catch (e) {
|
|
1500
|
-
const error = new Error(`Failed to reject Cursor tool request (${label}): ${e.message}`);
|
|
1501
|
-
trace(`exec: REFUSED reply FAILED ${error.message}`);
|
|
1502
|
-
safeError(error);
|
|
1503
|
-
sessionManager.close(session);
|
|
1504
|
-
return false;
|
|
1505
|
-
}
|
|
1611
|
+
return ok;
|
|
1506
1612
|
};
|
|
1507
1613
|
/**
|
|
1508
1614
|
* Cursor's streamed edit handshake needs the complete target before it sends
|
|
@@ -1668,6 +1774,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1668
1774
|
const emitText = (text) => {
|
|
1669
1775
|
if (!text)
|
|
1670
1776
|
return;
|
|
1777
|
+
assistantText += text;
|
|
1671
1778
|
replaySafety.markBarrier("visible-text");
|
|
1672
1779
|
// Close reasoning before text (hosts expect reasoning-end before text-start).
|
|
1673
1780
|
if (reasoningStarted && !textStarted) {
|
|
@@ -1695,13 +1802,13 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1695
1802
|
const emitFinish = (te, reason, settledUsage, settledSource) => {
|
|
1696
1803
|
closeOpenSpans();
|
|
1697
1804
|
const est = session.usageEstimate;
|
|
1698
|
-
// OpenCode
|
|
1699
|
-
//
|
|
1700
|
-
//
|
|
1701
|
-
//
|
|
1702
|
-
//
|
|
1703
|
-
|
|
1704
|
-
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;
|
|
1705
1812
|
const contextSource = tokenDetails
|
|
1706
1813
|
? session.tokenDetailsFresh
|
|
1707
1814
|
? "checkpoint-current-run"
|
|
@@ -1715,15 +1822,29 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1715
1822
|
priorContextTokens: session.cacheDiagnostics?.priorTokenDetails?.usedTokens,
|
|
1716
1823
|
})
|
|
1717
1824
|
: emptyLanguageModelV3Usage()
|
|
1718
|
-
:
|
|
1825
|
+
: occupancyDetails
|
|
1826
|
+
? occupancyUsageFromTokenDetails(occupancyDetails, session.cacheDiagnostics?.priorTokenDetails)
|
|
1827
|
+
: emptyLanguageModelV3Usage());
|
|
1719
1828
|
const providerMetadata = te
|
|
1720
1829
|
? cursorTurnEndedProviderMetadata(te, tokenDetails, contextSource)
|
|
1721
|
-
:
|
|
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;
|
|
1722
1840
|
const reasonLabel = typeof reason === "object" && reason && "unified" in reason
|
|
1723
1841
|
? String(reason.unified ?? "unknown")
|
|
1724
1842
|
: String(reason);
|
|
1725
1843
|
const inTotal = usage.inputTokens?.total ?? 0;
|
|
1726
1844
|
const outTotal = usage.outputTokens?.total ?? 0;
|
|
1845
|
+
const occupancySource = occupancyDetails
|
|
1846
|
+
? `occupancy-${contextSource ?? "unavailable"}`
|
|
1847
|
+
: "intermediate-zero";
|
|
1727
1848
|
trace(`finish: reason=${reasonLabel} ` +
|
|
1728
1849
|
`v3In=${inTotal} v3Out=${outTotal} ` +
|
|
1729
1850
|
`v3CacheRead=${usage.inputTokens?.cacheRead ?? 0} v3CacheWrite=${usage.inputTokens?.cacheWrite ?? 0} ` +
|
|
@@ -1734,11 +1855,20 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1734
1855
|
`rawCacheWrite=${te ? turnEndedCounter(te, "cache_write") : est.cacheWrite} ` +
|
|
1735
1856
|
`source=${te
|
|
1736
1857
|
? settledSource ?? (contextSource ?? "unavailable")
|
|
1737
|
-
:
|
|
1858
|
+
: occupancySource}`);
|
|
1738
1859
|
if (counters) {
|
|
1739
1860
|
trace(formatTurnUsageValidation(counters, usage, tokenDetails, contextSource));
|
|
1740
1861
|
trace(formatCursorCacheDiagnostics(counters, tokenDetails, cacheDiagnostics.priorTokenDetails, cacheDiagnostics));
|
|
1741
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
|
+
}
|
|
1742
1872
|
safeEnqueue({
|
|
1743
1873
|
type: "finish",
|
|
1744
1874
|
usage,
|
|
@@ -1906,6 +2036,31 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1906
2036
|
`sessionKey=${session.openCodeSessionId}: ${String(error)}`);
|
|
1907
2037
|
});
|
|
1908
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
|
+
}
|
|
1909
2064
|
emitFinish(turnEnded, { unified: "stop", raw: undefined });
|
|
1910
2065
|
sessionManager.close(session);
|
|
1911
2066
|
return;
|
|
@@ -1985,6 +2140,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1985
2140
|
const input = JSON.stringify(bridged.args ?? {});
|
|
1986
2141
|
trace(`display BRIDGED tool-call toolCallId=${toolCallId} toolName=${bridged.toolName} ` +
|
|
1987
2142
|
`variant=${bridged.variant} callId=${callId} inputLen=${input.length}`);
|
|
2143
|
+
emittedHostTools++;
|
|
1988
2144
|
closeOpenSpans();
|
|
1989
2145
|
safeEnqueue({
|
|
1990
2146
|
type: "tool-call",
|
|
@@ -2075,6 +2231,24 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2075
2231
|
failRunProtocol("Cursor read_mcp_resource reply failed", RUN_REPLY_FAILED);
|
|
2076
2232
|
}
|
|
2077
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
|
+
}
|
|
2078
2252
|
else {
|
|
2079
2253
|
replaySafety.markBarrier("non-control-exec");
|
|
2080
2254
|
const displayCallId = extractExecDisplayCallId(esm);
|
|
@@ -2112,7 +2286,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2112
2286
|
trace(`exec: id=${parsed?.id} variant=${parsed ? Object.keys(parsed).join(",") : "none"} toolName=${parsed?.toolName} resultField=${parsed?.resultField}`);
|
|
2113
2287
|
if (parsed) {
|
|
2114
2288
|
if (parsed.localError) {
|
|
2115
|
-
if (!rejectExec(parsed, parsed.localError, "invalid mapping"))
|
|
2289
|
+
if (!await rejectExec(parsed, parsed.localError, "invalid mapping"))
|
|
2116
2290
|
return;
|
|
2117
2291
|
continue;
|
|
2118
2292
|
}
|
|
@@ -2122,7 +2296,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2122
2296
|
// pumping for text / turn_ended instead of emitting tool-call.
|
|
2123
2297
|
if (!session.allowTools) {
|
|
2124
2298
|
const reason = "Tool calls are not available during this turn (summary/compaction).";
|
|
2125
|
-
if (!rejectExec(parsed, reason, "allowTools=false"))
|
|
2299
|
+
if (!await rejectExec(parsed, reason, "allowTools=false"))
|
|
2126
2300
|
return;
|
|
2127
2301
|
continue;
|
|
2128
2302
|
}
|
|
@@ -2136,7 +2310,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2136
2310
|
if (!advertisedToolNameSet.has(CURSOR_IMAGE_SAVE_TOOL)) {
|
|
2137
2311
|
const reason = "This OpenCode agent cannot write binary file content. "
|
|
2138
2312
|
+ "Do not retry this write with the same bytes.";
|
|
2139
|
-
if (!rejectExec(parsed, reason, "binary write unsupported"))
|
|
2313
|
+
if (!await rejectExec(parsed, reason, "binary write unsupported"))
|
|
2140
2314
|
return;
|
|
2141
2315
|
continue;
|
|
2142
2316
|
}
|
|
@@ -2157,7 +2331,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2157
2331
|
});
|
|
2158
2332
|
}
|
|
2159
2333
|
catch (error) {
|
|
2160
|
-
if (!rejectExec(parsed, error.message, "binary write too large"))
|
|
2334
|
+
if (!await rejectExec(parsed, error.message, "binary write too large"))
|
|
2161
2335
|
return;
|
|
2162
2336
|
continue;
|
|
2163
2337
|
}
|
|
@@ -2173,6 +2347,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2173
2347
|
trace(`exec: binary write STAGED toolCallId=${toolCallId} ` +
|
|
2174
2348
|
`requested=${JSON.stringify(binaryWrite.path)} target=${JSON.stringify(target)} ` +
|
|
2175
2349
|
`bytes=${binaryWrite.data.length}`);
|
|
2350
|
+
emittedHostTools++;
|
|
2176
2351
|
closeOpenSpans();
|
|
2177
2352
|
safeEnqueue({
|
|
2178
2353
|
type: "tool-call",
|
|
@@ -2198,7 +2373,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2198
2373
|
`'${parsed.toolName}'.`;
|
|
2199
2374
|
trace(`exec: unavailable catalog target toolName=${parsed.toolName} ` +
|
|
2200
2375
|
`advertised=[${advertisedToolNames.join(",")}]`);
|
|
2201
|
-
if (!rejectExec(parsed, reason, "unavailable tool"))
|
|
2376
|
+
if (!await rejectExec(parsed, reason, "unavailable tool"))
|
|
2202
2377
|
return;
|
|
2203
2378
|
continue;
|
|
2204
2379
|
}
|
|
@@ -2215,6 +2390,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2215
2390
|
}
|
|
2216
2391
|
const tc = buildToolCallPart(parsed, session.sessionId);
|
|
2217
2392
|
if (parsed.resultField === "shell_stream"
|
|
2393
|
+
|| parsed.resultField === "shell_result"
|
|
2218
2394
|
|| parsed.resultField === "background_shell_spawn_result") {
|
|
2219
2395
|
registerCursorShellCall(tc.toolCallId, parsed.resultMetadata);
|
|
2220
2396
|
}
|
|
@@ -2222,6 +2398,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2222
2398
|
sessionManager.registerPending(parsed.id, session, parsed.resultField, parsed.toolName, false, parsed.resultMetadata);
|
|
2223
2399
|
// tc.input is already a JSON string (LanguageModelV3ToolCall.input).
|
|
2224
2400
|
trace(`exec: EMITTED tool-call toolCallId=${tc.toolCallId} toolName=${tc.toolName} inputLen=${tc.input.length}`);
|
|
2401
|
+
emittedHostTools++;
|
|
2225
2402
|
// Close open text/reasoning spans before tool-call (required by AI SDK V3).
|
|
2226
2403
|
closeOpenSpans();
|
|
2227
2404
|
safeEnqueue({
|
|
@@ -2233,11 +2410,36 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2233
2410
|
emitFinish(undefined, { unified: "tool-calls", raw: undefined });
|
|
2234
2411
|
return;
|
|
2235
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
|
+
}
|
|
2236
2439
|
// Never guess a response type for an unknown exec variant. Request and
|
|
2237
2440
|
// result field numbers are not universally identical; a structurally
|
|
2238
2441
|
// wrong reply recreates the heartbeat-only deadlock. Fail promptly so
|
|
2239
2442
|
// schema drift is actionable.
|
|
2240
|
-
const variantField = detectExecVariantField(payload);
|
|
2241
2443
|
const variantDescription = describeCursorExecVariant(variantField);
|
|
2242
2444
|
const hex = Array.from(payload.subarray(0, 48))
|
|
2243
2445
|
.map((x) => x.toString(16).padStart(2, "0"))
|
|
@@ -2313,6 +2515,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2313
2515
|
trace(`interaction_query: BRIDGED ask_question id=${handled.id} toolCallId=${toolCallId} ` +
|
|
2314
2516
|
`questions=${ask.args.questions.length} runAsync=${ask.args.runAsync} ` +
|
|
2315
2517
|
`cursorToolCallId=${ask.toolCallId || "(none)"}`);
|
|
2518
|
+
emittedHostTools++;
|
|
2316
2519
|
closeOpenSpans();
|
|
2317
2520
|
safeEnqueue({
|
|
2318
2521
|
type: "tool-call",
|
|
@@ -2359,6 +2562,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2359
2562
|
`bridge=${bridgeKind} hostTool=${toolName} ` +
|
|
2360
2563
|
`target=${JSON.stringify(sw.args.targetModeId)} ` +
|
|
2361
2564
|
`cursorToolCallId=${sw.toolCallId || "(none)"}`);
|
|
2565
|
+
emittedHostTools++;
|
|
2362
2566
|
closeOpenSpans();
|
|
2363
2567
|
safeEnqueue({
|
|
2364
2568
|
type: "tool-call",
|
|
@@ -2400,6 +2604,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2400
2604
|
// before asking — approving a plan you cannot read is not approval.
|
|
2401
2605
|
if (plan.planReview)
|
|
2402
2606
|
emitText(plan.planReview);
|
|
2607
|
+
emittedHostTools++;
|
|
2403
2608
|
closeOpenSpans();
|
|
2404
2609
|
safeEnqueue({
|
|
2405
2610
|
type: "tool-call",
|
|
@@ -2630,6 +2835,9 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
2630
2835
|
: []),
|
|
2631
2836
|
...instructions,
|
|
2632
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.",
|
|
2633
2841
|
].join("\n");
|
|
2634
2842
|
}
|
|
2635
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>;
|