cursor-opencode-provider 0.4.3 → 0.5.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.
Files changed (37) hide show
  1. package/README.md +17 -12
  2. package/dist/context/build.d.ts +8 -0
  3. package/dist/context/build.js +72 -23
  4. package/dist/context/frozen.d.ts +29 -0
  5. package/dist/context/frozen.js +168 -0
  6. package/dist/context/index.d.ts +2 -1
  7. package/dist/context/index.js +2 -1
  8. package/dist/context/rules.d.ts +1 -0
  9. package/dist/context/rules.js +1 -1
  10. package/dist/language-model.d.ts +10 -9
  11. package/dist/language-model.js +387 -118
  12. package/dist/protocol/blob-reachability.d.ts +15 -0
  13. package/dist/protocol/blob-reachability.js +140 -0
  14. package/dist/protocol/blob-store.d.ts +37 -0
  15. package/dist/protocol/blob-store.js +115 -0
  16. package/dist/protocol/conversation-bind.d.ts +13 -0
  17. package/dist/protocol/conversation-bind.js +27 -1
  18. package/dist/protocol/conversation-persistence.d.ts +32 -0
  19. package/dist/protocol/conversation-persistence.js +504 -0
  20. package/dist/protocol/conversation-state.d.ts +16 -0
  21. package/dist/protocol/conversation-state.js +62 -0
  22. package/dist/protocol/kv.d.ts +1 -0
  23. package/dist/protocol/kv.js +2 -0
  24. package/dist/protocol/messages.d.ts +2 -0
  25. package/dist/protocol/messages.js +30 -2
  26. package/dist/protocol/request.d.ts +2 -0
  27. package/dist/protocol/request.js +2 -0
  28. package/dist/protocol/token-details.d.ts +33 -0
  29. package/dist/protocol/token-details.js +155 -0
  30. package/dist/protocol/tools.d.ts +17 -0
  31. package/dist/protocol/tools.js +76 -0
  32. package/dist/session.d.ts +39 -11
  33. package/dist/shared.d.ts +3 -0
  34. package/dist/shared.js +3 -0
  35. package/dist/usage.d.ts +40 -35
  36. package/dist/usage.js +199 -68
  37. package/package.json +1 -1
@@ -76,6 +76,8 @@ export function buildRunRequest(input) {
76
76
  });
