pi-cursor-sdk 0.1.51 → 0.1.53
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/CHANGELOG.md +31 -0
- package/README.md +2 -2
- package/docs/cursor-live-smoke-checklist.md +3 -3
- package/docs/cursor-model-ux-spec.md +1 -1
- package/docs/cursor-native-tool-replay.md +1 -1
- package/docs/cursor-tool-surfaces.md +1 -1
- package/docs/platform-smoke.md +2 -4
- package/package.json +1 -1
- package/scripts/platform-smoke/live-suite-runner.mjs +1 -1
- package/scripts/platform-smoke/targets.mjs +3 -5
- package/scripts/validate-smoke-jsonl.mjs +3 -4
- package/src/context.ts +38 -21
- package/src/cursor-live-run-accounting.ts +19 -0
- package/src/cursor-live-run-coordinator.ts +33 -0
- package/src/cursor-provider-live-run-drain.ts +24 -4
- package/src/cursor-provider-run-finalizer.ts +4 -2
- package/src/cursor-provider-turn-coordinator.ts +11 -0
- package/src/cursor-provider-turn-prepare.ts +11 -1
- package/src/cursor-provider-turn-runner.ts +1 -1
- package/src/cursor-provider.ts +9 -2
- package/src/cursor-session-turn-queue.ts +52 -0
- package/src/cursor-tool-manifest.ts +16 -13
- package/src/cursor-usage-accounting.ts +55 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.53 - 2026-06-29
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Use real per-turn Cursor SDK usage when available, including cache read/write fields, while keeping local prompt/output estimates as the no-SDK-usage fallback. SDK-attributed context occupancy is pinned to latest-turn input plus output so cumulative run input and cache reads do not overstate compaction pressure; fallback context occupancy uses the replayable context estimate so split tool turns do not show a tiny footer percentage next to large input counts.
|
|
10
|
+
- Shorten Cursor bootstrap and incremental prompt boundary text while preserving host-tool, configured MCP, pi bridge, exact-output, shell cwd, plan-mode, and latest-image guidance. The callable-surface manifest remains enabled by default but uses compact wording for Cursor host/configured MCP tools and pi tools/bridge exposure.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Resolve standalone Windows platform-smoke suite setup by resolving the local `pi` CLI after target dependencies are installed instead of falling back to a missing global `pi.cmd`.
|
|
15
|
+
|
|
16
|
+
### Validation
|
|
17
|
+
|
|
18
|
+
- Usage accounting regression tests prove the old 6.7M cumulative fixture now reports per-turn input `25,432`, cache read `24,000`, and context `26,044` instead of a sub-1k char/4 estimate or cumulative input.
|
|
19
|
+
- Prompt-builder measurement reduced pi's local bootstrap estimate from 537 to 367 tokens for explicit no-bridge eval prompts and from 546 to 367 tokens for interactive bridge prompts; this is not a claim about real Cursor token savings because SDK/Cursor-side settings, rules, and tool schemas dominate actual usage.
|
|
20
|
+
- `pi-model-eval` default-seed HumanEval+ smoke for `cursor/gpt-5.5@272k (medium)` passed 5/5 with average pi-estimated input reduced from 721.2 to 648.2 on the same first five tasks; real Cursor usage is now reported from SDK usage when available.
|
|
21
|
+
- Focused live smokes verified exact-output, Cursor-native file read, pi bridge `pi__read`, and corrected footer context percentages after split tool turns.
|
|
22
|
+
- `npm run verify`, `npm pack --dry-run`, and `npm run smoke:platform:all` pass locally; release-review subagent returned no findings.
|
|
23
|
+
|
|
24
|
+
## 0.1.52 - 2026-06-28
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Serialize Cursor provider turns per pi session scope so parallel same-session Cursor model evaluations cannot supersede each other's SDK session agent or drain the wrong live run. Queued aborts now return an aborted stream without creating a Cursor SDK agent or leaking the turn queue.
|
|
29
|
+
|
|
30
|
+
### Validation
|
|
31
|
+
|
|
32
|
+
- `npm run check:platform-smoke`, `npm run typecheck`, `npm test` (796 tests), and `npm publish --dry-run` pass locally.
|
|
33
|
+
- `npm run smoke:platform:all` passes on macOS, Ubuntu, and Windows native with Cursor auth. Artifact index: `.artifacts/platform-smoke/latest.json`.
|
|
34
|
+
- Subagent review/fix loop and thermo-nuclear release review completed with final `NO FINDINGS`.
|
|
35
|
+
|
|
5
36
|
## 0.1.51 - 2026-06-25
|
|
6
37
|
|
|
7
38
|
### Changed
|
package/README.md
CHANGED
|
@@ -317,7 +317,7 @@ PI_CURSOR_TOOL_MANIFEST=0 pi --model cursor/composer-2-5
|
|
|
317
317
|
PI_CURSOR_PI_TOOL_BRIDGE_DEBUG=1 pi --model cursor/composer-2-5
|
|
318
318
|
```
|
|
319
319
|
|
|
320
|
-
On bootstrap sends, a compact **callable tool surfaces** block is injected into the Cursor prompt by default
|
|
320
|
+
On bootstrap sends, a compact **callable tool surfaces** block is injected into the Cursor prompt by default. It reminds the model that Cursor host/configured MCP tools are controlled by Cursor, while pi tool toggles only affect pi tools/bridge exposure; when bridge tools are exposed, it lists the current `pi__*` names. Disable with `PI_CURSOR_TOOL_MANIFEST=0`.
|
|
321
321
|
|
|
322
322
|
`PI_CURSOR_PI_TOOL_BRIDGE=0` is the supported rollback flag and disables the bridge entirely. The bridge also treats `false`, `off`, `none`, `no`, and `disabled` as off; `1`, `true`, `on`, `yes`, and `enabled` as on. `PI_CURSOR_EXPOSE_BUILTIN_TOOLS=1` opts in to exposing overlapping pi tool names that Cursor already has native equivalents for. The installed Cursor SDK uses a 60-second MCP protocol default with no public per-server timeout option. pi-cursor-sdk overrides that seam in two directions by default: MCP `callTool` requests are extended to 3600 seconds for long-running local MCP tools (including the pi bridge and configured Cursor MCP servers), and known MCP initialize/listTools requests on first send are shortened to 10 seconds so unavailable configured MCP servers fail fast instead of blocking for a full minute. Unknown Cursor SDK MCP protocol timeout stacks keep the SDK default instead of being shortened. Override tool-call timeouts with `PI_CURSOR_MCP_TOOL_TIMEOUT_MS` or `PI_CURSOR_MCP_TOOL_TIMEOUT_SECONDS`, and first-send initialize/listTools timeouts with `PI_CURSOR_MCP_CONNECT_TIMEOUT_MS` or `PI_CURSOR_MCP_CONNECT_TIMEOUT_SECONDS`. `PI_CURSOR_PI_TOOL_BRIDGE_DEBUG=1` is off by default and emits typed, allowlisted, scrubbed single-line JSONL records to `process.stderr`. These records are operational diagnostics, not anonymous telemetry: they intentionally include tool names, safe correlation IDs, bridge run state, exposed pi↔MCP name pairs, queued requests, result resolution, rejection, cancellation, and pending counts. They must not include endpoint URLs, endpoint path components, endpoint tokens, raw args/results, stdout/stderr payloads, file contents, Cursor settings output, API keys, bearer tokens, cookies, session credentials, or secrets. Do not enable or share bridge debug logs where tool names themselves are sensitive.
|
|
323
323
|
|
|
@@ -354,7 +354,7 @@ Actual Cursor runs still need a key from `/login`, `CURSOR_API_KEY`, or `--api-k
|
|
|
354
354
|
- **AGENTS.md / CLAUDE.md are not duplicated on Cursor models when Cursor loads the same rules.** Pi discovers global and project context files (`AGENTS.md`, `CLAUDE.md`, and case variants) unless you start with `-nc`. On `cursor/*` models the extension removes only `<project_instructions>` blocks that overlap Cursor `settingSources` via the `before_agent_start` hook: `user` for `~/.pi/agent/AGENTS.md`, `project` for repo/parent `AGENTS.md` and `CLAUDE.md` (verified Cursor behavior: local agents load project `AGENTS.md` and `CLAUDE.md` alongside Cursor rules). `~/.pi/agent/CLAUDE.md` is not stripped (Cursor user rules use `~/.claude/CLAUDE.md`, not pi's agent dir). With `PI_CURSOR_SETTING_SOURCES=none` or `plugins`-only, pi context is left intact. Set `PI_CURSOR_PRESERVE_PI_AGENTS_MD=1` to keep duplicate injection.
|
|
355
355
|
- **Max Mode is not a manual pi variant.** Cursor's SDK may enable Max Mode automatically for models that require it. This extension only advertises exact context-window variants that the SDK catalog exposes and otherwise uses conservative SDK-derived default/non-Max context windows.
|
|
356
356
|
- **Output token limits are conservative.** Cursor SDK model metadata does not currently expose output token limits directly.
|
|
357
|
-
- **Token usage
|
|
357
|
+
- **Token usage uses Cursor SDK data when safely attributable.** For turns with in-time SDK usage, pi records the latest per-turn `inputTokens`, `outputTokens`, `cacheReadTokens`, and `cacheWriteTokens`; SDK-backed `totalTokens` stays `inputTokens + outputTokens` so cache reads do not inflate context occupancy. If the SDK reports no usage in time, the extension falls back to local `input/output` activity estimates while setting `totalTokens` to the current replayable context estimate so the footer/compaction percentage does not collapse after split tool turns. Later usage for that live run is ignored rather than risk applying stale usage to the wrong pi turn. Cursor SDK cost is not exposed, so pi cost remains zero/absent.
|
|
358
358
|
|
|
359
359
|
## Troubleshooting
|
|
360
360
|
|
|
@@ -90,7 +90,7 @@ Pass criteria:
|
|
|
90
90
|
- Exit code is `0`.
|
|
91
91
|
- stdout contains `PI_CURSOR_SMOKE_OK`.
|
|
92
92
|
- stderr is empty or contains only expected non-secret diagnostics for the specific test.
|
|
93
|
-
- The persisted JSONL has exactly one assistant message with non-negative usage fields and `cacheRead/cacheWrite
|
|
93
|
+
- The persisted JSONL has exactly one assistant message with non-negative usage fields and non-negative `cacheRead/cacheWrite`.
|
|
94
94
|
|
|
95
95
|
## 2. Default setting-source startup noise check
|
|
96
96
|
|
|
@@ -239,7 +239,7 @@ Pass criteria:
|
|
|
239
239
|
- Diagnostics include `run_created`, `tools_exposed`, two `request_queued`, two `request_resolved`, and `run_disposed`.
|
|
240
240
|
- The missing-file request has `isError: true`.
|
|
241
241
|
- Persisted JSONL contains real pi tool calls named `read`, matching `toolResult` messages, and final assistant output.
|
|
242
|
-
- Later assistant usage counts consumed tool-result input; no assistant usage has negative values
|
|
242
|
+
- Later assistant usage counts consumed tool-result input; no assistant usage has negative values.
|
|
243
243
|
|
|
244
244
|
## 7. Native replay cards without the pi bridge
|
|
245
245
|
|
|
@@ -346,7 +346,7 @@ Script-enforced pass criteria:
|
|
|
346
346
|
- Every scanned JSONL file contains at least one assistant message.
|
|
347
347
|
- Every assistant message has usage metadata.
|
|
348
348
|
- Assistant usage `input`, `output`, and `totalTokens` are non-negative numbers.
|
|
349
|
-
- Assistant usage `cacheRead` and `cacheWrite` are
|
|
349
|
+
- Assistant usage `cacheRead` and `cacheWrite` are non-negative when present.
|
|
350
350
|
|
|
351
351
|
Additional manual usage checks for provider/accounting changes:
|
|
352
352
|
|
|
@@ -31,7 +31,7 @@ Current implementation notes:
|
|
|
31
31
|
- This repo does not provide a generic desktop-automation, browser-driver, or CDP recipe. Provider docs should describe pi-cursor-sdk's Cursor provider/bridge contract only.
|
|
32
32
|
- Cursor internal tool activity is recorded from SDK events and scrubbed. Maintainer reference for all 16 `@cursor/sdk@1.0.22` `ToolType` values, runtime alias normalization, and intentional mapping/fallback rules: [Cursor native tool replay — SDK ToolType replay matrix](./cursor-native-tool-replay.md#sdk-tooltype-replay-matrix) (official SDK docs: https://cursor.com/docs/sdk/typescript). In TUI sessions and structured JSON/RPC modes, supported completed `read`, `bash`, `grep`, `find`, `ls`, `edit`, `write`, diagnostics, delete, todo/plan, task, image generation, MCP, semantic search, and screen recording activity is replayed through pi's native tool-call rendering path with recorded Cursor results, so users and JSON/RPC consumers can see native-looking cards/events without rerunning Cursor's reads/shell commands/file edits. Cursor `glob` activity is replayed through native `find` cards. Cursor write activity is replayed through native-looking `write` cards, and Cursor StrReplace/edit activity uses native-looking `edit` only when recorded arguments truthfully satisfy pi's `edit` schema; path-only Cursor edit and notebook edit replay falls back to neutral Cursor activity before pi validation. Diagnostics, delete, todos/plans, task/subagent, image, and MCP activity use neutral Cursor activity cards with pi's default success/error shell. Cursor SDK `task` activity is labeled **Cursor subagent** by default because it represents Cursor-spawned child-agent work; the card summary includes description plus subagent kind/model/short ID when Cursor reports them, and `PI_CURSOR_TASK_PRESENTATION=task` restores the older **Cursor task** wording for comparison. This is visibility over Cursor SDK task events, not a native pi subagent session: pi shows start/final output plus any `conversationSteps` tool-call summaries Cursor returns, but cannot show a live nested read/shell/MCP trail when the SDK only returns final subagent text. Neutral Cursor activity calls include `activityTitle` and, when available, `activitySummary` so partial/collapsed cards preserve identity such as `Cursor plan`, `Cursor todos`, `Cursor subagent`, `Cursor MCP`, or `Cursor edit`. For long-running or externally meaningful Cursor tools (`task`, `shell`, `mcp`, `generateImage`, `recordScreen`, `semSearch`, web search/fetch, plan/todo), the provider may surface one low-noise deferred in-progress thinking line such as `Cursor MCP: external_search` from bounded, scrubbed SDK args; fast local tools (`read`, `grep`, `glob`, and similar) skip lifecycle lines when completion follows immediately, and pi bridge MCP calls are excluded because pi already shows real pi tool execution ([lifecycle visibility](./cursor-native-tool-replay.md#low-noise-tool-lifecycle-visibility)). Replay-only tools display recorded Cursor results, normalize workspace-local paths/diff headers for display, use pi diff colors for edit previews and path-inferred syntax highlighting for write previews, and fail closed if called without a recorded result. Native replay wrappers are registered only for tool names not already owned by another extension; conflicting tools use the bounded scrubbed transcript fallback. Cursor workflow tools such as mode/task/todo/plan activity are not pi workflow controls; reported todo/plan events are displayed as Cursor activity only. Plan/todo replay cards can be followed by Cursor's final plan text, selected from `run.wait().result` when Cursor provides one and trimmed against already-emitted text. Started Cursor SDK tool calls that never receive a completion event are surfaced with bounded user-visible labels/traces (neutral activity cards when native replay routing allows, otherwise the same inactive or transcript trace fallbacks used for completed replay) instead of being silently discarded when the run failed/aborted, produced no assistant text, or involved external/side-effectful tools; incomplete fast local discovery starts (`read`, `grep`, `glob`, `ls`) remain maintainer-debug-only after successful text-producing runs so stale SDK start events do not create red post-answer cards. Explicit failures remain visible when Cursor reports them through completed tool calls or step results. Pi bridge MCP starts remain excluded from duplicate incomplete Cursor cards because pi already shows real pi tool execution. `PI_CURSOR_NATIVE_TOOL_DISPLAY=0` disables native replay, and `PI_CURSOR_REGISTER_NATIVE_TOOLS=0` is a registration-only opt-out that keeps the transcript fallback without shadowing pi tool names. When bridge or native replay cards are emitted, the provider mirrors Codex's turn shape as Cursor SDK activity arrives: assistant `toolUse`, pi `toolResult`s, live post-tool Cursor thinking/text, any later tool batches as further `toolUse` turns, then Cursor's final assistant answer. For shell replay, completed `stdout` / `stderr` are primary; unambiguous `shell-output-delta` data is also shown as bounded live progress while one shell call is active and used as display-only fallback for empty successful shell completions, while overlapping shell calls drop ambiguous deltas instead of guessing. Print mode keeps bounded scrubbed transcript output instead, preserving `pi -p` assistant text output. Cursor text deltas stream live when no live-run turn split is active.
|
|
33
33
|
- Cursor native replay uses one neutral replay tool name, `cursor`, plus native-compatible card names when renderer-compatible (`read`, `bash`, `grep`, `find`, `ls`, `edit`, `write`). Neutral replay identity lives in `activityTitle`, `activitySummary`, and typed replay details, not in extra registered tool names. Bridge MCP names such as `pi__sem_reindex` are MCP-only; pi session output uses real pi tool names.
|
|
34
|
-
- Cursor SDK usage events
|
|
34
|
+
- Cursor SDK usage events are used when the SDK reports them before the corresponding pi turn is emitted. For each SDK-attributed assistant turn, `usage.input`, `usage.output`, `usage.cacheRead`, and `usage.cacheWrite` come from the latest per-turn SDK usage; `usage.totalTokens = latestTurn.inputTokens + latestTurn.outputTokens` so cache reads do not inflate context occupancy. Cumulative `RunResult.usage` is never used for per-message occupancy. If the SDK reports no usage in time, the provider falls back to local `input/output` activity estimates while setting `usage.totalTokens` to the current replayable context estimate so footer/compaction context does not collapse after split tool turns; after a split live-run turn times out waiting for SDK usage, later SDK usage for that live run is ignored rather than risk applying stale usage to the wrong pi turn. Cursor SDK cost is unavailable, so cost remains absent/zero. `src/cursor-usage-accounting.ts` owns this policy.
|
|
35
35
|
- Audit observation, 2026-05-19, superseded by the 2026-05-21 replay pass and #68 incomplete visibility, then narrowed by the 2026-05-26 fast-local suppression: a missing-file read with Composer 2.5 emitted `tool-call-started` for Cursor `read`, then streamed final text `Error: File not found`, but did not emit `tool-call-completed` or an `onStep` `toolCall` error result. Leftover external/side-effectful started calls are surfaced at run completion through the same native replay routing as completed tools (activity cards when allowed, otherwise inactive/transcript traces), while fast local discovery starts are debug-only after a successful text-producing run. Cursor-reported completed/step errors remain visible.
|
|
36
36
|
- Maintainer visual verification for replay-card changes should follow [Cursor Native Tool Visual Audit Workflow](./cursor-native-tool-visual-audit.md): offscreen PTY-driven pi run, xterm.js/Playwright screenshot rendering, and JSONL inspection before accepting commits or PRs.
|
|
37
37
|
- Cursor provider/runtime releases must pass the [Platform Smoke Gate](./platform-smoke.md): `npm run smoke:platform:doctor && npm run smoke:platform:all`. Use [Cursor Live Smoke Checklist](./cursor-live-smoke-checklist.md) only for focused inner-loop/debug runs with real `pi --approve -e . --cursor-no-fast --model cursor/composer-2-5` invocations, manual observation, temporary session dirs, diagnostics scans, and persisted JSONL inspection. See [Cursor testing lessons](./cursor-testing-lessons.md) for auth.json seeding, isolated smoke harnesses, and replay JSONL scans. Assume every runtime surface is in scope.
|
|
@@ -182,7 +182,7 @@ Lifecycle rules:
|
|
|
182
182
|
|
|
183
183
|
As Cursor SDK tool completions arrive, the extension mirrors native Codex ordering by ending a tool-use turn, letting pi render the recorded tool results, then continuing with live post-tool Cursor thinking/text, later Cursor tool batches, or Cursor's final answer as the next assistant turn. For plan-mode runs, neutral Cursor plan/todo cards can therefore appear before the final Cursor plan text.
|
|
184
184
|
|
|
185
|
-
Bridged pi tool calls follow the same visible pi `toolUse` turn shape, but they are real pi tool executions rather than replayed Cursor results. Usage accounting
|
|
185
|
+
Bridged pi tool calls follow the same visible pi `toolUse` turn shape, but they are real pi tool executions rather than replayed Cursor results. Usage accounting uses per-turn Cursor SDK usage when available before the corresponding pi turn is emitted, including cache read/write fields; SDK-backed `usage.totalTokens` remains latest-turn input plus output so cache reads do not inflate context occupancy. If the SDK reports no usage in time, the provider falls back to local `input/output` activity estimates with `usage.totalTokens` set to the current replayable context estimate, then ignores later usage for that live run rather than risk applying stale usage to the wrong pi turn.
|
|
186
186
|
|
|
187
187
|
For shell replay, completed `stdout` / `stderr` remain the primary source. While exactly one shell call is active, the provider also emits a bounded scrubbed preview of the first few `shell-output-delta` stdout/stderr chunks so long-running commands show visible progress before completion. If a successful completed shell result is empty, the replay card uses unambiguous buffered delta data as display-only fallback data. Overlapping shell calls make delta attribution ambiguous, so those fallback/progress deltas are dropped rather than guessed. `(no output)` is kept only when no completed output or safe delta fallback is available.
|
|
188
188
|
|
|
@@ -17,7 +17,7 @@ Pi CLI tool toggles apply at the pi tool-registry boundary. `--no-tools`, `--too
|
|
|
17
17
|
## Discoverability
|
|
18
18
|
|
|
19
19
|
- **MCP `listTools`** (and pi's MCP catalog when present) lists **MCP servers only** — for example `pi_tools` with `pi__cursor_ask_question`. It does **not** enumerate Cursor SDK host tools such as `Read` or `Shell`.
|
|
20
|
-
- **Bootstrap prompts** include a short **Cursor SDK tool boundary** block plus a compact **callable tool surfaces** manifest by default (disable manifest with `PI_CURSOR_TOOL_MANIFEST=0`). The manifest
|
|
20
|
+
- **Bootstrap prompts** include a short **Cursor SDK tool boundary** block plus a compact **callable tool surfaces** manifest by default (disable manifest with `PI_CURSOR_TOOL_MANIFEST=0`). The manifest reminds the model that Cursor host/configured MCP tools are controlled by Cursor, while pi tool toggles only affect pi tools/bridge exposure; when bridge tools are exposed, it lists the current `pi__*` names. MCP `listTools` entries for bridged pi tools point back to the bootstrap prompt instead of repeating the full contract.
|
|
21
21
|
- **Incremental prompts** omit the full boundary block but keep a short tail guard (including an explicit shell `cd` hint); the session agent retains prior bootstrap context.
|
|
22
22
|
- **In-session debug:** `/cursor-tools` prints bridge enablement, manifest enablement, effective `PI_CURSOR_SETTING_SOURCES`, and the current callable-surface snapshot.
|
|
23
23
|
|
package/docs/platform-smoke.md
CHANGED
|
@@ -481,8 +481,7 @@ Required JSONL evidence:
|
|
|
481
481
|
- successful native `find` result proving `src/cursor-provider.ts` was enumerated;
|
|
482
482
|
- failed shell result with `isError=true` and `native shell failure` output;
|
|
483
483
|
- final assistant message's last non-empty `text` part contains `NATIVE_MATRIX_OK`;
|
|
484
|
-
- assistant usage fields are non-negative
|
|
485
|
-
- `cacheRead=0` and `cacheWrite=0`.
|
|
484
|
+
- assistant usage fields are non-negative.
|
|
486
485
|
|
|
487
486
|
### `cursor-bridge-visual-matrix`
|
|
488
487
|
|
|
@@ -556,8 +555,7 @@ Required JSONL evidence:
|
|
|
556
555
|
- real pi tool call named `read`, failure;
|
|
557
556
|
- real pi tool call named `bash`, success;
|
|
558
557
|
- final assistant message's last non-empty `text` part contains `BRIDGE_MATRIX_OK`;
|
|
559
|
-
- assistant usage fields are non-negative
|
|
560
|
-
- `cacheRead=0` and `cacheWrite=0`.
|
|
558
|
+
- assistant usage fields are non-negative.
|
|
561
559
|
|
|
562
560
|
### `cursor-abort-cleanup`
|
|
563
561
|
|
package/package.json
CHANGED
|
@@ -581,7 +581,6 @@ async function main() {
|
|
|
581
581
|
try {
|
|
582
582
|
console.log(`[platform-live] suite=${args.suite} target=${args.target} model=${args.model}`);
|
|
583
583
|
copyFixtureWorkspace(workspaceDir);
|
|
584
|
-
const piCli = resolvePiCli();
|
|
585
584
|
const piEnv = { ...process.env, PI_CODING_AGENT_DIR: agentDir, PI_OFFLINE: "1" };
|
|
586
585
|
let installPath = `./node_modules/${packageName}`;
|
|
587
586
|
if (args.prepDir) {
|
|
@@ -603,6 +602,7 @@ async function main() {
|
|
|
603
602
|
const npmInstallPacked = runLogged(logDir, "workspace-npm-install-packed", commandName("npm"), ["install", "--no-save", tarballPath], { cwd: workspaceDir, timeout: 180_000 });
|
|
604
603
|
requireOk(npmInstallPacked, "workspace npm install packed tarball");
|
|
605
604
|
}
|
|
605
|
+
const piCli = resolvePiCli();
|
|
606
606
|
const install = runLogged(logDir, "pi-install", piCli, ["install", "--approve", "-l", installPath], { cwd: workspaceDir, env: piEnv, timeout: 120_000 });
|
|
607
607
|
requireOk(install, "pi install --approve packed package directory");
|
|
608
608
|
const list = runLogged(logDir, "pi-list", piCli, ["list", "--approve"], { cwd: workspaceDir, env: piEnv, timeout: 60_000 });
|
|
@@ -638,7 +638,6 @@ async function executeLiveSuite(config, targetName, suiteName, suiteDir, slug, l
|
|
|
638
638
|
{ id: "provider-debug-artifacts", fn: () => providerDebugFiles.some((file) => file.endsWith("session.json")) && providerDebugFiles.length > 1 },
|
|
639
639
|
...(suiteName !== "cursor-abort-cleanup" ? [
|
|
640
640
|
{ id: "jsonl-usage-non-negative", fn: () => usageChecks.seen && usageChecks.nonNegative },
|
|
641
|
-
{ id: "jsonl-cache-zero", fn: () => usageChecks.seen && usageChecks.cacheZero },
|
|
642
641
|
] : []),
|
|
643
642
|
{ id: "final-marker", fn: () => scenario?.finalMarker ? status?.finalMarkerObserved === true : status?.ok === true },
|
|
644
643
|
...(suiteName === "cursor-abort-cleanup" ? [{ id: "abort-no-successful-answer", fn: () => !hasAbortSuccessClaim(jsonlRaw) }] : []),
|
|
@@ -798,7 +797,6 @@ function collectBridgeDiagnostics(terminalText) {
|
|
|
798
797
|
function collectUsageChecks(jsonlRaw) {
|
|
799
798
|
let seen = false;
|
|
800
799
|
let nonNegative = true;
|
|
801
|
-
let cacheZero = true;
|
|
802
800
|
for (const line of jsonlRaw.split(/\r?\n/)) {
|
|
803
801
|
if (!line.trim()) continue;
|
|
804
802
|
let event;
|
|
@@ -809,10 +807,10 @@ function collectUsageChecks(jsonlRaw) {
|
|
|
809
807
|
for (const value of Object.values(usage)) {
|
|
810
808
|
if (typeof value === "number" && value < 0) nonNegative = false;
|
|
811
809
|
}
|
|
812
|
-
if (typeof usage.cacheRead === "number" && usage.cacheRead
|
|
813
|
-
if (typeof usage.cacheWrite === "number" && usage.cacheWrite
|
|
810
|
+
if (typeof usage.cacheRead === "number" && usage.cacheRead < 0) nonNegative = false;
|
|
811
|
+
if (typeof usage.cacheWrite === "number" && usage.cacheWrite < 0) nonNegative = false;
|
|
814
812
|
}
|
|
815
|
-
return { seen, nonNegative
|
|
813
|
+
return { seen, nonNegative };
|
|
816
814
|
}
|
|
817
815
|
|
|
818
816
|
export function hasAbortSuccessClaim(jsonlRaw) {
|
|
@@ -37,8 +37,7 @@ Enforced invariants (default mode):
|
|
|
37
37
|
- each scanned JSONL file contains parseable JSONL records
|
|
38
38
|
- each scanned JSONL file contains at least one persisted assistant message
|
|
39
39
|
- every persisted assistant message has usage metadata
|
|
40
|
-
- assistant usage input/output/totalTokens are non-negative numbers
|
|
41
|
-
- assistant usage cacheRead/cacheWrite are exactly 0
|
|
40
|
+
- assistant usage input/output/totalTokens/cacheRead/cacheWrite are non-negative numbers
|
|
42
41
|
|
|
43
42
|
Replay error scan (--replay-errors / --replay-errors-only):
|
|
44
43
|
- no persisted error toolResult or error assistant message contains "Tool grep/cursor/find/ls not found"
|
|
@@ -79,8 +78,8 @@ function isBadUsage(usage) {
|
|
|
79
78
|
!isNonNegativeNumber(usage.input) ||
|
|
80
79
|
!isNonNegativeNumber(usage.output) ||
|
|
81
80
|
!isNonNegativeNumber(usage.totalTokens) ||
|
|
82
|
-
usage.cacheRead
|
|
83
|
-
usage.cacheWrite
|
|
81
|
+
!isNonNegativeNumber(usage.cacheRead) ||
|
|
82
|
+
!isNonNegativeNumber(usage.cacheWrite)
|
|
84
83
|
);
|
|
85
84
|
}
|
|
86
85
|
|
package/src/context.ts
CHANGED
|
@@ -16,54 +16,66 @@ export interface CursorPromptOptions {
|
|
|
16
16
|
agentMode?: AgentModeOption;
|
|
17
17
|
/** Compact callable-surface summary; included on bootstrap prompts when set. */
|
|
18
18
|
toolManifest?: string;
|
|
19
|
+
includePiBridgeGuidance?: boolean;
|
|
20
|
+
includePiAskQuestionGuidance?: boolean;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export const CURSOR_APPROX_CHARS_PER_TOKEN = 4;
|
|
22
24
|
export const CURSOR_IMAGE_TOKEN_ESTIMATE = 1200;
|
|
23
25
|
const SECTION_SEPARATOR = "\n\n";
|
|
24
26
|
|
|
25
|
-
export function getCursorPlanModeToolGuidanceText(
|
|
27
|
+
export function getCursorPlanModeToolGuidanceText(
|
|
28
|
+
agentMode: AgentModeOption | undefined,
|
|
29
|
+
options: { includePiBridgeGuidance?: boolean } = {},
|
|
30
|
+
): string | undefined {
|
|
26
31
|
if (agentMode !== "plan") return undefined;
|
|
27
32
|
return [
|
|
28
33
|
"Cursor SDK mode is plan for this run. In pi-cursor-sdk, plan mode may still use available Cursor SDK/MCP tools for inspection when needed.",
|
|
29
34
|
"Safe/read-only shell commands that inspect or print information are allowed when Cursor chooses to call Shell; do not say Shell is blocked by plan mode and then call it anyway.",
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
options.includePiBridgeGuidance === false
|
|
36
|
+
? undefined
|
|
37
|
+
: "Exposed pi__* bridge tools are also callable in plan mode when the user asks for them or they are needed to answer.",
|
|
38
|
+
].filter((line): line is string => line !== undefined).join("\n");
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
export function getCursorToolTailGuardText(
|
|
35
|
-
options: Pick<CursorPromptOptions, "agentMode"> & { includePlanModeGuidance?: boolean } = {},
|
|
42
|
+
options: Pick<CursorPromptOptions, "agentMode"> & { includePlanModeGuidance?: boolean; includePiBridgeGuidance?: boolean } = {},
|
|
36
43
|
): string {
|
|
37
44
|
return [
|
|
38
|
-
"Shell: use
|
|
39
|
-
options.includePlanModeGuidance === false
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
"Shell: use explicit `cd` to repo path for project commands; session cwd may differ from tool args.",
|
|
46
|
+
options.includePlanModeGuidance === false
|
|
47
|
+
? undefined
|
|
48
|
+
: getCursorPlanModeToolGuidanceText(options.agentMode, { includePiBridgeGuidance: options.includePiBridgeGuidance }),
|
|
49
|
+
"Exact-output requests: output exactly the requested text; no preamble or checks unless asked.",
|
|
50
|
+
"Tools: call available Cursor SDK/MCP tools; never print tool cards as assistant text.",
|
|
42
51
|
].filter((line): line is string => line !== undefined).join("\n");
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
function getCursorToolBoundaryText(
|
|
54
|
+
function getCursorToolBoundaryText(
|
|
55
|
+
options: Pick<CursorPromptOptions, "agentMode" | "includePiAskQuestionGuidance"> & { hasToolManifest?: boolean; includePiBridgeGuidance?: boolean } = {},
|
|
56
|
+
): string {
|
|
57
|
+
const includePiBridgeGuidance = options.includePiBridgeGuidance !== false;
|
|
58
|
+
const includePiAskQuestionGuidance = includePiBridgeGuidance && options.includePiAskQuestionGuidance !== false;
|
|
46
59
|
const lines = [
|
|
47
60
|
"Cursor SDK tool boundary:",
|
|
48
|
-
"Call only tools exposed
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
"Call only Cursor SDK/MCP tools exposed in this run; pi history names, replay labels, and transcript names are not callable.",
|
|
62
|
+
includePiBridgeGuidance
|
|
63
|
+
? "For exposed pi bridge tools, call pi__* MCP names, not pi card/history names."
|
|
64
|
+
: undefined,
|
|
65
|
+
"Do not claim pi-side or WebSearch/WebFetch tools unless Cursor ran an equivalent tool.",
|
|
66
|
+
includePiAskQuestionGuidance ? "Use pi__cursor_ask_question for material choices if exposed." : undefined,
|
|
67
|
+
getCursorPlanModeToolGuidanceText(options.agentMode, { includePiBridgeGuidance }),
|
|
68
|
+
"Images: only latest user images are sent; ask to reattach prior images.",
|
|
54
69
|
].filter((line): line is string => line !== undefined);
|
|
55
70
|
if (options.hasToolManifest) {
|
|
56
|
-
lines.push("See callable
|
|
71
|
+
lines.push("See callable surfaces below.");
|
|
57
72
|
}
|
|
58
73
|
return lines.join("\n");
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
function getCursorBootstrapTailSections(options: Pick<CursorPromptOptions, "agentMode"> = {}): string[] {
|
|
62
77
|
return [
|
|
63
|
-
|
|
64
|
-
"Answer the latest user request above using Cursor SDK capabilities only. Do not list, promise, or call pi-only tools from the system prompt as if they were available.",
|
|
65
|
-
"If web research is requested, do not claim it unless a Cursor web/search/browser/MCP tool ran.",
|
|
66
|
-
].join("\n"),
|
|
78
|
+
"Answer the latest user request above using the instructions and Cursor SDK capabilities available in this run.",
|
|
67
79
|
getCursorToolTailGuardText({ ...options, includePlanModeGuidance: false }),
|
|
68
80
|
];
|
|
69
81
|
}
|
|
@@ -389,7 +401,12 @@ export function buildCursorIncrementalPrompt(context: Context, options: CursorPr
|
|
|
389
401
|
}
|
|
390
402
|
|
|
391
403
|
export function buildCursorPrompt(context: Context, options: CursorPromptOptions = {}): CursorPrompt {
|
|
392
|
-
const sectionsBeforeMessages: string[] = [getCursorToolBoundaryText({
|
|
404
|
+
const sectionsBeforeMessages: string[] = [getCursorToolBoundaryText({
|
|
405
|
+
agentMode: options.agentMode,
|
|
406
|
+
hasToolManifest: Boolean(options.toolManifest),
|
|
407
|
+
includePiBridgeGuidance: options.includePiBridgeGuidance,
|
|
408
|
+
includePiAskQuestionGuidance: options.includePiAskQuestionGuidance,
|
|
409
|
+
})];
|
|
393
410
|
if (options.toolManifest) {
|
|
394
411
|
sectionsBeforeMessages.push(options.toolManifest);
|
|
395
412
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { Context, Message, ToolResultMessage } from "@earendil-works/pi-ai/compat";
|
|
2
2
|
import { CURSOR_APPROX_CHARS_PER_TOKEN, estimateCursorPromptMessageTokens } from "./context.js";
|
|
3
|
+
import type { CursorSdkTurnUsage } from "./cursor-usage-accounting.js";
|
|
3
4
|
|
|
4
5
|
export interface CursorLiveRunAccountingState {
|
|
5
6
|
promptInputTokens: number;
|
|
6
7
|
promptInputTokensReported: boolean;
|
|
7
8
|
consumedToolResultIds: ReadonlySet<string>;
|
|
9
|
+
sdkTurnEnded: boolean;
|
|
10
|
+
sdkTurnUsage?: CursorSdkTurnUsage;
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
export interface CursorLiveToolResultConsumption {
|
|
@@ -19,9 +22,25 @@ export function createCursorLiveRunAccountingState(promptInputTokens: number): C
|
|
|
19
22
|
promptInputTokens,
|
|
20
23
|
promptInputTokensReported: false,
|
|
21
24
|
consumedToolResultIds: new Set(),
|
|
25
|
+
sdkTurnEnded: false,
|
|
22
26
|
};
|
|
23
27
|
}
|
|
24
28
|
|
|
29
|
+
export function recordCursorLiveSdkTurnEnded(
|
|
30
|
+
state: CursorLiveRunAccountingState,
|
|
31
|
+
sdkTurnUsage?: CursorSdkTurnUsage,
|
|
32
|
+
): CursorLiveRunAccountingState {
|
|
33
|
+
return { ...state, sdkTurnEnded: true, sdkTurnUsage };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function takeCursorLiveSdkTurnUsage(state: CursorLiveRunAccountingState): {
|
|
37
|
+
state: CursorLiveRunAccountingState;
|
|
38
|
+
sdkTurnUsage?: CursorSdkTurnUsage;
|
|
39
|
+
} {
|
|
40
|
+
const { sdkTurnUsage, ...nextState } = state;
|
|
41
|
+
return { state: { ...nextState, sdkTurnEnded: false }, sdkTurnUsage };
|
|
42
|
+
}
|
|
43
|
+
|
|
25
44
|
function asToolResultMessage(message: Message): ToolResultMessage | undefined {
|
|
26
45
|
return message.role === "toolResult" ? message : undefined;
|
|
27
46
|
}
|
|
@@ -3,10 +3,13 @@ import type { SDKAgent } from "@cursor/sdk";
|
|
|
3
3
|
import {
|
|
4
4
|
consumeCursorLiveToolResults,
|
|
5
5
|
createCursorLiveRunAccountingState,
|
|
6
|
+
recordCursorLiveSdkTurnEnded,
|
|
7
|
+
takeCursorLiveSdkTurnUsage,
|
|
6
8
|
takeCursorLiveTurnInputTokens,
|
|
7
9
|
type CursorLiveRunAccountingState,
|
|
8
10
|
type CursorLiveToolResultConsumption,
|
|
9
11
|
} from "./cursor-live-run-accounting.js";
|
|
12
|
+
import type { CursorSdkTurnUsage } from "./cursor-usage-accounting.js";
|
|
10
13
|
import type { CursorNativeToolDisplayItem } from "./cursor-native-tool-display-state.js";
|
|
11
14
|
import type { CursorPiBridgeToolRequest, CursorPiToolBridgeRun } from "./cursor-pi-tool-bridge.js";
|
|
12
15
|
import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
@@ -38,6 +41,7 @@ export interface CursorLiveRun {
|
|
|
38
41
|
sessionBridgeRun?: CursorPiToolBridgeRun;
|
|
39
42
|
sessionAgentScopeKey: string;
|
|
40
43
|
sdkRun?: CursorLiveSdkRun;
|
|
44
|
+
ignoreFutureSdkTurnUsage?: boolean;
|
|
41
45
|
accounting: CursorLiveRunAccountingState;
|
|
42
46
|
pendingEvents: CursorLiveQueuedEvent[];
|
|
43
47
|
textDeltas: string[];
|
|
@@ -77,6 +81,9 @@ export interface CursorLiveRunCoordinator {
|
|
|
77
81
|
markFinished(run: CursorLiveRun, finalText: string): void;
|
|
78
82
|
markCancelled(run: CursorLiveRun, abortMessage?: string): void;
|
|
79
83
|
markError(run: CursorLiveRun, errorMessage: string): void;
|
|
84
|
+
recordSdkTurnEnded(run: CursorLiveRun, usage?: CursorSdkTurnUsage): void;
|
|
85
|
+
ignoreFutureSdkTurnUsage(run: CursorLiveRun): void;
|
|
86
|
+
hasSdkTurnEnded(run: CursorLiveRun): boolean;
|
|
80
87
|
queueEvent(run: CursorLiveRun, event: CursorLiveQueuedEvent): void;
|
|
81
88
|
peekEvent(run: CursorLiveRun): CursorLiveQueuedEvent | undefined;
|
|
82
89
|
shiftEvent(run: CursorLiveRun): CursorLiveQueuedEvent | undefined;
|
|
@@ -84,6 +91,7 @@ export interface CursorLiveRunCoordinator {
|
|
|
84
91
|
collectBridgeToolBatch(run: CursorLiveRun): CursorPiBridgeToolRequest[];
|
|
85
92
|
consumeToolResults(run: CursorLiveRun, context: Context, getReplayId: CursorReplayIdResolver): CursorLiveToolResultConsumption;
|
|
86
93
|
takeTurnInputTokens(run: CursorLiveRun, toolResultInputTokens: number): number;
|
|
94
|
+
takeSdkTurnUsage(run: CursorLiveRun): CursorSdkTurnUsage | undefined;
|
|
87
95
|
getPendingFromContext(context: Context, getReplayId: CursorReplayIdResolver): CursorLiveRun | undefined;
|
|
88
96
|
getActiveForScope(scopeKey?: string): CursorLiveRun | undefined;
|
|
89
97
|
isReady(run: CursorLiveRun): boolean;
|
|
@@ -328,6 +336,25 @@ export function createCursorLiveRunCoordinator(deps: CursorLiveRunCoordinatorDep
|
|
|
328
336
|
coordinator.requestIdleDispose(run);
|
|
329
337
|
},
|
|
330
338
|
|
|
339
|
+
recordSdkTurnEnded(run, usage): void {
|
|
340
|
+
if (run.disposed) return;
|
|
341
|
+
if (run.ignoreFutureSdkTurnUsage) {
|
|
342
|
+
run.accounting = { ...run.accounting, sdkTurnEnded: false, sdkTurnUsage: undefined };
|
|
343
|
+
notifyProgress(run);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
run.accounting = recordCursorLiveSdkTurnEnded(run.accounting, usage);
|
|
347
|
+
notifyProgress(run);
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
ignoreFutureSdkTurnUsage(run): void {
|
|
351
|
+
if (!run.disposed) run.ignoreFutureSdkTurnUsage = true;
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
hasSdkTurnEnded(run): boolean {
|
|
355
|
+
return run.accounting.sdkTurnEnded;
|
|
356
|
+
},
|
|
357
|
+
|
|
331
358
|
queueEvent(run, event): void {
|
|
332
359
|
if (run.disposed) return;
|
|
333
360
|
run.pendingEvents.push(event);
|
|
@@ -375,6 +402,12 @@ export function createCursorLiveRunCoordinator(deps: CursorLiveRunCoordinatorDep
|
|
|
375
402
|
return taken.sessionInputTokens;
|
|
376
403
|
},
|
|
377
404
|
|
|
405
|
+
takeSdkTurnUsage(run): CursorSdkTurnUsage | undefined {
|
|
406
|
+
const taken = takeCursorLiveSdkTurnUsage(run.accounting);
|
|
407
|
+
run.accounting = taken.state;
|
|
408
|
+
return taken.sdkTurnUsage;
|
|
409
|
+
},
|
|
410
|
+
|
|
378
411
|
getPendingFromContext(context, getReplayId): CursorLiveRun | undefined {
|
|
379
412
|
let index = context.messages.length - 1;
|
|
380
413
|
while (index >= 0 && context.messages[index]?.role === "user") {
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "./cursor-native-tool-display-state.js";
|
|
21
21
|
import { type CursorPiBridgeToolRequest } from "./cursor-pi-tool-bridge.js";
|
|
22
22
|
import { resetSessionCursorAgent } from "./cursor-session-agent.js";
|
|
23
|
-
import {
|
|
23
|
+
import { applyCursorUsage } from "./cursor-usage-accounting.js";
|
|
24
24
|
import { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
|
|
25
25
|
import { trimCurrentTurnAlreadyEmittedCursorText } from "./cursor-run-final-text.js";
|
|
26
26
|
import { formatCursorSdkAbortMessage, resolveCursorSdkAbortCause } from "./cursor-provider-errors.js";
|
|
@@ -105,6 +105,17 @@ export async function settleCursorLiveToolBatch(run: CursorLiveRun): Promise<voi
|
|
|
105
105
|
await scheduler.wait(75);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
async function waitForCursorLiveSdkTurnEnded(run: CursorLiveRun, signal?: AbortSignal): Promise<boolean> {
|
|
109
|
+
const deadline = Date.now() + 125;
|
|
110
|
+
while (!cursorLiveRuns.hasSdkTurnEnded(run) && !run.done && !run.disposed && !run.cancelled && !run.errorMessage) {
|
|
111
|
+
const remainingMs = deadline - Date.now();
|
|
112
|
+
if (remainingMs <= 0) return false;
|
|
113
|
+
await scheduler.wait(Math.min(25, remainingMs));
|
|
114
|
+
if (signal?.aborted) throw new CursorLiveRunAbortError();
|
|
115
|
+
}
|
|
116
|
+
return cursorLiveRuns.hasSdkTurnEnded(run);
|
|
117
|
+
}
|
|
118
|
+
|
|
108
119
|
export function flushPendingCursorLiveRunTraceEventsToStream(
|
|
109
120
|
stream: AssistantMessageEventStream,
|
|
110
121
|
partial: AssistantMessage,
|
|
@@ -184,7 +195,9 @@ function emitCursorNativeToolUseTurn(
|
|
|
184
195
|
});
|
|
185
196
|
}
|
|
186
197
|
}
|
|
187
|
-
|
|
198
|
+
applyCursorUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens), {
|
|
199
|
+
turn: cursorLiveRuns.takeSdkTurnUsage(run),
|
|
200
|
+
});
|
|
188
201
|
partial.stopReason = "toolUse";
|
|
189
202
|
stream.push({ type: "done", reason: "toolUse", message: partial });
|
|
190
203
|
cursorLiveRuns.requestIdleDispose(run);
|
|
@@ -229,7 +242,9 @@ function emitCursorBridgeToolUseTurn(
|
|
|
229
242
|
const block = partial.content[contentIndex];
|
|
230
243
|
if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
|
|
231
244
|
}
|
|
232
|
-
|
|
245
|
+
applyCursorUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens), {
|
|
246
|
+
turn: cursorLiveRuns.takeSdkTurnUsage(run),
|
|
247
|
+
});
|
|
233
248
|
partial.stopReason = "toolUse";
|
|
234
249
|
stream.push({ type: "done", reason: "toolUse", message: partial });
|
|
235
250
|
cursorLiveRuns.requestIdleDispose(run);
|
|
@@ -249,6 +264,7 @@ async function emitCursorLiveRunPendingToolUseTurn(
|
|
|
249
264
|
const eventType = cursorLiveRuns.peekEvent(run)?.type;
|
|
250
265
|
if (eventType !== "tool" && eventType !== "bridge-tool") return undefined;
|
|
251
266
|
await settleCursorLiveToolBatch(run);
|
|
267
|
+
const sdkTurnEnded = await waitForCursorLiveSdkTurnEnded(run, options.signal);
|
|
252
268
|
if (options.signal?.aborted) throw new CursorLiveRunAbortError();
|
|
253
269
|
if (eventType === "tool") {
|
|
254
270
|
const { active, inactive } = partitionNativeToolsByActiveContext(context, cursorLiveRuns.collectNativeToolBatch(run));
|
|
@@ -257,9 +273,11 @@ async function emitCursorLiveRunPendingToolUseTurn(
|
|
|
257
273
|
// Inactive-only batch: trace was emitted above; do not emit toolUse.
|
|
258
274
|
return "handled";
|
|
259
275
|
}
|
|
276
|
+
if (!sdkTurnEnded) cursorLiveRuns.ignoreFutureSdkTurnUsage(run);
|
|
260
277
|
if (options.mode === "emit") turn.emitter.closeAll();
|
|
261
278
|
emitCursorNativeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, active, debugRecorder);
|
|
262
279
|
} else {
|
|
280
|
+
if (!sdkTurnEnded) cursorLiveRuns.ignoreFutureSdkTurnUsage(run);
|
|
263
281
|
if (options.mode === "emit") turn.emitter.closeAll();
|
|
264
282
|
const requests = cursorLiveRuns.collectBridgeToolBatch(run);
|
|
265
283
|
emitCursorBridgeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, requests);
|
|
@@ -358,7 +376,9 @@ export async function drainCursorLiveRunTurn(
|
|
|
358
376
|
if (finalText) {
|
|
359
377
|
await emitTextDeltas(stream, partial, splitTextIntoReplayDeltas(finalText));
|
|
360
378
|
}
|
|
361
|
-
|
|
379
|
+
applyCursorUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens), {
|
|
380
|
+
turn: cursorLiveRuns.takeSdkTurnUsage(run),
|
|
381
|
+
});
|
|
362
382
|
partial.stopReason = "stop";
|
|
363
383
|
stream.push({ type: "done", reason: "stop", message: partial });
|
|
364
384
|
await cursorLiveRuns.release(run);
|
|
@@ -24,7 +24,7 @@ import type {
|
|
|
24
24
|
CursorProviderTurnSend,
|
|
25
25
|
CursorProviderTurnSendResult,
|
|
26
26
|
} from "./cursor-provider-turn-types.js";
|
|
27
|
-
import {
|
|
27
|
+
import { applyCursorUsage } from "./cursor-usage-accounting.js";
|
|
28
28
|
import { hasUsableText } from "./cursor-record-utils.js";
|
|
29
29
|
export type CursorTurnTerminalEvent =
|
|
30
30
|
| {
|
|
@@ -174,7 +174,9 @@ export class CursorRunFinalizer {
|
|
|
174
174
|
prepared.runtime.turnCoordinator.flushText(
|
|
175
175
|
outcome.kind === "finished" && hasUsableText(outcome.finalText) ? [outcome.finalText] : [],
|
|
176
176
|
);
|
|
177
|
-
|
|
177
|
+
applyCursorUsage(partial, model, context, prepared.meta.promptInputTokens, {
|
|
178
|
+
turn: prepared.runtime.turnCoordinator.lastSdkTurnUsage,
|
|
179
|
+
});
|
|
178
180
|
stream.push({ type: "done", reason: "stop", message: partial });
|
|
179
181
|
break;
|
|
180
182
|
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
CursorToolCompletionLedger,
|
|
33
33
|
getToolFingerprint,
|
|
34
34
|
} from "./cursor-provider-turn-tool-ledger.js";
|
|
35
|
+
import { readCursorSdkTurnUsageFromUpdate, type CursorSdkTurnUsage } from "./cursor-usage-accounting.js";
|
|
35
36
|
|
|
36
37
|
export interface CursorSdkTurnCoordinatorOptions {
|
|
37
38
|
stream: AssistantMessageEventStream;
|
|
@@ -63,6 +64,7 @@ export class CursorSdkTurnCoordinator {
|
|
|
63
64
|
private readonly displayRouter: CursorTurnDisplayRouter;
|
|
64
65
|
private readonly lifecycleEmitter: CursorToolLifecycleEmitter;
|
|
65
66
|
private readonly contentEmitter;
|
|
67
|
+
private sdkTurnUsage?: CursorSdkTurnUsage;
|
|
66
68
|
|
|
67
69
|
constructor(options: CursorSdkTurnCoordinatorOptions) {
|
|
68
70
|
this.stream = options.stream;
|
|
@@ -104,6 +106,10 @@ export class CursorSdkTurnCoordinator {
|
|
|
104
106
|
return this.displayRouter.nativeToolReplayStarted;
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
get lastSdkTurnUsage(): CursorSdkTurnUsage | undefined {
|
|
110
|
+
return this.sdkTurnUsage;
|
|
111
|
+
}
|
|
112
|
+
|
|
107
113
|
discardIncompleteStartedToolCalls(
|
|
108
114
|
outcome: IncompleteCursorToolRunOutcome = buildIncompleteCursorToolRunOutcome(),
|
|
109
115
|
): void {
|
|
@@ -141,6 +147,11 @@ export class CursorSdkTurnCoordinator {
|
|
|
141
147
|
}
|
|
142
148
|
|
|
143
149
|
handleDelta(update: InteractionUpdate): void {
|
|
150
|
+
const sdkTurnUsage = readCursorSdkTurnUsageFromUpdate(update);
|
|
151
|
+
if (sdkTurnUsage) this.sdkTurnUsage = sdkTurnUsage;
|
|
152
|
+
if (this.liveRun && (update.type === "turn-ended" || sdkTurnUsage)) {
|
|
153
|
+
cursorLiveRuns.recordSdkTurnEnded(this.liveRun, sdkTurnUsage);
|
|
154
|
+
}
|
|
144
155
|
if (update.type === "text-delta") {
|
|
145
156
|
this.textDeltas.push(update.text);
|
|
146
157
|
if (this.liveRun) {
|
|
@@ -88,8 +88,15 @@ export async function prepareCursorProviderTurn(
|
|
|
88
88
|
sessionAgentScopeKey = sessionAgentLease.scopeKey;
|
|
89
89
|
throwIfAborted();
|
|
90
90
|
|
|
91
|
+
let bridgeToolNames = new Set(sessionAgentLease.bridgeRun?.snapshot.tools.map((tool) => tool.mcpToolName) ?? []);
|
|
92
|
+
let includePiBridgeGuidance = bridgeToolNames.size > 0;
|
|
91
93
|
const buildPromptOptions = (plan: ReturnType<typeof planCursorSessionSend>) => {
|
|
92
|
-
const promptOptions = {
|
|
94
|
+
const promptOptions = {
|
|
95
|
+
...getCursorPromptOptions(model),
|
|
96
|
+
agentMode,
|
|
97
|
+
includePiBridgeGuidance,
|
|
98
|
+
includePiAskQuestionGuidance: bridgeToolNames.has("pi__cursor_ask_question"),
|
|
99
|
+
};
|
|
93
100
|
if (plan.mode !== "bootstrap" || !resolveCursorToolManifestEnabled()) {
|
|
94
101
|
return promptOptions;
|
|
95
102
|
}
|
|
@@ -98,6 +105,7 @@ export async function prepareCursorProviderTurn(
|
|
|
98
105
|
toolManifest: buildCursorToolManifestText({
|
|
99
106
|
bridgeSnapshot: sessionAgentLease.bridgeRun?.snapshot,
|
|
100
107
|
piBridgeEnabled: resolveCursorPiToolBridgeEnabled(),
|
|
108
|
+
includePiBridgeGuidance,
|
|
101
109
|
}),
|
|
102
110
|
};
|
|
103
111
|
};
|
|
@@ -108,6 +116,8 @@ export async function prepareCursorProviderTurn(
|
|
|
108
116
|
await resetSessionCursorAgent(sessionAgentScopeKey);
|
|
109
117
|
sessionAgentLease = await acquireSessionCursorAgent(sessionAgentAcquireParams);
|
|
110
118
|
sessionAgentScopeKey = sessionAgentLease.scopeKey;
|
|
119
|
+
bridgeToolNames = new Set(sessionAgentLease.bridgeRun?.snapshot.tools.map((tool) => tool.mcpToolName) ?? []);
|
|
120
|
+
includePiBridgeGuidance = bridgeToolNames.size > 0;
|
|
111
121
|
sendPlan = planCursorSessionSend(sessionAgentLease.sendState, context);
|
|
112
122
|
promptOptions = buildPromptOptions(sendPlan);
|
|
113
123
|
prompt = buildCursorSessionSendPrompt(context, promptOptions, sendPlan);
|
|
@@ -46,7 +46,6 @@ export class CursorProviderTurnRunner {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
try {
|
|
49
|
-
stream.push({ type: "start", partial });
|
|
50
49
|
this.throwIfAborted();
|
|
51
50
|
const cwd = getCursorSessionCwd();
|
|
52
51
|
this.sdkEventDebug = CursorSdkEventDebugSink.maybeCreate({
|
|
@@ -62,6 +61,7 @@ export class CursorProviderTurnRunner {
|
|
|
62
61
|
) {
|
|
63
62
|
return;
|
|
64
63
|
}
|
|
64
|
+
this.throwIfAborted();
|
|
65
65
|
|
|
66
66
|
this.resolvedApiKey = requireCursorApiKey(options);
|
|
67
67
|
prepared = await prepareCursorProviderTurn({
|
package/src/cursor-provider.ts
CHANGED
|
@@ -22,6 +22,8 @@ import { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-gu
|
|
|
22
22
|
import { sanitizeCursorProviderError } from "./cursor-provider-errors.js";
|
|
23
23
|
import { resolveCursorApiKey } from "./cursor-api-key.js";
|
|
24
24
|
import { CursorProviderTurnRunner } from "./cursor-provider-turn-runner.js";
|
|
25
|
+
import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
26
|
+
import { runExclusiveCursorSessionTurn, __testUtils as cursorSessionTurnQueueTestUtils } from "./cursor-session-turn-queue.js";
|
|
25
27
|
|
|
26
28
|
function makeInitialMessage(model: Model<Api>): AssistantMessage {
|
|
27
29
|
return {
|
|
@@ -54,7 +56,6 @@ export function streamCursor(
|
|
|
54
56
|
|
|
55
57
|
(async () => {
|
|
56
58
|
const partial = makeInitialMessage(model);
|
|
57
|
-
const sdkProcessErrorGuard = installCursorSdkProcessErrorGuard();
|
|
58
59
|
|
|
59
60
|
const runner = new CursorProviderTurnRunner({
|
|
60
61
|
model,
|
|
@@ -66,7 +67,12 @@ export function streamCursor(
|
|
|
66
67
|
});
|
|
67
68
|
|
|
68
69
|
try {
|
|
69
|
-
|
|
70
|
+
stream.push({ type: "start", partial });
|
|
71
|
+
await runExclusiveCursorSessionTurn(
|
|
72
|
+
getCursorSessionScopeKey(),
|
|
73
|
+
() => runner.run(installCursorSdkProcessErrorGuard()),
|
|
74
|
+
options?.signal,
|
|
75
|
+
);
|
|
70
76
|
} catch (error) {
|
|
71
77
|
await runner.handleOuterCatch(error);
|
|
72
78
|
}
|
|
@@ -93,4 +99,5 @@ export const __testUtils = {
|
|
|
93
99
|
resetCursorNativeReplayIdleDisposeMs,
|
|
94
100
|
releaseAllPendingCursorLiveRunsForTests,
|
|
95
101
|
resetSessionCursorAgents: () => disposeAllSessionCursorAgents(),
|
|
102
|
+
resetSessionTurnQueue: cursorSessionTurnQueueTestUtils.reset,
|
|
96
103
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
|
|
2
|
+
|
|
3
|
+
const turnQueuesByScope = new Map<string, Promise<void>>();
|
|
4
|
+
|
|
5
|
+
async function waitForPreviousTurn(previous: Promise<void>, signal?: AbortSignal): Promise<void> {
|
|
6
|
+
if (!signal) {
|
|
7
|
+
await previous.catch(() => undefined);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (signal.aborted) throw new CursorLiveRunAbortError();
|
|
11
|
+
await new Promise<void>((resolve, reject) => {
|
|
12
|
+
const onAbort = (): void => {
|
|
13
|
+
reject(new CursorLiveRunAbortError());
|
|
14
|
+
};
|
|
15
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
16
|
+
previous.catch(() => undefined).then(() => {
|
|
17
|
+
signal.removeEventListener("abort", onAbort);
|
|
18
|
+
resolve();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runExclusiveCursorSessionTurn<T>(scopeKey: string, body: () => Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
24
|
+
const previous = turnQueuesByScope.get(scopeKey);
|
|
25
|
+
let releaseCurrent!: () => void;
|
|
26
|
+
const current = new Promise<void>((resolve) => {
|
|
27
|
+
releaseCurrent = resolve;
|
|
28
|
+
});
|
|
29
|
+
const tail = (previous ?? Promise.resolve()).catch(() => undefined).then(() => current);
|
|
30
|
+
turnQueuesByScope.set(scopeKey, tail);
|
|
31
|
+
void tail.finally(() => {
|
|
32
|
+
if (turnQueuesByScope.get(scopeKey) === tail) {
|
|
33
|
+
turnQueuesByScope.delete(scopeKey);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
if (previous) await waitForPreviousTurn(previous, signal);
|
|
39
|
+
return await body();
|
|
40
|
+
} finally {
|
|
41
|
+
releaseCurrent();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const __testUtils = {
|
|
46
|
+
reset(): void {
|
|
47
|
+
turnQueuesByScope.clear();
|
|
48
|
+
},
|
|
49
|
+
count(): number {
|
|
50
|
+
return turnQueuesByScope.size;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -8,7 +8,7 @@ export const CURSOR_TOOL_MANIFEST_ENV = "PI_CURSOR_TOOL_MANIFEST";
|
|
|
8
8
|
* See docs/cursor-native-tool-replay.md#sdk-tooltype-replay-matrix.
|
|
9
9
|
*/
|
|
10
10
|
export const CURSOR_HOST_TOOL_MANIFEST_SUMMARY =
|
|
11
|
-
"read
|
|
11
|
+
"read/shell/search/edit/write and other host tools when Cursor exposes them";
|
|
12
12
|
|
|
13
13
|
export function resolveCursorToolManifestEnabled(
|
|
14
14
|
env: Record<string, string | undefined> = process.env,
|
|
@@ -20,23 +20,26 @@ export function buildCursorToolManifestText(options: {
|
|
|
20
20
|
bridgeSnapshot?: CursorPiToolBridgeSnapshot;
|
|
21
21
|
/** When false, bridge is off via PI_CURSOR_PI_TOOL_BRIDGE=0 (not merely empty). */
|
|
22
22
|
piBridgeEnabled?: boolean;
|
|
23
|
+
includePiBridgeGuidance?: boolean;
|
|
23
24
|
} = {}): string {
|
|
24
25
|
const piBridgeEnabled = options.piBridgeEnabled ?? true;
|
|
26
|
+
const includePiBridgeGuidance = options.includePiBridgeGuidance !== false;
|
|
25
27
|
const lines = [
|
|
26
28
|
"Callable tool surfaces this run:",
|
|
27
|
-
`- Cursor
|
|
28
|
-
"-
|
|
29
|
-
"- Pi CLI tool toggles such as --no-tools affect pi tools and bridge exposure only; they do not disable Cursor SDK host tools or configured Cursor MCP.",
|
|
29
|
+
`- Cursor host/MCP: ${CURSOR_HOST_TOOL_MANIFEST_SUMMARY}; configured MCP depends on Cursor settings.`,
|
|
30
|
+
"- Pi tool toggles affect pi tools/bridge exposure only; they do not disable Cursor host/configured MCP tools.",
|
|
30
31
|
];
|
|
31
|
-
const bridgeTools = options.bridgeSnapshot?.tools ?? [];
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
const bridgeTools = includePiBridgeGuidance ? options.bridgeSnapshot?.tools ?? [] : [];
|
|
33
|
+
if (includePiBridgeGuidance) {
|
|
34
|
+
if (!piBridgeEnabled) {
|
|
35
|
+
lines.push("- Pi bridge: disabled (PI_CURSOR_PI_TOOL_BRIDGE=0).");
|
|
36
|
+
} else if (bridgeTools.length === 0) {
|
|
37
|
+
lines.push("- Pi bridge: no pi__* tools exposed this run.");
|
|
38
|
+
} else {
|
|
39
|
+
const names = [...bridgeTools.map((tool) => tool.mcpToolName)].sort().join(", ");
|
|
40
|
+
lines.push(`- Pi bridge: call exposed pi__* MCP names (${names}); pi shows real pi names.`);
|
|
41
|
+
}
|
|
39
42
|
}
|
|
40
|
-
lines.push("- Not callable: cursor-replay-* IDs, pi history
|
|
43
|
+
lines.push("- Not callable: cursor-replay-* IDs, pi history names, transcript labels.");
|
|
41
44
|
return lines.join("\n");
|
|
42
45
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
estimateCursorTextTokens,
|
|
7
7
|
type CursorPromptOptions,
|
|
8
8
|
} from "./context.js";
|
|
9
|
+
import { asRecord, getNumber } from "./cursor-record-utils.js";
|
|
9
10
|
|
|
10
11
|
export interface CursorUsagePromptOptions extends CursorPromptOptions {
|
|
11
12
|
maxInputTokens: number;
|
|
@@ -13,6 +14,13 @@ export interface CursorUsagePromptOptions extends CursorPromptOptions {
|
|
|
13
14
|
imageTokenEstimate: number;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
export interface CursorSdkTurnUsage {
|
|
18
|
+
inputTokens: number;
|
|
19
|
+
outputTokens: number;
|
|
20
|
+
cacheReadTokens: number;
|
|
21
|
+
cacheWriteTokens: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
function getPromptInputTokenBudget(model: Model<Api>): number {
|
|
17
25
|
const outputReserveTokens = Math.min(model.maxTokens, Math.max(1, Math.floor(model.contextWindow * 0.2)));
|
|
18
26
|
return Math.max(1, model.contextWindow - outputReserveTokens);
|
|
@@ -26,6 +34,26 @@ export function getCursorPromptOptions(model: Model<Api>): CursorUsagePromptOpti
|
|
|
26
34
|
};
|
|
27
35
|
}
|
|
28
36
|
|
|
37
|
+
function getNonNegativeTokenCount(record: Record<string, unknown> | undefined, key: string): number | undefined {
|
|
38
|
+
const value = getNumber(record, key);
|
|
39
|
+
return value === undefined ? undefined : Math.max(0, Math.floor(value));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function readCursorSdkTurnUsage(value: unknown): CursorSdkTurnUsage | undefined {
|
|
43
|
+
const record = asRecord(value);
|
|
44
|
+
const inputTokens = getNonNegativeTokenCount(record, "inputTokens");
|
|
45
|
+
const outputTokens = getNonNegativeTokenCount(record, "outputTokens");
|
|
46
|
+
const cacheReadTokens = getNonNegativeTokenCount(record, "cacheReadTokens");
|
|
47
|
+
const cacheWriteTokens = getNonNegativeTokenCount(record, "cacheWriteTokens");
|
|
48
|
+
if (inputTokens === undefined || outputTokens === undefined || cacheReadTokens === undefined || cacheWriteTokens === undefined) return undefined;
|
|
49
|
+
return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function readCursorSdkTurnUsageFromUpdate(update: unknown): CursorSdkTurnUsage | undefined {
|
|
53
|
+
const record = asRecord(update);
|
|
54
|
+
return record?.type === "turn-ended" ? readCursorSdkTurnUsage(record.usage) : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
29
57
|
function stringifyUsageValue(value: unknown): string {
|
|
30
58
|
try {
|
|
31
59
|
return JSON.stringify(value) ?? "";
|
|
@@ -56,11 +84,36 @@ export function estimateCursorContextTotalTokens(partial: AssistantMessage, mode
|
|
|
56
84
|
return estimateCursorContextTokens(withAssistantMessage(context, partial), getCursorPromptOptions(model));
|
|
57
85
|
}
|
|
58
86
|
|
|
59
|
-
export function
|
|
87
|
+
export function applyCursorSdkUsage(partial: AssistantMessage, turnUsage: CursorSdkTurnUsage): void {
|
|
88
|
+
partial.usage.input = turnUsage.inputTokens;
|
|
89
|
+
partial.usage.output = turnUsage.outputTokens;
|
|
90
|
+
partial.usage.cacheRead = turnUsage.cacheReadTokens;
|
|
91
|
+
partial.usage.cacheWrite = turnUsage.cacheWriteTokens;
|
|
92
|
+
partial.usage.totalTokens = turnUsage.inputTokens + turnUsage.outputTokens;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function applyCursorApproximateUsage(partial: AssistantMessage, model: Model<Api>, context: Context, sessionInputTokens: number): void {
|
|
60
96
|
const outputTokens = estimateCursorAssistantSessionOutputTokens(partial);
|
|
61
97
|
partial.usage.input = Math.max(0, sessionInputTokens);
|
|
62
98
|
partial.usage.output = outputTokens;
|
|
63
99
|
partial.usage.cacheRead = 0;
|
|
64
100
|
partial.usage.cacheWrite = 0;
|
|
65
|
-
partial.usage.totalTokens =
|
|
101
|
+
partial.usage.totalTokens = Math.max(
|
|
102
|
+
partial.usage.input + partial.usage.output,
|
|
103
|
+
estimateCursorContextTotalTokens(partial, model, context),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function applyCursorUsage(
|
|
108
|
+
partial: AssistantMessage,
|
|
109
|
+
model: Model<Api>,
|
|
110
|
+
context: Context,
|
|
111
|
+
sessionInputTokens: number,
|
|
112
|
+
sdkUsage?: { turn?: CursorSdkTurnUsage },
|
|
113
|
+
): void {
|
|
114
|
+
if (sdkUsage?.turn) {
|
|
115
|
+
applyCursorSdkUsage(partial, sdkUsage.turn);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
applyCursorApproximateUsage(partial, model, context, sessionInputTokens);
|
|
66
119
|
}
|