77
77
  const runRequest = {
78
78
  conversation_id: input.conversationId,
79
+ conversation_group_id: input.conversationGroupId ?? input.conversationId,
80
+ run_id: msgId,
79
81
  action,
80
82
  requested_model: {
81
83
  // The provider always selects a concrete model. Cursor's "default"
@@ -0,0 +1,33 @@
1
+ export type CursorPromptTokenCategory = {
2
+ id: string;
3
+ label: string;
4
+ estimatedTokens: number;
5
+ characterCount?: number;
6
+ };
7
+ export type CursorPromptTokenBreakdown = {
8
+ totalUsedTokens: number;
9
+ maxTokens: number;
10
+ categories: CursorPromptTokenCategory[];
11
+ };
12
+ export type CursorConversationTokenDetails = {
13
+ usedTokens: number;
14
+ maxTokens: number;
15
+ breakdown?: CursorPromptTokenBreakdown;
16
+ };
17
+ export type CursorContextUsageSource = "checkpoint-current-run" | "checkpoint-previous-turn";
18
+ /**
19
+ * Read field #5 (`ConversationTokenDetails`) from Cursor's opaque
20
+ * `ConversationStateStructure` checkpoint without decoding or re-encoding the
21
+ * rest of the state. Unknown checkpoint fields therefore remain byte-exact.
22
+ */
23
+ export declare function decodeConversationTokenDetails(checkpoint: Uint8Array | undefined): CursorConversationTokenDetails | undefined;
24
+ export declare function cursorContextUsageMetadata(details: CursorConversationTokenDetails, source?: CursorContextUsageSource): {
25
+ contextUsageVersion: number;
26
+ source: CursorContextUsageSource;
27
+ stale: boolean;
28
+ usedTokens: number;
29
+ maxTokens: number;
30
+ remainingTokens: number;
31
+ usedPercent?: number;
32
+ breakdown?: CursorPromptTokenBreakdown;
33
+ };
@@ -0,0 +1,155 @@
1
+ import protobuf from "protobufjs";
2
+ const MAX_BREAKDOWN_CATEGORIES = 128;
3
+ function requireWireType(actual, expected, field) {
4
+ if (actual !== expected)
5
+ throw new Error(`invalid ${field} wire type`);
6
+ }
7
+ function decodeCategory(data) {
8
+ const reader = protobuf.Reader.create(data);
9
+ let id = "";
10
+ let label = "";
11
+ let estimatedTokens = 0;
12
+ let characterCount;
13
+ while (reader.pos < reader.len) {
14
+ const tag = reader.uint32();
15
+ const wireType = tag & 7;
16
+ switch (tag >>> 3) {
17
+ case 1:
18
+ requireWireType(wireType, 2, "category id");
19
+ id = reader.string();
20
+ break;
21
+ case 2:
22
+ requireWireType(wireType, 2, "category label");
23
+ label = reader.string();
24
+ break;
25
+ case 3:
26
+ requireWireType(wireType, 0, "category token count");
27
+ estimatedTokens = reader.uint32();
28
+ break;
29
+ case 4:
30
+ requireWireType(wireType, 0, "category character count");
31
+ characterCount = reader.uint32();
32
+ break;
33
+ default:
34
+ reader.skipType(wireType);
35
+ }
36
+ }
37
+ return {
38
+ id,
39
+ label,
40
+ estimatedTokens,
41
+ ...(characterCount === undefined ? {} : { characterCount }),
42
+ };
43
+ }
44
+ function decodeBreakdown(data) {
45
+ const reader = protobuf.Reader.create(data);
46
+ let totalUsedTokens = 0;
47
+ let maxTokens = 0;
48
+ const categories = [];
49
+ while (reader.pos < reader.len) {
50
+ const tag = reader.uint32();
51
+ const wireType = tag & 7;
52
+ switch (tag >>> 3) {
53
+ case 1:
54
+ requireWireType(wireType, 0, "breakdown used tokens");
55
+ totalUsedTokens = reader.uint32();
56
+ break;
57
+ case 2:
58
+ requireWireType(wireType, 0, "breakdown max tokens");
59
+ maxTokens = reader.uint32();
60
+ break;
61
+ case 3:
62
+ requireWireType(wireType, 2, "breakdown category");
63
+ if (categories.length < MAX_BREAKDOWN_CATEGORIES) {
64
+ categories.push(decodeCategory(reader.bytes()));
65
+ }
66
+ else {
67
+ reader.bytes();
68
+ }
69
+ break;
70
+ default:
71
+ reader.skipType(wireType);
72
+ }
73
+ }
74
+ return { totalUsedTokens, maxTokens, categories };
75
+ }
76
+ function decodeTokenDetails(data) {
77
+ const reader = protobuf.Reader.create(data);
78
+ let usedTokens = 0;
79
+ let maxTokens = 0;
80
+ let sawUsedTokens = false;
81
+ let sawMaxTokens = false;
82
+ let breakdown;
83
+ while (reader.pos < reader.len) {
84
+ const tag = reader.uint32();
85
+ const wireType = tag & 7;
86
+ switch (tag >>> 3) {
87
+ case 1:
88
+ requireWireType(wireType, 0, "used tokens");
89
+ usedTokens = reader.uint32();
90
+ sawUsedTokens = true;
91
+ break;
92
+ case 2:
93
+ requireWireType(wireType, 0, "max tokens");
94
+ maxTokens = reader.uint32();
95
+ sawMaxTokens = true;
96
+ break;
97
+ case 3:
98
+ requireWireType(wireType, 2, "token breakdown");
99
+ breakdown = decodeBreakdown(reader.bytes());
100
+ break;
101
+ default:
102
+ reader.skipType(wireType);
103
+ }
104
+ }
105
+ if (!sawUsedTokens && !sawMaxTokens && !breakdown)
106
+ return undefined;
107
+ return {
108
+ usedTokens,
109
+ maxTokens,
110
+ ...(breakdown ? { breakdown } : {}),
111
+ };
112
+ }
113
+ /**
114
+ * Read field #5 (`ConversationTokenDetails`) from Cursor's opaque
115
+ * `ConversationStateStructure` checkpoint without decoding or re-encoding the
116
+ * rest of the state. Unknown checkpoint fields therefore remain byte-exact.
117
+ */
118
+ export function decodeConversationTokenDetails(checkpoint) {
119
+ if (!checkpoint?.length)
120
+ return undefined;
121
+ try {
122
+ const reader = protobuf.Reader.create(checkpoint);
123
+ let details;
124
+ while (reader.pos < reader.len) {
125
+ const tag = reader.uint32();
126
+ const wireType = tag & 7;
127
+ if ((tag >>> 3) === 5) {
128
+ requireWireType(wireType, 2, "conversation token details");
129
+ details = decodeTokenDetails(reader.bytes());
130
+ }
131
+ else {
132
+ reader.skipType(wireType);
133
+ }
134
+ }
135
+ return details;
136
+ }
137
+ catch {
138
+ return undefined;
139
+ }
140
+ }
141
+ export function cursorContextUsageMetadata(details, source = "checkpoint-current-run") {
142
+ const usedPercent = details.maxTokens > 0
143
+ ? Math.max(0, Math.min(100, Math.round(details.usedTokens / details.maxTokens * 1_000) / 10))
144
+ : undefined;
145
+ return {
146
+ contextUsageVersion: 2,
147
+ source,
148
+ stale: source === "checkpoint-previous-turn",
149
+ usedTokens: details.usedTokens,
150
+ maxTokens: details.maxTokens,
151
+ remainingTokens: Math.max(0, details.maxTokens - details.usedTokens),
152
+ ...(usedPercent === undefined ? {} : { usedPercent }),
153
+ ...(details.breakdown ? { breakdown: details.breakdown } : {}),
154
+ };
155
+ }
@@ -89,6 +89,16 @@ export declare function resolveCursorSubagentType(subagentType: string, catalog?
89
89
  export declare function mapCursorSubagentTypeToOpenCode(subagentType: string): string;
90
90
  /** Resolve a native Cursor subagent request to this turn's executor and catalog. */
91
91
  export declare function remapNativeSubagentForCatalog(parsed: ParsedExecRequest, advertisedToolNames: Iterable<string>, catalog?: HostSubagentCatalog): void;
92
+ /**
93
+ * Complete the private read inside Cursor's legacy edit transaction without
94
+ * routing it through OpenCode's user-facing `read` tool. The latter caps every
95
+ * result at 50 KB, while Cursor's editor needs the complete file to calculate
96
+ * the correlated whole-file `write_args` that follows.
97
+ *
98
+ * The caller is responsible for proving that `filePath` is the same regular
99
+ * file named by an active `edit_tool_call` and is contained by the workspace.
100
+ */
101
+ export declare function buildCompleteEditReadMessages(execId: number, sourcePath: string, resultPath?: string): Uint8Array[] | undefined;
92
102
  /**
93
103
  * Preserve a legacy Cursor edit's intent when its internal executor follows
94
104
  * `edit_tool_call` with a whole-file `write_args` request.
@@ -125,6 +135,13 @@ export type ParsedExecRequest = {
125
135
  /** Original request values needed by typed Cursor result messages. */
126
136
  resultMetadata?: Record<string, unknown>;
127
137
  };
138
+ /**
139
+ * A partial-read notice is for Cursor's model, never for a file. Refuse only
140
+ * whole-file mutation forms that echo it. A targeted edit or Update File patch
141
+ * cannot truncate an unseen tail, and may legitimately edit source that quotes
142
+ * this provider's warning text.
143
+ */
144
+ export declare function rejectPartialReadMutation(parsed: ParsedExecRequest): void;
128
145
  export declare function parseExecServerMessage(msg: Record<string, unknown>): ParsedExecRequest | undefined;
129
146
  /**
130
147
  * Remap Cursor exec / MCP arg shapes onto OpenCode's Effect Schema keys.
@@ -506,6 +506,45 @@ export function remapNativeSubagentForCatalog(parsed, advertisedToolNames, catal
506
506
  }
507
507
  /** Matches Cursor's own pre-write read threshold (`local-exec` 52428800). */
508
508
  const MAX_EDIT_SOURCE_BYTES = 50 * 1024 * 1024;
509
+ /**
510
+ * Complete the private read inside Cursor's legacy edit transaction without
511
+ * routing it through OpenCode's user-facing `read` tool. The latter caps every
512
+ * result at 50 KB, while Cursor's editor needs the complete file to calculate
513
+ * the correlated whole-file `write_args` that follows.
514
+ *
515
+ * The caller is responsible for proving that `filePath` is the same regular
516
+ * file named by an active `edit_tool_call` and is contained by the workspace.
517
+ */
518
+ export function buildCompleteEditReadMessages(execId, sourcePath, resultPath = sourcePath) {
519
+ try {
520
+ const stat = fs.statSync(sourcePath);
521
+ if (!stat.isFile() || stat.size > MAX_EDIT_SOURCE_BYTES)
522
+ return undefined;
523
+ const content = fs.readFileSync(sourcePath, "utf8");
524
+ return [
525
+ encodeMessage("AgentClientMessage", {
526
+ exec_client_message: {
527
+ id: execId,
528
+ local_execution_time_ms: 0,
529
+ read_result: {
530
+ success: {
531
+ path: resultPath,
532
+ content,
533
+ total_lines: countLines(content),
534
+ file_size: stat.size,
535
+ truncated: false,
536
+ range_applied: false,
537
+ },
538
+ },
539
+ },
540
+ }),
541
+ buildExecStreamClose(execId),
542
+ ];
543
+ }
544
+ catch {
545
+ return undefined;
546
+ }
547
+ }
509
548
  /** Return the number of non-overlapping occurrences, stopping after ambiguity. */
510
549
  function replacementOccurrenceCount(source, needle) {
511
550
  if (!needle)
@@ -709,6 +748,43 @@ export function remapEditToolsForCatalog(parsed, advertisedToolNames, workspaceR
709
748
  // the requested path for the typed Cursor result.
710
749
  parsed.resultMetadata = { ...parsed.resultMetadata, path: filePath };
711
750
  }
751
+ /**
752
+ * A partial-read notice is for Cursor's model, never for a file. Refuse only
753
+ * whole-file mutation forms that echo it. A targeted edit or Update File patch
754
+ * cannot truncate an unseen tail, and may legitimately edit source that quotes
755
+ * this provider's warning text.
756
+ */
757
+ export function rejectPartialReadMutation(parsed) {
758
+ if (parsed.localError)
759
+ return;
760
+ let content;
761
+ if (parsed.toolName === "write") {
762
+ content = parsed.args.content;
763
+ }
764
+ else if (parsed.toolName === APPLY_PATCH_TOOL) {
765
+ const patchText = parsed.args.patchText;
766
+ if (typeof patchText !== "string" || !patchText.includes("*** Add File:"))
767
+ return;
768
+ content = patchText;
769
+ }
770
+ else {
771
+ return;
772
+ }
773
+ if (typeof content !== "string"
774
+ || !content.includes("[Partial read:")
775
+ || !content.includes("It is NOT the complete file."))
776
+ return;
777
+ const filePath = typeof parsed.args.filePath === "string"
778
+ ? parsed.args.filePath
779
+ : "the target file";
780
+ const nextOffset = /Continue with offset=(\d+)/.exec(content)?.[1];
781
+ parsed.localError =
782
+ `NO FILE CHANGE WAS MADE. Refusing a whole-file mutation of ${JSON.stringify(filePath)} ` +
783
+ "because it contains the provider's partial-read notice. Do not retry the same mutation. " +
784
+ (nextOffset
785
+ ? `Read the file from offset=${nextOffset}, then use a targeted edit or Update File patch.`
786
+ : "Read the remaining file ranges, then use a targeted edit or Update File patch.");
787
+ }
712
788
  export function parseExecServerMessage(msg) {
713
789
  const id = msg.id;
714
790
  if (id === undefined)
package/dist/session.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { BidiStream } from "./transport/connect.js";
2
2
  import { type CursorProviderError } from "./errors.js";
3
3
  import { type SessionActivitySource } from "./activity.js";
4
- import type { HostSubagentCatalog, ToolAliasRegistry } from "./protocol/tools.js";
4
+ import type { HostSubagentCatalog, OpencodeToolDef, ToolAliasRegistry } from "./protocol/tools.js";
5
+ import type { CursorConversationTokenDetails } from "./protocol/token-details.js";
5
6
  export type Frame = {
6
7
  flags: number;
7
8
  payload: Uint8Array;
@@ -99,10 +100,44 @@ export type CursorSession = {
99
100
  * conversation_checkpoint_update (CLI parity).
100
101
  */
101
102
  conversationId: string;
103
+ /** Cache root owning this conversation's durable restart state. */
104
+ cacheDir?: string;
102
105
  /** Latest eligible checkpoint emitted by this Run attempt. */
103
106
  resumeCheckpoint?: Uint8Array;
107
+ /** Last checkpoint-derived context snapshot, independent of retry eligibility. */
108
+ tokenDetails?: CursorConversationTokenDetails;
109
+ /** True only after this Run receives a checkpoint containing token details. */
110
+ tokenDetailsFresh?: boolean;
111
+ /**
112
+ * Run-lifetime cache evidence. Cursor exposes only aggregate cache counters at
113
+ * TurnEnded, so retain the inputs and protocol activity needed to explain
114
+ * whether a low ratio came from a cold/rebased prefix, context growth, or a
115
+ * multi-step agent Run.
116
+ */
117
+ cacheDiagnostics?: {
118
+ sessionKey?: string;
119
+ conversationId: string;
120
+ conversationGroupId?: string;
121
+ modelId?: string;
122
+ priorTokenDetails?: CursorConversationTokenDetails;
123
+ startedWithCheckpoint: boolean;
124
+ requestContextReused: boolean;
125
+ requestContextHash: string;
126
+ systemPromptHash?: string;
127
+ checkpointUpdates: number;
128
+ tokenDetailUpdates: number;
129
+ pumpPasses: number;
130
+ stepStarts: number;
131
+ stepCompletes: number;
132
+ displayToolCalls: number;
133
+ execRequests: number;
134
+ };
104
135
  /** OpenCode session whose own or descendant activity renews tool leases. */
105
136
  openCodeSessionId?: string;
137
+ /** Completed compaction must rebase once before resuming a normal agent. */
138
+ postCompactionRebase?: boolean;
139
+ /** Last real host catalog, retained only as a lifecycle-turn fallback. */
140
+ toolCatalog?: OpencodeToolDef[];
106
141
  stream: BidiStream;
107
142
  frames: AsyncIterator<Frame>;
108
143
  pending: Map<number, PendingExec>;
@@ -119,6 +154,7 @@ export type CursorSession = {
119
154
  */
120
155
  editToolCalls?: Map<string, {
121
156
  path: string;
157
+ completeRead?: boolean;
122
158
  }>;
123
159
  /** Monotonic synthetic exec ids for bridged (display-only) OpenCode tool calls. */
124
160
  nextBridgedExecId: number;
@@ -139,9 +175,8 @@ export type CursorSession = {
139
175
  */
140
176
  allowTools: boolean;
141
177
  /**
142
- * Best-effort token usage for this held-open Run. Updated from text/tool
143
- * activity and replaced by TurnEnded only for a single-step Run. Emitted on
144
- * tool-call finishes so OpenCode does not store all-zero usage mid-loop.
178
+ * Best-effort held-Run activity counters used only for diagnostics. AI SDK
179
+ * accounting comes from the request-local final TurnEnded, never this estimate.
145
180
  */
146
181
  usageEstimate: {
147
182
  inputTokens: number;
@@ -150,13 +185,6 @@ export type CursorSession = {
150
185
  cacheWrite: number;
151
186
  reasoningTokens: number;
152
187
  };
153
- /**
154
- * True after this held Run has already crossed an OpenCode tool-call
155
- * boundary. Cursor's eventual TurnEnded counters then cover the whole Run,
156
- * not only the final doStream request, so they cannot be used as that
157
- * request's context usage.
158
- */
159
- hadToolCallBoundary?: boolean;
160
188
  /**
161
189
  * True while a doStream pull() is actively reading this session's frames.
162
190
  * Prevents a late cancel/abort from a prior ReadableStream from destroying
package/dist/shared.d.ts CHANGED
@@ -12,6 +12,9 @@ export declare const MODEL_CACHE_FILE = "cursor-models.json";
12
12
  /** Bumped when the on-disk model cache shape/semantics change (forces refetch). */
13
13
  export declare const MODEL_CACHE_SCHEMA_VERSION = 3;
14
14
  export declare const MODEL_CACHE_TTL_MS = 86400000;
15
+ export declare const CONVERSATION_CACHE_DIR = "cursor-conversations";
16
+ export declare const CONVERSATION_CACHE_SCHEMA_VERSION = 3;
17
+ export declare const CONVERSATION_CACHE_TTL_MS = 86400000;
15
18
  export declare const VERSION_CACHE_FILE = "cursor-client-version.json";
16
19
  export declare const CONTENT_TYPE_CONNECT_PROTO = "application/connect+proto";
17
20
  export declare const CONNECT_PROTOCOL_VERSION = "1";
package/dist/shared.js CHANGED
@@ -12,6 +12,9 @@ export const MODEL_CACHE_FILE = "cursor-models.json";
12
12
  /** Bumped when the on-disk model cache shape/semantics change (forces refetch). */
13
13
  export const MODEL_CACHE_SCHEMA_VERSION = 3;
14
14
  export const MODEL_CACHE_TTL_MS = 86_400_000;
15
+ export const CONVERSATION_CACHE_DIR = "cursor-conversations";
16
+ export const CONVERSATION_CACHE_SCHEMA_VERSION = 3;
17
+ export const CONVERSATION_CACHE_TTL_MS = 86_400_000;
15
18
  export const VERSION_CACHE_FILE = "cursor-client-version.json";
16
19
  export const CONTENT_TYPE_CONNECT_PROTO = "application/connect+proto";
17
20
  export const CONNECT_PROTOCOL_VERSION = "1";
package/dist/usage.d.ts CHANGED
@@ -1,49 +1,54 @@
1
1
  import type { LanguageModelV3Usage } from "@ai-sdk/provider";
2
- /** Non-negative integer counter from a Cursor `turn_ended` field. */
3
- export declare function turnEndedCounter(te: Record<string, unknown>, key: string): number;
4
- /**
5
- * Map request-local Cursor TurnEnded counters to AI SDK V3 usage (Kilo/OpenCode
6
- * flatten via input total = noCache + cacheRead + cacheWrite). Only use this
7
- * for a Run with no prior tool boundary; multi-step TurnEnded is cumulative.
8
- *
9
- * Contract: `input_tokens` is non-cached input; cache fields are separate.
10
- * Cursor's agent wire currently sends `cache_write=0` even when later turns
11
- * report large `cache_read`; we still forward the counter when non-zero.
12
- * If production semantics differ (input_tokens already total), adjust here.
13
- */
14
- export declare function buildLanguageModelV3UsageFromTurnEnded(te: Record<string, unknown>): LanguageModelV3Usage;
15
- export type CursorUsageEstimate = {
2
+ import type { CursorContextUsageSource, CursorConversationTokenDetails } from "./protocol/token-details.js";
3
+ export type CursorUsageCounters = {
16
4
  inputTokens: number;
17
5
  outputTokens: number;
18
6
  cacheRead: number;
19
7
  cacheWrite: number;
20
8
  reasoningTokens: number;
21
9
  };
10
+ export type CursorUsageOptions = {
11
+ /** Cursor checkpoint occupancy, including the current turn's output. */
12
+ contextTotalTokens?: number;
13
+ };
14
+ export type CursorCacheDiagnosticStats = {
15
+ sessionKey?: string;
16
+ conversationId: string;
17
+ conversationGroupId?: string;
18
+ modelId?: string;
19
+ startedWithCheckpoint: boolean;
20
+ requestContextReused: boolean;
21
+ requestContextHash: string;
22
+ systemPromptHash?: string;
23
+ checkpointUpdates: number;
24
+ tokenDetailUpdates: number;
25
+ pumpPasses: number;
26
+ stepStarts: number;
27
+ stepCompletes: number;
28
+ displayToolCalls: number;
29
+ execRequests: number;
30
+ };
31
+ /** Non-negative integer counter from a Cursor `turn_ended` field. */
32
+ export declare function turnEndedCounter(te: Record<string, unknown>, key: string): number;
33
+ export declare function cursorUsageCountersFromTurnEnded(te: Record<string, unknown>): CursorUsageCounters;
22
34
  /**
23
- * A single-step trust path for TurnEnded counters is only safe when the
24
- * totals are close to the request-local budget. Cursor reports cumulative
25
- * conversation/Run totals even on Runs that never crossed an OpenCode tool
26
- * boundary: single-step follow-up turns and parallel no-tools side calls
27
- * carry the whole conversation's `cache_read`. Treating those totals as
28
- * request-local inflates OpenCode's context usage and triggers premature
29
- * compaction (issue #20).
35
+ * Map Cursor counters to AI SDK V3. Cursor's `input_tokens` already includes
36
+ * cache reads/writes, and `output_tokens` already includes reasoning.
30
37
  */
31
- export declare const CUMULATIVE_TURN_ENDED_RATIO = 3;
32
- export declare const MIN_TRUSTED_TURN_ENDED_TOTAL = 1024;
33
- export declare function exceedsRequestLocalBudget(te: Record<string, unknown>, budget: {
34
- inputTokens: number;
35
- outputTokens: number;
36
- }): boolean;
38
+ export declare function buildLanguageModelV3UsageFromCounters(counters: CursorUsageCounters, options?: CursorUsageOptions): LanguageModelV3Usage;
39
+ export declare function buildLanguageModelV3UsageFromTurnEnded(te: Record<string, unknown>, options?: CursorUsageOptions): LanguageModelV3Usage;
40
+ /** Compact, parseable category snapshot for checkpoint-by-checkpoint traces. */
41
+ export declare function formatCursorTokenCategories(details: CursorConversationTokenDetails | undefined): string;
37
42
  /**
38
- * Request-local usage for a held Cursor Run.
39
- *
40
- * `est.inputTokens` tracks the seed plus delivered tool results, while
41
- * `est.outputTokens` accumulates model output across earlier tool boundaries.
42
- * Move prior output into the current input estimate and report only this
43
- * doStream request's output. Cache counters are intentionally absent: the
44
- * only counters Cursor exposes at TurnEnded are cumulative for the whole Run.
43
+ * Explain Cursor's aggregate cache counters using the state visible to this
44
+ * client. `stepStarts` is deliberately not called a model-call count: Cursor
45
+ * does not expose per-model-call cache accounting on the Run stream.
45
46
  */
46
- export declare function buildLanguageModelV3UsageFromEstimate(est: CursorUsageEstimate, promptTokens: number, outputChars: number, estimateCharsToTokens: (chars: number) => number): LanguageModelV3Usage;
47
+ export declare function formatCursorCacheDiagnostics(counters: CursorUsageCounters, current: CursorConversationTokenDetails | undefined, prior: CursorConversationTokenDetails | undefined, stats: CursorCacheDiagnosticStats): string;
48
+ /** One-line proof that Cursor, AI SDK, and projected OpenCode totals agree. */
49
+ export declare function formatTurnUsageValidation(counters: CursorUsageCounters, usage: LanguageModelV3Usage, tokenDetails?: CursorConversationTokenDetails, contextSource?: CursorContextUsageSource): string;
50
+ /** OpenCode requires a usage object at every step boundary. */
51
+ export declare function emptyLanguageModelV3Usage(): LanguageModelV3Usage;
47
52
  /** Mirror Kilo `Session.getUsage` / AI SDK flat fields from V3 nested usage. */
48
53
  export declare function kiloShapedUsageFromV3(usage: LanguageModelV3Usage): {
49
54
  inputTokens: number;